From bf6655a534a6775d30cafa67bd801276bda1d98d Mon Sep 17 00:00:00 2001 From: polo Date: Tue, 13 Aug 2024 23:45:21 +0200 Subject: =?UTF-8?q?VERSION=200.2=20doctrine=20ORM=20et=20entit=C3=A9s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- vendor/doctrine/orm/src/AbstractQuery.php | 1116 +++++++ vendor/doctrine/orm/src/Cache.php | 106 + .../orm/src/Cache/AssociationCacheEntry.php | 30 + .../doctrine/orm/src/Cache/CacheConfiguration.php | 60 + vendor/doctrine/orm/src/Cache/CacheEntry.php | 16 + vendor/doctrine/orm/src/Cache/CacheException.php | 26 + vendor/doctrine/orm/src/Cache/CacheFactory.php | 64 + vendor/doctrine/orm/src/Cache/CacheKey.php | 16 + .../orm/src/Cache/CollectionCacheEntry.php | 25 + .../doctrine/orm/src/Cache/CollectionCacheKey.php | 39 + .../doctrine/orm/src/Cache/CollectionHydrator.php | 21 + vendor/doctrine/orm/src/Cache/ConcurrentRegion.php | 36 + vendor/doctrine/orm/src/Cache/DefaultCache.php | 245 ++ .../doctrine/orm/src/Cache/DefaultCacheFactory.php | 189 ++ .../orm/src/Cache/DefaultCollectionHydrator.php | 75 + .../orm/src/Cache/DefaultEntityHydrator.php | 176 ++ .../doctrine/orm/src/Cache/DefaultQueryCache.php | 414 +++ vendor/doctrine/orm/src/Cache/EntityCacheEntry.php | 50 + vendor/doctrine/orm/src/Cache/EntityCacheKey.php | 38 + vendor/doctrine/orm/src/Cache/EntityHydrator.php | 28 + .../orm/src/Cache/Exception/CacheException.php | 14 + .../Exception/CannotUpdateReadOnlyCollection.php | 19 + .../Cache/Exception/CannotUpdateReadOnlyEntity.php | 15 + .../src/Cache/Exception/FeatureNotImplemented.php | 23 + .../orm/src/Cache/Exception/NonCacheableEntity.php | 18 + .../Exception/NonCacheableEntityAssociation.php | 19 + vendor/doctrine/orm/src/Cache/Lock.php | 25 + vendor/doctrine/orm/src/Cache/LockException.php | 14 + .../doctrine/orm/src/Cache/Logging/CacheLogger.php | 60 + .../orm/src/Cache/Logging/CacheLoggerChain.php | 94 + .../src/Cache/Logging/StatisticsCacheLogger.php | 174 ++ .../orm/src/Cache/Persister/CachedPersister.php | 25 + .../Collection/AbstractCollectionPersister.php | 168 + .../Collection/CachedCollectionPersister.php | 36 + ...NonStrictReadWriteCachedCollectionPersister.php | 74 + .../ReadOnlyCachedCollectionPersister.php | 24 + .../ReadWriteCachedCollectionPersister.php | 103 + .../Persister/Entity/AbstractEntityPersister.php | 557 ++++ .../Persister/Entity/CachedEntityPersister.php | 20 + .../NonStrictReadWriteCachedEntityPersister.php | 85 + .../Entity/ReadOnlyCachedEntityPersister.php | 19 + .../Entity/ReadWriteCachedEntityPersister.php | 105 + vendor/doctrine/orm/src/Cache/QueryCache.php | 28 + vendor/doctrine/orm/src/Cache/QueryCacheEntry.php | 29 + vendor/doctrine/orm/src/Cache/QueryCacheKey.php | 23 + .../doctrine/orm/src/Cache/QueryCacheValidator.php | 16 + vendor/doctrine/orm/src/Cache/Region.php | 73 + .../orm/src/Cache/Region/DefaultRegion.php | 113 + .../orm/src/Cache/Region/FileLockRegion.php | 194 ++ .../orm/src/Cache/Region/UpdateTimestampCache.php | 20 + .../orm/src/Cache/RegionsConfiguration.php | 63 + .../doctrine/orm/src/Cache/TimestampCacheEntry.php | 29 + .../doctrine/orm/src/Cache/TimestampCacheKey.php | 17 + .../orm/src/Cache/TimestampQueryCacheValidator.php | 38 + vendor/doctrine/orm/src/Cache/TimestampRegion.php | 18 + vendor/doctrine/orm/src/Configuration.php | 649 ++++ .../orm/src/Decorator/EntityManagerDecorator.php | 174 ++ vendor/doctrine/orm/src/EntityManager.php | 626 ++++ vendor/doctrine/orm/src/EntityManagerInterface.php | 242 ++ .../doctrine/orm/src/EntityNotFoundException.php | 46 + vendor/doctrine/orm/src/EntityRepository.php | 236 ++ vendor/doctrine/orm/src/Event/ListenersInvoker.php | 98 + .../orm/src/Event/LoadClassMetadataEventArgs.php | 25 + .../src/Event/OnClassMetadataNotFoundEventArgs.php | 49 + vendor/doctrine/orm/src/Event/OnClearEventArgs.php | 19 + vendor/doctrine/orm/src/Event/OnFlushEventArgs.php | 19 + .../doctrine/orm/src/Event/PostFlushEventArgs.php | 19 + .../doctrine/orm/src/Event/PostLoadEventArgs.php | 13 + .../orm/src/Event/PostPersistEventArgs.php | 13 + .../doctrine/orm/src/Event/PostRemoveEventArgs.php | 13 + .../doctrine/orm/src/Event/PostUpdateEventArgs.php | 13 + .../doctrine/orm/src/Event/PreFlushEventArgs.php | 19 + .../doctrine/orm/src/Event/PrePersistEventArgs.php | 13 + .../doctrine/orm/src/Event/PreRemoveEventArgs.php | 13 + .../doctrine/orm/src/Event/PreUpdateEventArgs.php | 100 + vendor/doctrine/orm/src/Events.php | 125 + .../orm/src/Exception/ConfigurationException.php | 9 + .../Exception/EntityIdentityCollisionException.php | 39 + .../orm/src/Exception/EntityManagerClosed.php | 15 + .../orm/src/Exception/EntityMissingAssignedId.php | 20 + .../orm/src/Exception/InvalidEntityRepository.php | 18 + .../orm/src/Exception/InvalidHydrationMode.php | 17 + .../orm/src/Exception/ManagerException.php | 11 + .../orm/src/Exception/MissingIdentifierField.php | 21 + .../MissingMappingDriverImplementation.php | 18 + .../Exception/MultipleSelectorsFoundException.php | 26 + vendor/doctrine/orm/src/Exception/NotSupported.php | 44 + vendor/doctrine/orm/src/Exception/ORMException.php | 11 + .../orm/src/Exception/PersisterException.php | 11 + .../orm/src/Exception/RepositoryException.php | 13 + .../orm/src/Exception/SchemaToolException.php | 11 + .../src/Exception/UnexpectedAssociationValue.php | 27 + .../src/Exception/UnrecognizedIdentifierFields.php | 23 + vendor/doctrine/orm/src/Id/AbstractIdGenerator.php | 28 + vendor/doctrine/orm/src/Id/AssignedGenerator.php | 45 + .../orm/src/Id/BigIntegerIdentityGenerator.php | 25 + vendor/doctrine/orm/src/Id/IdentityGenerator.php | 25 + vendor/doctrine/orm/src/Id/SequenceGenerator.php | 112 + .../src/Internal/Hydration/AbstractHydrator.php | 556 ++++ .../orm/src/Internal/Hydration/ArrayHydrator.php | 270 ++ .../src/Internal/Hydration/HydrationException.php | 67 + .../orm/src/Internal/Hydration/ObjectHydrator.php | 586 ++++ .../Internal/Hydration/ScalarColumnHydrator.php | 34 + .../orm/src/Internal/Hydration/ScalarHydrator.php | 35 + .../Internal/Hydration/SimpleObjectHydrator.php | 176 ++ .../Internal/Hydration/SingleScalarHydrator.php | 40 + .../orm/src/Internal/HydrationCompleteHandler.php | 64 + .../orm/src/Internal/NoUnknownNamedArguments.php | 55 + vendor/doctrine/orm/src/Internal/QueryType.php | 13 + .../doctrine/orm/src/Internal/SQLResultCasing.php | 30 + .../src/Internal/StronglyConnectedComponents.php | 159 + .../doctrine/orm/src/Internal/TopologicalSort.php | 155 + .../TopologicalSort/CycleDetectedException.php | 47 + vendor/doctrine/orm/src/LazyCriteriaCollection.php | 96 + .../doctrine/orm/src/Mapping/AnsiQuoteStrategy.php | 76 + .../orm/src/Mapping/ArrayAccessImplementation.php | 70 + .../orm/src/Mapping/AssociationMapping.php | 359 +++ .../orm/src/Mapping/AssociationOverride.php | 51 + .../orm/src/Mapping/AssociationOverrides.php | 38 + .../doctrine/orm/src/Mapping/AttributeOverride.php | 15 + .../orm/src/Mapping/AttributeOverrides.php | 38 + .../orm/src/Mapping/Builder/AssociationBuilder.php | 171 + .../src/Mapping/Builder/ClassMetadataBuilder.php | 426 +++ .../orm/src/Mapping/Builder/EmbeddedBuilder.php | 46 + .../src/Mapping/Builder/EntityListenerBuilder.php | 55 + .../orm/src/Mapping/Builder/FieldBuilder.php | 243 ++ .../Builder/ManyToManyAssociationBuilder.php | 73 + .../Builder/OneToManyAssociationBuilder.php | 46 + vendor/doctrine/orm/src/Mapping/Cache.php | 19 + .../orm/src/Mapping/ChainTypedFieldMapper.php | 35 + .../orm/src/Mapping/ChangeTrackingPolicy.php | 17 + vendor/doctrine/orm/src/Mapping/ClassMetadata.php | 2649 ++++++++++++++++ .../orm/src/Mapping/ClassMetadataFactory.php | 729 +++++ vendor/doctrine/orm/src/Mapping/Column.php | 36 + .../doctrine/orm/src/Mapping/CustomIdGenerator.php | 16 + .../src/Mapping/DefaultEntityListenerResolver.php | 40 + .../orm/src/Mapping/DefaultNamingStrategy.php | 68 + .../orm/src/Mapping/DefaultQuoteStrategy.php | 145 + .../orm/src/Mapping/DefaultTypedFieldMapper.php | 80 + .../orm/src/Mapping/DiscriminatorColumn.php | 24 + .../orm/src/Mapping/DiscriminatorColumnMapping.php | 83 + .../doctrine/orm/src/Mapping/DiscriminatorMap.php | 17 + .../orm/src/Mapping/Driver/AttributeDriver.php | 768 +++++ .../orm/src/Mapping/Driver/AttributeReader.php | 146 + .../orm/src/Mapping/Driver/DatabaseDriver.php | 528 ++++ .../src/Mapping/Driver/ReflectionBasedDriver.php | 44 + .../Driver/RepeatableAttributeCollection.php | 16 + .../orm/src/Mapping/Driver/SimplifiedXmlDriver.php | 25 + .../doctrine/orm/src/Mapping/Driver/XmlDriver.php | 940 ++++++ vendor/doctrine/orm/src/Mapping/Embeddable.php | 12 + vendor/doctrine/orm/src/Mapping/Embedded.php | 17 + .../orm/src/Mapping/EmbeddedClassMapping.php | 93 + vendor/doctrine/orm/src/Mapping/Entity.php | 20 + .../orm/src/Mapping/EntityListenerResolver.php | 30 + .../doctrine/orm/src/Mapping/EntityListeners.php | 21 + .../Mapping/Exception/InvalidCustomGenerator.php | 28 + .../src/Mapping/Exception/UnknownGeneratorType.php | 16 + vendor/doctrine/orm/src/Mapping/FieldMapping.php | 169 + vendor/doctrine/orm/src/Mapping/GeneratedValue.php | 17 + .../orm/src/Mapping/HasLifecycleCallbacks.php | 12 + vendor/doctrine/orm/src/Mapping/Id.php | 12 + vendor/doctrine/orm/src/Mapping/Index.php | 26 + .../doctrine/orm/src/Mapping/InheritanceType.php | 17 + .../doctrine/orm/src/Mapping/InverseJoinColumn.php | 13 + .../orm/src/Mapping/InverseSideMapping.php | 30 + vendor/doctrine/orm/src/Mapping/JoinColumn.php | 13 + .../doctrine/orm/src/Mapping/JoinColumnMapping.php | 77 + .../orm/src/Mapping/JoinColumnProperties.php | 21 + vendor/doctrine/orm/src/Mapping/JoinColumns.php | 14 + vendor/doctrine/orm/src/Mapping/JoinTable.php | 35 + .../doctrine/orm/src/Mapping/JoinTableMapping.php | 115 + vendor/doctrine/orm/src/Mapping/ManyToMany.php | 27 + .../src/Mapping/ManyToManyAssociationMapping.php | 9 + .../src/Mapping/ManyToManyInverseSideMapping.php | 9 + .../src/Mapping/ManyToManyOwningSideMapping.php | 185 ++ vendor/doctrine/orm/src/Mapping/ManyToOne.php | 24 + .../src/Mapping/ManyToOneAssociationMapping.php | 12 + .../doctrine/orm/src/Mapping/MappedSuperclass.php | 18 + .../doctrine/orm/src/Mapping/MappingAttribute.php | 10 + .../doctrine/orm/src/Mapping/MappingException.php | 691 +++++ vendor/doctrine/orm/src/Mapping/NamingStrategy.php | 71 + vendor/doctrine/orm/src/Mapping/OneToMany.php | 26 + .../src/Mapping/OneToManyAssociationMapping.php | 75 + vendor/doctrine/orm/src/Mapping/OneToOne.php | 26 + .../orm/src/Mapping/OneToOneAssociationMapping.php | 9 + .../orm/src/Mapping/OneToOneInverseSideMapping.php | 9 + .../orm/src/Mapping/OneToOneOwningSideMapping.php | 9 + vendor/doctrine/orm/src/Mapping/OrderBy.php | 17 + .../doctrine/orm/src/Mapping/OwningSideMapping.php | 28 + vendor/doctrine/orm/src/Mapping/PostLoad.php | 12 + vendor/doctrine/orm/src/Mapping/PostPersist.php | 12 + vendor/doctrine/orm/src/Mapping/PostRemove.php | 12 + vendor/doctrine/orm/src/Mapping/PostUpdate.php | 12 + vendor/doctrine/orm/src/Mapping/PreFlush.php | 12 + vendor/doctrine/orm/src/Mapping/PrePersist.php | 12 + vendor/doctrine/orm/src/Mapping/PreRemove.php | 12 + vendor/doctrine/orm/src/Mapping/PreUpdate.php | 12 + vendor/doctrine/orm/src/Mapping/QuoteStrategy.php | 68 + .../orm/src/Mapping/ReflectionEmbeddedProperty.php | 61 + .../orm/src/Mapping/ReflectionEnumProperty.php | 87 + .../orm/src/Mapping/ReflectionReadonlyProperty.php | 49 + .../doctrine/orm/src/Mapping/SequenceGenerator.php | 18 + vendor/doctrine/orm/src/Mapping/Table.php | 45 + .../orm/src/Mapping/ToManyAssociationMapping.php | 16 + .../ToManyAssociationMappingImplementation.php | 69 + .../orm/src/Mapping/ToManyInverseSideMapping.php | 10 + .../orm/src/Mapping/ToManyOwningSideMapping.php | 10 + .../orm/src/Mapping/ToOneAssociationMapping.php | 9 + .../orm/src/Mapping/ToOneInverseSideMapping.php | 52 + .../orm/src/Mapping/ToOneOwningSideMapping.php | 212 ++ .../doctrine/orm/src/Mapping/TypedFieldMapper.php | 20 + .../orm/src/Mapping/UnderscoreNamingStrategy.php | 108 + .../doctrine/orm/src/Mapping/UniqueConstraint.php | 24 + vendor/doctrine/orm/src/Mapping/Version.php | 12 + vendor/doctrine/orm/src/NativeQuery.php | 68 + vendor/doctrine/orm/src/NoResultException.php | 16 + .../doctrine/orm/src/NonUniqueResultException.php | 18 + .../orm/src/ORMInvalidArgumentException.php | 195 ++ vendor/doctrine/orm/src/ORMSetup.php | 127 + .../doctrine/orm/src/OptimisticLockException.php | 55 + vendor/doctrine/orm/src/PersistentCollection.php | 652 ++++ .../Collection/AbstractCollectionPersister.php | 50 + .../Persisters/Collection/CollectionPersister.php | 59 + .../Persisters/Collection/ManyToManyPersister.php | 770 +++++ .../Persisters/Collection/OneToManyPersister.php | 264 ++ .../Entity/AbstractEntityInheritancePersister.php | 66 + .../src/Persisters/Entity/BasicEntityPersister.php | 2085 +++++++++++++ .../Persisters/Entity/CachedPersisterContext.php | 60 + .../orm/src/Persisters/Entity/EntityPersister.php | 298 ++ .../Persisters/Entity/JoinedSubclassPersister.php | 601 ++++ .../src/Persisters/Entity/SingleTablePersister.php | 166 + .../Exception/CantUseInOperatorOnCompositeKeys.php | 15 + .../Persisters/Exception/InvalidOrientation.php | 15 + .../src/Persisters/Exception/UnrecognizedField.php | 24 + .../MatchingAssociationFieldRequiresObject.php | 22 + .../orm/src/Persisters/PersisterException.php | 23 + .../orm/src/Persisters/SqlExpressionVisitor.php | 79 + .../orm/src/Persisters/SqlValueVisitor.php | 88 + .../doctrine/orm/src/PessimisticLockException.php | 16 + vendor/doctrine/orm/src/Proxy/Autoloader.php | 86 + .../src/Proxy/DefaultProxyClassNameResolver.php | 35 + vendor/doctrine/orm/src/Proxy/InternalProxy.php | 18 + vendor/doctrine/orm/src/Proxy/NotAProxyClass.php | 22 + vendor/doctrine/orm/src/Proxy/ProxyFactory.php | 439 +++ vendor/doctrine/orm/src/Query.php | 682 ++++ vendor/doctrine/orm/src/Query/AST/ASTException.php | 20 + .../orm/src/Query/AST/AggregateExpression.php | 23 + .../orm/src/Query/AST/ArithmeticExpression.php | 34 + .../orm/src/Query/AST/ArithmeticFactor.php | 36 + .../doctrine/orm/src/Query/AST/ArithmeticTerm.php | 25 + .../orm/src/Query/AST/BetweenExpression.php | 23 + .../orm/src/Query/AST/CoalesceExpression.php | 25 + .../src/Query/AST/CollectionMemberExpression.php | 27 + .../orm/src/Query/AST/ComparisonExpression.php | 32 + .../orm/src/Query/AST/ConditionalExpression.php | 25 + .../orm/src/Query/AST/ConditionalFactor.php | 26 + .../orm/src/Query/AST/ConditionalPrimary.php | 34 + .../doctrine/orm/src/Query/AST/ConditionalTerm.php | 25 + vendor/doctrine/orm/src/Query/AST/DeleteClause.php | 26 + .../doctrine/orm/src/Query/AST/DeleteStatement.php | 26 + .../AST/EmptyCollectionComparisonExpression.php | 26 + .../orm/src/Query/AST/ExistsExpression.php | 26 + vendor/doctrine/orm/src/Query/AST/FromClause.php | 25 + .../orm/src/Query/AST/Functions/AbsFunction.php | 37 + .../orm/src/Query/AST/Functions/AvgFunction.php | 27 + .../orm/src/Query/AST/Functions/BitAndFunction.php | 43 + .../orm/src/Query/AST/Functions/BitOrFunction.php | 43 + .../orm/src/Query/AST/Functions/ConcatFunction.php | 58 + .../orm/src/Query/AST/Functions/CountFunction.php | 35 + .../Query/AST/Functions/CurrentDateFunction.php | 29 + .../Query/AST/Functions/CurrentTimeFunction.php | 29 + .../AST/Functions/CurrentTimestampFunction.php | 29 + .../src/Query/AST/Functions/DateAddFunction.php | 83 + .../src/Query/AST/Functions/DateDiffFunction.php | 41 + .../src/Query/AST/Functions/DateSubFunction.php | 62 + .../orm/src/Query/AST/Functions/FunctionNode.php | 32 + .../src/Query/AST/Functions/IdentityFunction.php | 90 + .../orm/src/Query/AST/Functions/LengthFunction.php | 45 + .../orm/src/Query/AST/Functions/LocateFunction.php | 62 + .../orm/src/Query/AST/Functions/LowerFunction.php | 40 + .../orm/src/Query/AST/Functions/MaxFunction.php | 27 + .../orm/src/Query/AST/Functions/MinFunction.php | 27 + .../orm/src/Query/AST/Functions/ModFunction.php | 43 + .../orm/src/Query/AST/Functions/SizeFunction.php | 113 + .../orm/src/Query/AST/Functions/SqrtFunction.php | 40 + .../src/Query/AST/Functions/SubstringFunction.php | 58 + .../orm/src/Query/AST/Functions/SumFunction.php | 27 + .../orm/src/Query/AST/Functions/TrimFunction.php | 119 + .../orm/src/Query/AST/Functions/UpperFunction.php | 40 + .../orm/src/Query/AST/GeneralCaseExpression.php | 27 + .../doctrine/orm/src/Query/AST/GroupByClause.php | 20 + vendor/doctrine/orm/src/Query/AST/HavingClause.php | 19 + .../AST/IdentificationVariableDeclaration.php | 28 + .../orm/src/Query/AST/InListExpression.php | 23 + .../orm/src/Query/AST/InSubselectExpression.php | 22 + vendor/doctrine/orm/src/Query/AST/IndexBy.php | 26 + .../doctrine/orm/src/Query/AST/InputParameter.php | 35 + .../orm/src/Query/AST/InstanceOfExpression.php | 29 + vendor/doctrine/orm/src/Query/AST/Join.php | 34 + .../src/Query/AST/JoinAssociationDeclaration.php | 27 + .../Query/AST/JoinAssociationPathExpression.php | 19 + .../orm/src/Query/AST/JoinClassPathExpression.php | 26 + .../orm/src/Query/AST/JoinVariableDeclaration.php | 24 + .../doctrine/orm/src/Query/AST/LikeExpression.php | 29 + vendor/doctrine/orm/src/Query/AST/Literal.php | 26 + .../orm/src/Query/AST/NewObjectExpression.php | 25 + vendor/doctrine/orm/src/Query/AST/Node.php | 85 + .../orm/src/Query/AST/NullComparisonExpression.php | 26 + .../orm/src/Query/AST/NullIfExpression.php | 24 + .../doctrine/orm/src/Query/AST/OrderByClause.php | 25 + vendor/doctrine/orm/src/Query/AST/OrderByItem.php | 38 + .../orm/src/Query/AST/ParenthesisExpression.php | 22 + .../doctrine/orm/src/Query/AST/PathExpression.php | 39 + .../src/Query/AST/Phase2OptimizableConditional.php | 17 + .../orm/src/Query/AST/QuantifiedExpression.php | 43 + .../orm/src/Query/AST/RangeVariableDeclaration.php | 27 + vendor/doctrine/orm/src/Query/AST/SelectClause.php | 27 + .../orm/src/Query/AST/SelectExpression.php | 28 + .../doctrine/orm/src/Query/AST/SelectStatement.php | 32 + .../src/Query/AST/SimpleArithmeticExpression.php | 25 + .../orm/src/Query/AST/SimpleCaseExpression.php | 28 + .../orm/src/Query/AST/SimpleSelectClause.php | 26 + .../orm/src/Query/AST/SimpleSelectExpression.php | 27 + .../orm/src/Query/AST/SimpleWhenClause.php | 26 + vendor/doctrine/orm/src/Query/AST/Subselect.php | 32 + .../orm/src/Query/AST/SubselectFromClause.php | 25 + .../SubselectIdentificationVariableDeclaration.php | 19 + .../doctrine/orm/src/Query/AST/TypedExpression.php | 15 + vendor/doctrine/orm/src/Query/AST/UpdateClause.php | 29 + vendor/doctrine/orm/src/Query/AST/UpdateItem.php | 26 + .../doctrine/orm/src/Query/AST/UpdateStatement.php | 26 + vendor/doctrine/orm/src/Query/AST/WhenClause.php | 26 + vendor/doctrine/orm/src/Query/AST/WhereClause.php | 24 + .../orm/src/Query/Exec/AbstractSqlExecutor.php | 61 + .../src/Query/Exec/MultiTableDeleteExecutor.php | 131 + .../src/Query/Exec/MultiTableUpdateExecutor.php | 180 ++ .../orm/src/Query/Exec/SingleSelectExecutor.php | 31 + .../Query/Exec/SingleTableDeleteUpdateExecutor.php | 42 + vendor/doctrine/orm/src/Query/Expr.php | 615 ++++ vendor/doctrine/orm/src/Query/Expr/Andx.php | 32 + vendor/doctrine/orm/src/Query/Expr/Base.php | 96 + vendor/doctrine/orm/src/Query/Expr/Comparison.php | 47 + vendor/doctrine/orm/src/Query/Expr/Composite.php | 50 + vendor/doctrine/orm/src/Query/Expr/From.php | 48 + vendor/doctrine/orm/src/Query/Expr/Func.php | 48 + vendor/doctrine/orm/src/Query/Expr/GroupBy.php | 25 + vendor/doctrine/orm/src/Query/Expr/Join.php | 77 + vendor/doctrine/orm/src/Query/Expr/Literal.php | 25 + vendor/doctrine/orm/src/Query/Expr/Math.php | 59 + vendor/doctrine/orm/src/Query/Expr/OrderBy.php | 60 + vendor/doctrine/orm/src/Query/Expr/Orx.php | 32 + vendor/doctrine/orm/src/Query/Expr/Select.php | 28 + .../orm/src/Query/Filter/FilterException.php | 23 + vendor/doctrine/orm/src/Query/Filter/SQLFilter.php | 174 ++ vendor/doctrine/orm/src/Query/FilterCollection.php | 260 ++ vendor/doctrine/orm/src/Query/Lexer.php | 150 + vendor/doctrine/orm/src/Query/Parameter.php | 89 + .../orm/src/Query/ParameterTypeInferer.php | 77 + vendor/doctrine/orm/src/Query/Parser.php | 3269 ++++++++++++++++++++ vendor/doctrine/orm/src/Query/ParserResult.php | 118 + vendor/doctrine/orm/src/Query/Printer.php | 64 + vendor/doctrine/orm/src/Query/QueryException.php | 155 + .../orm/src/Query/QueryExpressionVisitor.php | 180 ++ vendor/doctrine/orm/src/Query/ResultSetMapping.php | 547 ++++ .../orm/src/Query/ResultSetMappingBuilder.php | 281 ++ vendor/doctrine/orm/src/Query/SqlWalker.php | 2264 ++++++++++++++ vendor/doctrine/orm/src/Query/TokenType.php | 91 + vendor/doctrine/orm/src/Query/TreeWalker.php | 44 + .../doctrine/orm/src/Query/TreeWalkerAdapter.php | 90 + vendor/doctrine/orm/src/Query/TreeWalkerChain.php | 88 + vendor/doctrine/orm/src/QueryBuilder.php | 1375 ++++++++ .../src/Repository/DefaultRepositoryFactory.php | 49 + .../src/Repository/Exception/InvalidFindByCall.php | 21 + .../Exception/InvalidMagicMethodCall.php | 27 + .../orm/src/Repository/RepositoryFactory.php | 26 + .../src/Tools/AttachEntityListenersListener.php | 69 + .../Command/AbstractEntityManagerCommand.php | 25 + .../Command/ClearCache/CollectionRegionCommand.php | 119 + .../Command/ClearCache/EntityRegionCommand.php | 110 + .../Console/Command/ClearCache/MetadataCommand.php | 52 + .../Console/Command/ClearCache/QueryCommand.php | 54 + .../Command/ClearCache/QueryRegionCommand.php | 101 + .../Console/Command/ClearCache/ResultCommand.php | 65 + .../Console/Command/GenerateProxiesCommand.php | 96 + .../orm/src/Tools/Console/Command/InfoCommand.php | 80 + .../Console/Command/MappingDescribeCommand.php | 279 ++ .../src/Tools/Console/Command/RunDqlCommand.php | 118 + .../Console/Command/SchemaTool/AbstractCommand.php | 39 + .../Console/Command/SchemaTool/CreateCommand.php | 75 + .../Console/Command/SchemaTool/DropCommand.php | 116 + .../Console/Command/SchemaTool/UpdateCommand.php | 147 + .../Console/Command/ValidateSchemaCommand.php | 89 + .../orm/src/Tools/Console/ConsoleRunner.php | 88 + .../src/Tools/Console/EntityManagerProvider.php | 14 + .../ConnectionFromManagerProvider.php | 26 + .../SingleManagerProvider.php | 31 + .../UnknownManagerException.php | 23 + .../orm/src/Tools/Console/MetadataFilter.php | 92 + vendor/doctrine/orm/src/Tools/Debug.php | 158 + .../orm/src/Tools/DebugUnitOfWorkListener.php | 144 + .../src/Tools/Event/GenerateSchemaEventArgs.php | 33 + .../Tools/Event/GenerateSchemaTableEventArgs.php | 40 + .../src/Tools/Exception/MissingColumnException.php | 23 + .../orm/src/Tools/Exception/NotSupported.php | 16 + .../orm/src/Tools/Pagination/CountOutputWalker.php | 125 + .../orm/src/Tools/Pagination/CountWalker.php | 68 + .../Exception/RowNumberOverFunctionNotEnabled.php | 16 + .../Tools/Pagination/LimitSubqueryOutputWalker.php | 544 ++++ .../src/Tools/Pagination/LimitSubqueryWalker.php | 155 + .../orm/src/Tools/Pagination/Paginator.php | 263 ++ .../orm/src/Tools/Pagination/RootTypeWalker.php | 48 + .../src/Tools/Pagination/RowNumberOverFunction.php | 40 + .../orm/src/Tools/Pagination/WhereInWalker.php | 116 + .../orm/src/Tools/ResolveTargetEntityListener.php | 117 + vendor/doctrine/orm/src/Tools/SchemaTool.php | 932 ++++++ vendor/doctrine/orm/src/Tools/SchemaValidator.php | 443 +++ vendor/doctrine/orm/src/Tools/ToolEvents.php | 23 + vendor/doctrine/orm/src/Tools/ToolsException.php | 24 + .../orm/src/TransactionRequiredException.php | 21 + .../doctrine/orm/src/UnexpectedResultException.php | 15 + vendor/doctrine/orm/src/UnitOfWork.php | 3252 +++++++++++++++++++ .../src/Utility/HierarchyDiscriminatorResolver.php | 44 + .../orm/src/Utility/IdentifierFlattener.php | 83 + vendor/doctrine/orm/src/Utility/LockSqlHelper.php | 35 + .../doctrine/orm/src/Utility/PersisterHelper.php | 108 + 425 files changed, 51805 insertions(+) create mode 100644 vendor/doctrine/orm/src/AbstractQuery.php create mode 100644 vendor/doctrine/orm/src/Cache.php create mode 100644 vendor/doctrine/orm/src/Cache/AssociationCacheEntry.php create mode 100644 vendor/doctrine/orm/src/Cache/CacheConfiguration.php create mode 100644 vendor/doctrine/orm/src/Cache/CacheEntry.php create mode 100644 vendor/doctrine/orm/src/Cache/CacheException.php create mode 100644 vendor/doctrine/orm/src/Cache/CacheFactory.php create mode 100644 vendor/doctrine/orm/src/Cache/CacheKey.php create mode 100644 vendor/doctrine/orm/src/Cache/CollectionCacheEntry.php create mode 100644 vendor/doctrine/orm/src/Cache/CollectionCacheKey.php create mode 100644 vendor/doctrine/orm/src/Cache/CollectionHydrator.php create mode 100644 vendor/doctrine/orm/src/Cache/ConcurrentRegion.php create mode 100644 vendor/doctrine/orm/src/Cache/DefaultCache.php create mode 100644 vendor/doctrine/orm/src/Cache/DefaultCacheFactory.php create mode 100644 vendor/doctrine/orm/src/Cache/DefaultCollectionHydrator.php create mode 100644 vendor/doctrine/orm/src/Cache/DefaultEntityHydrator.php create mode 100644 vendor/doctrine/orm/src/Cache/DefaultQueryCache.php create mode 100644 vendor/doctrine/orm/src/Cache/EntityCacheEntry.php create mode 100644 vendor/doctrine/orm/src/Cache/EntityCacheKey.php create mode 100644 vendor/doctrine/orm/src/Cache/EntityHydrator.php create mode 100644 vendor/doctrine/orm/src/Cache/Exception/CacheException.php create mode 100644 vendor/doctrine/orm/src/Cache/Exception/CannotUpdateReadOnlyCollection.php create mode 100644 vendor/doctrine/orm/src/Cache/Exception/CannotUpdateReadOnlyEntity.php create mode 100644 vendor/doctrine/orm/src/Cache/Exception/FeatureNotImplemented.php create mode 100644 vendor/doctrine/orm/src/Cache/Exception/NonCacheableEntity.php create mode 100644 vendor/doctrine/orm/src/Cache/Exception/NonCacheableEntityAssociation.php create mode 100644 vendor/doctrine/orm/src/Cache/Lock.php create mode 100644 vendor/doctrine/orm/src/Cache/LockException.php create mode 100644 vendor/doctrine/orm/src/Cache/Logging/CacheLogger.php create mode 100644 vendor/doctrine/orm/src/Cache/Logging/CacheLoggerChain.php create mode 100644 vendor/doctrine/orm/src/Cache/Logging/StatisticsCacheLogger.php create mode 100644 vendor/doctrine/orm/src/Cache/Persister/CachedPersister.php create mode 100644 vendor/doctrine/orm/src/Cache/Persister/Collection/AbstractCollectionPersister.php create mode 100644 vendor/doctrine/orm/src/Cache/Persister/Collection/CachedCollectionPersister.php create mode 100644 vendor/doctrine/orm/src/Cache/Persister/Collection/NonStrictReadWriteCachedCollectionPersister.php create mode 100644 vendor/doctrine/orm/src/Cache/Persister/Collection/ReadOnlyCachedCollectionPersister.php create mode 100644 vendor/doctrine/orm/src/Cache/Persister/Collection/ReadWriteCachedCollectionPersister.php create mode 100644 vendor/doctrine/orm/src/Cache/Persister/Entity/AbstractEntityPersister.php create mode 100644 vendor/doctrine/orm/src/Cache/Persister/Entity/CachedEntityPersister.php create mode 100644 vendor/doctrine/orm/src/Cache/Persister/Entity/NonStrictReadWriteCachedEntityPersister.php create mode 100644 vendor/doctrine/orm/src/Cache/Persister/Entity/ReadOnlyCachedEntityPersister.php create mode 100644 vendor/doctrine/orm/src/Cache/Persister/Entity/ReadWriteCachedEntityPersister.php create mode 100644 vendor/doctrine/orm/src/Cache/QueryCache.php create mode 100644 vendor/doctrine/orm/src/Cache/QueryCacheEntry.php create mode 100644 vendor/doctrine/orm/src/Cache/QueryCacheKey.php create mode 100644 vendor/doctrine/orm/src/Cache/QueryCacheValidator.php create mode 100644 vendor/doctrine/orm/src/Cache/Region.php create mode 100644 vendor/doctrine/orm/src/Cache/Region/DefaultRegion.php create mode 100644 vendor/doctrine/orm/src/Cache/Region/FileLockRegion.php create mode 100644 vendor/doctrine/orm/src/Cache/Region/UpdateTimestampCache.php create mode 100644 vendor/doctrine/orm/src/Cache/RegionsConfiguration.php create mode 100644 vendor/doctrine/orm/src/Cache/TimestampCacheEntry.php create mode 100644 vendor/doctrine/orm/src/Cache/TimestampCacheKey.php create mode 100644 vendor/doctrine/orm/src/Cache/TimestampQueryCacheValidator.php create mode 100644 vendor/doctrine/orm/src/Cache/TimestampRegion.php create mode 100644 vendor/doctrine/orm/src/Configuration.php create mode 100644 vendor/doctrine/orm/src/Decorator/EntityManagerDecorator.php create mode 100644 vendor/doctrine/orm/src/EntityManager.php create mode 100644 vendor/doctrine/orm/src/EntityManagerInterface.php create mode 100644 vendor/doctrine/orm/src/EntityNotFoundException.php create mode 100644 vendor/doctrine/orm/src/EntityRepository.php create mode 100644 vendor/doctrine/orm/src/Event/ListenersInvoker.php create mode 100644 vendor/doctrine/orm/src/Event/LoadClassMetadataEventArgs.php create mode 100644 vendor/doctrine/orm/src/Event/OnClassMetadataNotFoundEventArgs.php create mode 100644 vendor/doctrine/orm/src/Event/OnClearEventArgs.php create mode 100644 vendor/doctrine/orm/src/Event/OnFlushEventArgs.php create mode 100644 vendor/doctrine/orm/src/Event/PostFlushEventArgs.php create mode 100644 vendor/doctrine/orm/src/Event/PostLoadEventArgs.php create mode 100644 vendor/doctrine/orm/src/Event/PostPersistEventArgs.php create mode 100644 vendor/doctrine/orm/src/Event/PostRemoveEventArgs.php create mode 100644 vendor/doctrine/orm/src/Event/PostUpdateEventArgs.php create mode 100644 vendor/doctrine/orm/src/Event/PreFlushEventArgs.php create mode 100644 vendor/doctrine/orm/src/Event/PrePersistEventArgs.php create mode 100644 vendor/doctrine/orm/src/Event/PreRemoveEventArgs.php create mode 100644 vendor/doctrine/orm/src/Event/PreUpdateEventArgs.php create mode 100644 vendor/doctrine/orm/src/Events.php create mode 100644 vendor/doctrine/orm/src/Exception/ConfigurationException.php create mode 100644 vendor/doctrine/orm/src/Exception/EntityIdentityCollisionException.php create mode 100644 vendor/doctrine/orm/src/Exception/EntityManagerClosed.php create mode 100644 vendor/doctrine/orm/src/Exception/EntityMissingAssignedId.php create mode 100644 vendor/doctrine/orm/src/Exception/InvalidEntityRepository.php create mode 100644 vendor/doctrine/orm/src/Exception/InvalidHydrationMode.php create mode 100644 vendor/doctrine/orm/src/Exception/ManagerException.php create mode 100644 vendor/doctrine/orm/src/Exception/MissingIdentifierField.php create mode 100644 vendor/doctrine/orm/src/Exception/MissingMappingDriverImplementation.php create mode 100644 vendor/doctrine/orm/src/Exception/MultipleSelectorsFoundException.php create mode 100644 vendor/doctrine/orm/src/Exception/NotSupported.php create mode 100644 vendor/doctrine/orm/src/Exception/ORMException.php create mode 100644 vendor/doctrine/orm/src/Exception/PersisterException.php create mode 100644 vendor/doctrine/orm/src/Exception/RepositoryException.php create mode 100644 vendor/doctrine/orm/src/Exception/SchemaToolException.php create mode 100644 vendor/doctrine/orm/src/Exception/UnexpectedAssociationValue.php create mode 100644 vendor/doctrine/orm/src/Exception/UnrecognizedIdentifierFields.php create mode 100644 vendor/doctrine/orm/src/Id/AbstractIdGenerator.php create mode 100644 vendor/doctrine/orm/src/Id/AssignedGenerator.php create mode 100644 vendor/doctrine/orm/src/Id/BigIntegerIdentityGenerator.php create mode 100644 vendor/doctrine/orm/src/Id/IdentityGenerator.php create mode 100644 vendor/doctrine/orm/src/Id/SequenceGenerator.php create mode 100644 vendor/doctrine/orm/src/Internal/Hydration/AbstractHydrator.php create mode 100644 vendor/doctrine/orm/src/Internal/Hydration/ArrayHydrator.php create mode 100644 vendor/doctrine/orm/src/Internal/Hydration/HydrationException.php create mode 100644 vendor/doctrine/orm/src/Internal/Hydration/ObjectHydrator.php create mode 100644 vendor/doctrine/orm/src/Internal/Hydration/ScalarColumnHydrator.php create mode 100644 vendor/doctrine/orm/src/Internal/Hydration/ScalarHydrator.php create mode 100644 vendor/doctrine/orm/src/Internal/Hydration/SimpleObjectHydrator.php create mode 100644 vendor/doctrine/orm/src/Internal/Hydration/SingleScalarHydrator.php create mode 100644 vendor/doctrine/orm/src/Internal/HydrationCompleteHandler.php create mode 100644 vendor/doctrine/orm/src/Internal/NoUnknownNamedArguments.php create mode 100644 vendor/doctrine/orm/src/Internal/QueryType.php create mode 100644 vendor/doctrine/orm/src/Internal/SQLResultCasing.php create mode 100644 vendor/doctrine/orm/src/Internal/StronglyConnectedComponents.php create mode 100644 vendor/doctrine/orm/src/Internal/TopologicalSort.php create mode 100644 vendor/doctrine/orm/src/Internal/TopologicalSort/CycleDetectedException.php create mode 100644 vendor/doctrine/orm/src/LazyCriteriaCollection.php create mode 100644 vendor/doctrine/orm/src/Mapping/AnsiQuoteStrategy.php create mode 100644 vendor/doctrine/orm/src/Mapping/ArrayAccessImplementation.php create mode 100644 vendor/doctrine/orm/src/Mapping/AssociationMapping.php create mode 100644 vendor/doctrine/orm/src/Mapping/AssociationOverride.php create mode 100644 vendor/doctrine/orm/src/Mapping/AssociationOverrides.php create mode 100644 vendor/doctrine/orm/src/Mapping/AttributeOverride.php create mode 100644 vendor/doctrine/orm/src/Mapping/AttributeOverrides.php create mode 100644 vendor/doctrine/orm/src/Mapping/Builder/AssociationBuilder.php create mode 100644 vendor/doctrine/orm/src/Mapping/Builder/ClassMetadataBuilder.php create mode 100644 vendor/doctrine/orm/src/Mapping/Builder/EmbeddedBuilder.php create mode 100644 vendor/doctrine/orm/src/Mapping/Builder/EntityListenerBuilder.php create mode 100644 vendor/doctrine/orm/src/Mapping/Builder/FieldBuilder.php create mode 100644 vendor/doctrine/orm/src/Mapping/Builder/ManyToManyAssociationBuilder.php create mode 100644 vendor/doctrine/orm/src/Mapping/Builder/OneToManyAssociationBuilder.php create mode 100644 vendor/doctrine/orm/src/Mapping/Cache.php create mode 100644 vendor/doctrine/orm/src/Mapping/ChainTypedFieldMapper.php create mode 100644 vendor/doctrine/orm/src/Mapping/ChangeTrackingPolicy.php create mode 100644 vendor/doctrine/orm/src/Mapping/ClassMetadata.php create mode 100644 vendor/doctrine/orm/src/Mapping/ClassMetadataFactory.php create mode 100644 vendor/doctrine/orm/src/Mapping/Column.php create mode 100644 vendor/doctrine/orm/src/Mapping/CustomIdGenerator.php create mode 100644 vendor/doctrine/orm/src/Mapping/DefaultEntityListenerResolver.php create mode 100644 vendor/doctrine/orm/src/Mapping/DefaultNamingStrategy.php create mode 100644 vendor/doctrine/orm/src/Mapping/DefaultQuoteStrategy.php create mode 100644 vendor/doctrine/orm/src/Mapping/DefaultTypedFieldMapper.php create mode 100644 vendor/doctrine/orm/src/Mapping/DiscriminatorColumn.php create mode 100644 vendor/doctrine/orm/src/Mapping/DiscriminatorColumnMapping.php create mode 100644 vendor/doctrine/orm/src/Mapping/DiscriminatorMap.php create mode 100644 vendor/doctrine/orm/src/Mapping/Driver/AttributeDriver.php create mode 100644 vendor/doctrine/orm/src/Mapping/Driver/AttributeReader.php create mode 100644 vendor/doctrine/orm/src/Mapping/Driver/DatabaseDriver.php create mode 100644 vendor/doctrine/orm/src/Mapping/Driver/ReflectionBasedDriver.php create mode 100644 vendor/doctrine/orm/src/Mapping/Driver/RepeatableAttributeCollection.php create mode 100644 vendor/doctrine/orm/src/Mapping/Driver/SimplifiedXmlDriver.php create mode 100644 vendor/doctrine/orm/src/Mapping/Driver/XmlDriver.php create mode 100644 vendor/doctrine/orm/src/Mapping/Embeddable.php create mode 100644 vendor/doctrine/orm/src/Mapping/Embedded.php create mode 100644 vendor/doctrine/orm/src/Mapping/EmbeddedClassMapping.php create mode 100644 vendor/doctrine/orm/src/Mapping/Entity.php create mode 100644 vendor/doctrine/orm/src/Mapping/EntityListenerResolver.php create mode 100644 vendor/doctrine/orm/src/Mapping/EntityListeners.php create mode 100644 vendor/doctrine/orm/src/Mapping/Exception/InvalidCustomGenerator.php create mode 100644 vendor/doctrine/orm/src/Mapping/Exception/UnknownGeneratorType.php create mode 100644 vendor/doctrine/orm/src/Mapping/FieldMapping.php create mode 100644 vendor/doctrine/orm/src/Mapping/GeneratedValue.php create mode 100644 vendor/doctrine/orm/src/Mapping/HasLifecycleCallbacks.php create mode 100644 vendor/doctrine/orm/src/Mapping/Id.php create mode 100644 vendor/doctrine/orm/src/Mapping/Index.php create mode 100644 vendor/doctrine/orm/src/Mapping/InheritanceType.php create mode 100644 vendor/doctrine/orm/src/Mapping/InverseJoinColumn.php create mode 100644 vendor/doctrine/orm/src/Mapping/InverseSideMapping.php create mode 100644 vendor/doctrine/orm/src/Mapping/JoinColumn.php create mode 100644 vendor/doctrine/orm/src/Mapping/JoinColumnMapping.php create mode 100644 vendor/doctrine/orm/src/Mapping/JoinColumnProperties.php create mode 100644 vendor/doctrine/orm/src/Mapping/JoinColumns.php create mode 100644 vendor/doctrine/orm/src/Mapping/JoinTable.php create mode 100644 vendor/doctrine/orm/src/Mapping/JoinTableMapping.php create mode 100644 vendor/doctrine/orm/src/Mapping/ManyToMany.php create mode 100644 vendor/doctrine/orm/src/Mapping/ManyToManyAssociationMapping.php create mode 100644 vendor/doctrine/orm/src/Mapping/ManyToManyInverseSideMapping.php create mode 100644 vendor/doctrine/orm/src/Mapping/ManyToManyOwningSideMapping.php create mode 100644 vendor/doctrine/orm/src/Mapping/ManyToOne.php create mode 100644 vendor/doctrine/orm/src/Mapping/ManyToOneAssociationMapping.php create mode 100644 vendor/doctrine/orm/src/Mapping/MappedSuperclass.php create mode 100644 vendor/doctrine/orm/src/Mapping/MappingAttribute.php create mode 100644 vendor/doctrine/orm/src/Mapping/MappingException.php create mode 100644 vendor/doctrine/orm/src/Mapping/NamingStrategy.php create mode 100644 vendor/doctrine/orm/src/Mapping/OneToMany.php create mode 100644 vendor/doctrine/orm/src/Mapping/OneToManyAssociationMapping.php create mode 100644 vendor/doctrine/orm/src/Mapping/OneToOne.php create mode 100644 vendor/doctrine/orm/src/Mapping/OneToOneAssociationMapping.php create mode 100644 vendor/doctrine/orm/src/Mapping/OneToOneInverseSideMapping.php create mode 100644 vendor/doctrine/orm/src/Mapping/OneToOneOwningSideMapping.php create mode 100644 vendor/doctrine/orm/src/Mapping/OrderBy.php create mode 100644 vendor/doctrine/orm/src/Mapping/OwningSideMapping.php create mode 100644 vendor/doctrine/orm/src/Mapping/PostLoad.php create mode 100644 vendor/doctrine/orm/src/Mapping/PostPersist.php create mode 100644 vendor/doctrine/orm/src/Mapping/PostRemove.php create mode 100644 vendor/doctrine/orm/src/Mapping/PostUpdate.php create mode 100644 vendor/doctrine/orm/src/Mapping/PreFlush.php create mode 100644 vendor/doctrine/orm/src/Mapping/PrePersist.php create mode 100644 vendor/doctrine/orm/src/Mapping/PreRemove.php create mode 100644 vendor/doctrine/orm/src/Mapping/PreUpdate.php create mode 100644 vendor/doctrine/orm/src/Mapping/QuoteStrategy.php create mode 100644 vendor/doctrine/orm/src/Mapping/ReflectionEmbeddedProperty.php create mode 100644 vendor/doctrine/orm/src/Mapping/ReflectionEnumProperty.php create mode 100644 vendor/doctrine/orm/src/Mapping/ReflectionReadonlyProperty.php create mode 100644 vendor/doctrine/orm/src/Mapping/SequenceGenerator.php create mode 100644 vendor/doctrine/orm/src/Mapping/Table.php create mode 100644 vendor/doctrine/orm/src/Mapping/ToManyAssociationMapping.php create mode 100644 vendor/doctrine/orm/src/Mapping/ToManyAssociationMappingImplementation.php create mode 100644 vendor/doctrine/orm/src/Mapping/ToManyInverseSideMapping.php create mode 100644 vendor/doctrine/orm/src/Mapping/ToManyOwningSideMapping.php create mode 100644 vendor/doctrine/orm/src/Mapping/ToOneAssociationMapping.php create mode 100644 vendor/doctrine/orm/src/Mapping/ToOneInverseSideMapping.php create mode 100644 vendor/doctrine/orm/src/Mapping/ToOneOwningSideMapping.php create mode 100644 vendor/doctrine/orm/src/Mapping/TypedFieldMapper.php create mode 100644 vendor/doctrine/orm/src/Mapping/UnderscoreNamingStrategy.php create mode 100644 vendor/doctrine/orm/src/Mapping/UniqueConstraint.php create mode 100644 vendor/doctrine/orm/src/Mapping/Version.php create mode 100644 vendor/doctrine/orm/src/NativeQuery.php create mode 100644 vendor/doctrine/orm/src/NoResultException.php create mode 100644 vendor/doctrine/orm/src/NonUniqueResultException.php create mode 100644 vendor/doctrine/orm/src/ORMInvalidArgumentException.php create mode 100644 vendor/doctrine/orm/src/ORMSetup.php create mode 100644 vendor/doctrine/orm/src/OptimisticLockException.php create mode 100644 vendor/doctrine/orm/src/PersistentCollection.php create mode 100644 vendor/doctrine/orm/src/Persisters/Collection/AbstractCollectionPersister.php create mode 100644 vendor/doctrine/orm/src/Persisters/Collection/CollectionPersister.php create mode 100644 vendor/doctrine/orm/src/Persisters/Collection/ManyToManyPersister.php create mode 100644 vendor/doctrine/orm/src/Persisters/Collection/OneToManyPersister.php create mode 100644 vendor/doctrine/orm/src/Persisters/Entity/AbstractEntityInheritancePersister.php create mode 100644 vendor/doctrine/orm/src/Persisters/Entity/BasicEntityPersister.php create mode 100644 vendor/doctrine/orm/src/Persisters/Entity/CachedPersisterContext.php create mode 100644 vendor/doctrine/orm/src/Persisters/Entity/EntityPersister.php create mode 100644 vendor/doctrine/orm/src/Persisters/Entity/JoinedSubclassPersister.php create mode 100644 vendor/doctrine/orm/src/Persisters/Entity/SingleTablePersister.php create mode 100644 vendor/doctrine/orm/src/Persisters/Exception/CantUseInOperatorOnCompositeKeys.php create mode 100644 vendor/doctrine/orm/src/Persisters/Exception/InvalidOrientation.php create mode 100644 vendor/doctrine/orm/src/Persisters/Exception/UnrecognizedField.php create mode 100644 vendor/doctrine/orm/src/Persisters/MatchingAssociationFieldRequiresObject.php create mode 100644 vendor/doctrine/orm/src/Persisters/PersisterException.php create mode 100644 vendor/doctrine/orm/src/Persisters/SqlExpressionVisitor.php create mode 100644 vendor/doctrine/orm/src/Persisters/SqlValueVisitor.php create mode 100644 vendor/doctrine/orm/src/PessimisticLockException.php create mode 100644 vendor/doctrine/orm/src/Proxy/Autoloader.php create mode 100644 vendor/doctrine/orm/src/Proxy/DefaultProxyClassNameResolver.php create mode 100644 vendor/doctrine/orm/src/Proxy/InternalProxy.php create mode 100644 vendor/doctrine/orm/src/Proxy/NotAProxyClass.php create mode 100644 vendor/doctrine/orm/src/Proxy/ProxyFactory.php create mode 100644 vendor/doctrine/orm/src/Query.php create mode 100644 vendor/doctrine/orm/src/Query/AST/ASTException.php create mode 100644 vendor/doctrine/orm/src/Query/AST/AggregateExpression.php create mode 100644 vendor/doctrine/orm/src/Query/AST/ArithmeticExpression.php create mode 100644 vendor/doctrine/orm/src/Query/AST/ArithmeticFactor.php create mode 100644 vendor/doctrine/orm/src/Query/AST/ArithmeticTerm.php create mode 100644 vendor/doctrine/orm/src/Query/AST/BetweenExpression.php create mode 100644 vendor/doctrine/orm/src/Query/AST/CoalesceExpression.php create mode 100644 vendor/doctrine/orm/src/Query/AST/CollectionMemberExpression.php create mode 100644 vendor/doctrine/orm/src/Query/AST/ComparisonExpression.php create mode 100644 vendor/doctrine/orm/src/Query/AST/ConditionalExpression.php create mode 100644 vendor/doctrine/orm/src/Query/AST/ConditionalFactor.php create mode 100644 vendor/doctrine/orm/src/Query/AST/ConditionalPrimary.php create mode 100644 vendor/doctrine/orm/src/Query/AST/ConditionalTerm.php create mode 100644 vendor/doctrine/orm/src/Query/AST/DeleteClause.php create mode 100644 vendor/doctrine/orm/src/Query/AST/DeleteStatement.php create mode 100644 vendor/doctrine/orm/src/Query/AST/EmptyCollectionComparisonExpression.php create mode 100644 vendor/doctrine/orm/src/Query/AST/ExistsExpression.php create mode 100644 vendor/doctrine/orm/src/Query/AST/FromClause.php create mode 100644 vendor/doctrine/orm/src/Query/AST/Functions/AbsFunction.php create mode 100644 vendor/doctrine/orm/src/Query/AST/Functions/AvgFunction.php create mode 100644 vendor/doctrine/orm/src/Query/AST/Functions/BitAndFunction.php create mode 100644 vendor/doctrine/orm/src/Query/AST/Functions/BitOrFunction.php create mode 100644 vendor/doctrine/orm/src/Query/AST/Functions/ConcatFunction.php create mode 100644 vendor/doctrine/orm/src/Query/AST/Functions/CountFunction.php create mode 100644 vendor/doctrine/orm/src/Query/AST/Functions/CurrentDateFunction.php create mode 100644 vendor/doctrine/orm/src/Query/AST/Functions/CurrentTimeFunction.php create mode 100644 vendor/doctrine/orm/src/Query/AST/Functions/CurrentTimestampFunction.php create mode 100644 vendor/doctrine/orm/src/Query/AST/Functions/DateAddFunction.php create mode 100644 vendor/doctrine/orm/src/Query/AST/Functions/DateDiffFunction.php create mode 100644 vendor/doctrine/orm/src/Query/AST/Functions/DateSubFunction.php create mode 100644 vendor/doctrine/orm/src/Query/AST/Functions/FunctionNode.php create mode 100644 vendor/doctrine/orm/src/Query/AST/Functions/IdentityFunction.php create mode 100644 vendor/doctrine/orm/src/Query/AST/Functions/LengthFunction.php create mode 100644 vendor/doctrine/orm/src/Query/AST/Functions/LocateFunction.php create mode 100644 vendor/doctrine/orm/src/Query/AST/Functions/LowerFunction.php create mode 100644 vendor/doctrine/orm/src/Query/AST/Functions/MaxFunction.php create mode 100644 vendor/doctrine/orm/src/Query/AST/Functions/MinFunction.php create mode 100644 vendor/doctrine/orm/src/Query/AST/Functions/ModFunction.php create mode 100644 vendor/doctrine/orm/src/Query/AST/Functions/SizeFunction.php create mode 100644 vendor/doctrine/orm/src/Query/AST/Functions/SqrtFunction.php create mode 100644 vendor/doctrine/orm/src/Query/AST/Functions/SubstringFunction.php create mode 100644 vendor/doctrine/orm/src/Query/AST/Functions/SumFunction.php create mode 100644 vendor/doctrine/orm/src/Query/AST/Functions/TrimFunction.php create mode 100644 vendor/doctrine/orm/src/Query/AST/Functions/UpperFunction.php create mode 100644 vendor/doctrine/orm/src/Query/AST/GeneralCaseExpression.php create mode 100644 vendor/doctrine/orm/src/Query/AST/GroupByClause.php create mode 100644 vendor/doctrine/orm/src/Query/AST/HavingClause.php create mode 100644 vendor/doctrine/orm/src/Query/AST/IdentificationVariableDeclaration.php create mode 100644 vendor/doctrine/orm/src/Query/AST/InListExpression.php create mode 100644 vendor/doctrine/orm/src/Query/AST/InSubselectExpression.php create mode 100644 vendor/doctrine/orm/src/Query/AST/IndexBy.php create mode 100644 vendor/doctrine/orm/src/Query/AST/InputParameter.php create mode 100644 vendor/doctrine/orm/src/Query/AST/InstanceOfExpression.php create mode 100644 vendor/doctrine/orm/src/Query/AST/Join.php create mode 100644 vendor/doctrine/orm/src/Query/AST/JoinAssociationDeclaration.php create mode 100644 vendor/doctrine/orm/src/Query/AST/JoinAssociationPathExpression.php create mode 100644 vendor/doctrine/orm/src/Query/AST/JoinClassPathExpression.php create mode 100644 vendor/doctrine/orm/src/Query/AST/JoinVariableDeclaration.php create mode 100644 vendor/doctrine/orm/src/Query/AST/LikeExpression.php create mode 100644 vendor/doctrine/orm/src/Query/AST/Literal.php create mode 100644 vendor/doctrine/orm/src/Query/AST/NewObjectExpression.php create mode 100644 vendor/doctrine/orm/src/Query/AST/Node.php create mode 100644 vendor/doctrine/orm/src/Query/AST/NullComparisonExpression.php create mode 100644 vendor/doctrine/orm/src/Query/AST/NullIfExpression.php create mode 100644 vendor/doctrine/orm/src/Query/AST/OrderByClause.php create mode 100644 vendor/doctrine/orm/src/Query/AST/OrderByItem.php create mode 100644 vendor/doctrine/orm/src/Query/AST/ParenthesisExpression.php create mode 100644 vendor/doctrine/orm/src/Query/AST/PathExpression.php create mode 100644 vendor/doctrine/orm/src/Query/AST/Phase2OptimizableConditional.php create mode 100644 vendor/doctrine/orm/src/Query/AST/QuantifiedExpression.php create mode 100644 vendor/doctrine/orm/src/Query/AST/RangeVariableDeclaration.php create mode 100644 vendor/doctrine/orm/src/Query/AST/SelectClause.php create mode 100644 vendor/doctrine/orm/src/Query/AST/SelectExpression.php create mode 100644 vendor/doctrine/orm/src/Query/AST/SelectStatement.php create mode 100644 vendor/doctrine/orm/src/Query/AST/SimpleArithmeticExpression.php create mode 100644 vendor/doctrine/orm/src/Query/AST/SimpleCaseExpression.php create mode 100644 vendor/doctrine/orm/src/Query/AST/SimpleSelectClause.php create mode 100644 vendor/doctrine/orm/src/Query/AST/SimpleSelectExpression.php create mode 100644 vendor/doctrine/orm/src/Query/AST/SimpleWhenClause.php create mode 100644 vendor/doctrine/orm/src/Query/AST/Subselect.php create mode 100644 vendor/doctrine/orm/src/Query/AST/SubselectFromClause.php create mode 100644 vendor/doctrine/orm/src/Query/AST/SubselectIdentificationVariableDeclaration.php create mode 100644 vendor/doctrine/orm/src/Query/AST/TypedExpression.php create mode 100644 vendor/doctrine/orm/src/Query/AST/UpdateClause.php create mode 100644 vendor/doctrine/orm/src/Query/AST/UpdateItem.php create mode 100644 vendor/doctrine/orm/src/Query/AST/UpdateStatement.php create mode 100644 vendor/doctrine/orm/src/Query/AST/WhenClause.php create mode 100644 vendor/doctrine/orm/src/Query/AST/WhereClause.php create mode 100644 vendor/doctrine/orm/src/Query/Exec/AbstractSqlExecutor.php create mode 100644 vendor/doctrine/orm/src/Query/Exec/MultiTableDeleteExecutor.php create mode 100644 vendor/doctrine/orm/src/Query/Exec/MultiTableUpdateExecutor.php create mode 100644 vendor/doctrine/orm/src/Query/Exec/SingleSelectExecutor.php create mode 100644 vendor/doctrine/orm/src/Query/Exec/SingleTableDeleteUpdateExecutor.php create mode 100644 vendor/doctrine/orm/src/Query/Expr.php create mode 100644 vendor/doctrine/orm/src/Query/Expr/Andx.php create mode 100644 vendor/doctrine/orm/src/Query/Expr/Base.php create mode 100644 vendor/doctrine/orm/src/Query/Expr/Comparison.php create mode 100644 vendor/doctrine/orm/src/Query/Expr/Composite.php create mode 100644 vendor/doctrine/orm/src/Query/Expr/From.php create mode 100644 vendor/doctrine/orm/src/Query/Expr/Func.php create mode 100644 vendor/doctrine/orm/src/Query/Expr/GroupBy.php create mode 100644 vendor/doctrine/orm/src/Query/Expr/Join.php create mode 100644 vendor/doctrine/orm/src/Query/Expr/Literal.php create mode 100644 vendor/doctrine/orm/src/Query/Expr/Math.php create mode 100644 vendor/doctrine/orm/src/Query/Expr/OrderBy.php create mode 100644 vendor/doctrine/orm/src/Query/Expr/Orx.php create mode 100644 vendor/doctrine/orm/src/Query/Expr/Select.php create mode 100644 vendor/doctrine/orm/src/Query/Filter/FilterException.php create mode 100644 vendor/doctrine/orm/src/Query/Filter/SQLFilter.php create mode 100644 vendor/doctrine/orm/src/Query/FilterCollection.php create mode 100644 vendor/doctrine/orm/src/Query/Lexer.php create mode 100644 vendor/doctrine/orm/src/Query/Parameter.php create mode 100644 vendor/doctrine/orm/src/Query/ParameterTypeInferer.php create mode 100644 vendor/doctrine/orm/src/Query/Parser.php create mode 100644 vendor/doctrine/orm/src/Query/ParserResult.php create mode 100644 vendor/doctrine/orm/src/Query/Printer.php create mode 100644 vendor/doctrine/orm/src/Query/QueryException.php create mode 100644 vendor/doctrine/orm/src/Query/QueryExpressionVisitor.php create mode 100644 vendor/doctrine/orm/src/Query/ResultSetMapping.php create mode 100644 vendor/doctrine/orm/src/Query/ResultSetMappingBuilder.php create mode 100644 vendor/doctrine/orm/src/Query/SqlWalker.php create mode 100644 vendor/doctrine/orm/src/Query/TokenType.php create mode 100644 vendor/doctrine/orm/src/Query/TreeWalker.php create mode 100644 vendor/doctrine/orm/src/Query/TreeWalkerAdapter.php create mode 100644 vendor/doctrine/orm/src/Query/TreeWalkerChain.php create mode 100644 vendor/doctrine/orm/src/QueryBuilder.php create mode 100644 vendor/doctrine/orm/src/Repository/DefaultRepositoryFactory.php create mode 100644 vendor/doctrine/orm/src/Repository/Exception/InvalidFindByCall.php create mode 100644 vendor/doctrine/orm/src/Repository/Exception/InvalidMagicMethodCall.php create mode 100644 vendor/doctrine/orm/src/Repository/RepositoryFactory.php create mode 100644 vendor/doctrine/orm/src/Tools/AttachEntityListenersListener.php create mode 100644 vendor/doctrine/orm/src/Tools/Console/Command/AbstractEntityManagerCommand.php create mode 100644 vendor/doctrine/orm/src/Tools/Console/Command/ClearCache/CollectionRegionCommand.php create mode 100644 vendor/doctrine/orm/src/Tools/Console/Command/ClearCache/EntityRegionCommand.php create mode 100644 vendor/doctrine/orm/src/Tools/Console/Command/ClearCache/MetadataCommand.php create mode 100644 vendor/doctrine/orm/src/Tools/Console/Command/ClearCache/QueryCommand.php create mode 100644 vendor/doctrine/orm/src/Tools/Console/Command/ClearCache/QueryRegionCommand.php create mode 100644 vendor/doctrine/orm/src/Tools/Console/Command/ClearCache/ResultCommand.php create mode 100644 vendor/doctrine/orm/src/Tools/Console/Command/GenerateProxiesCommand.php create mode 100644 vendor/doctrine/orm/src/Tools/Console/Command/InfoCommand.php create mode 100644 vendor/doctrine/orm/src/Tools/Console/Command/MappingDescribeCommand.php create mode 100644 vendor/doctrine/orm/src/Tools/Console/Command/RunDqlCommand.php create mode 100644 vendor/doctrine/orm/src/Tools/Console/Command/SchemaTool/AbstractCommand.php create mode 100644 vendor/doctrine/orm/src/Tools/Console/Command/SchemaTool/CreateCommand.php create mode 100644 vendor/doctrine/orm/src/Tools/Console/Command/SchemaTool/DropCommand.php create mode 100644 vendor/doctrine/orm/src/Tools/Console/Command/SchemaTool/UpdateCommand.php create mode 100644 vendor/doctrine/orm/src/Tools/Console/Command/ValidateSchemaCommand.php create mode 100644 vendor/doctrine/orm/src/Tools/Console/ConsoleRunner.php create mode 100644 vendor/doctrine/orm/src/Tools/Console/EntityManagerProvider.php create mode 100644 vendor/doctrine/orm/src/Tools/Console/EntityManagerProvider/ConnectionFromManagerProvider.php create mode 100644 vendor/doctrine/orm/src/Tools/Console/EntityManagerProvider/SingleManagerProvider.php create mode 100644 vendor/doctrine/orm/src/Tools/Console/EntityManagerProvider/UnknownManagerException.php create mode 100644 vendor/doctrine/orm/src/Tools/Console/MetadataFilter.php create mode 100644 vendor/doctrine/orm/src/Tools/Debug.php create mode 100644 vendor/doctrine/orm/src/Tools/DebugUnitOfWorkListener.php create mode 100644 vendor/doctrine/orm/src/Tools/Event/GenerateSchemaEventArgs.php create mode 100644 vendor/doctrine/orm/src/Tools/Event/GenerateSchemaTableEventArgs.php create mode 100644 vendor/doctrine/orm/src/Tools/Exception/MissingColumnException.php create mode 100644 vendor/doctrine/orm/src/Tools/Exception/NotSupported.php create mode 100644 vendor/doctrine/orm/src/Tools/Pagination/CountOutputWalker.php create mode 100644 vendor/doctrine/orm/src/Tools/Pagination/CountWalker.php create mode 100644 vendor/doctrine/orm/src/Tools/Pagination/Exception/RowNumberOverFunctionNotEnabled.php create mode 100644 vendor/doctrine/orm/src/Tools/Pagination/LimitSubqueryOutputWalker.php create mode 100644 vendor/doctrine/orm/src/Tools/Pagination/LimitSubqueryWalker.php create mode 100644 vendor/doctrine/orm/src/Tools/Pagination/Paginator.php create mode 100644 vendor/doctrine/orm/src/Tools/Pagination/RootTypeWalker.php create mode 100644 vendor/doctrine/orm/src/Tools/Pagination/RowNumberOverFunction.php create mode 100644 vendor/doctrine/orm/src/Tools/Pagination/WhereInWalker.php create mode 100644 vendor/doctrine/orm/src/Tools/ResolveTargetEntityListener.php create mode 100644 vendor/doctrine/orm/src/Tools/SchemaTool.php create mode 100644 vendor/doctrine/orm/src/Tools/SchemaValidator.php create mode 100644 vendor/doctrine/orm/src/Tools/ToolEvents.php create mode 100644 vendor/doctrine/orm/src/Tools/ToolsException.php create mode 100644 vendor/doctrine/orm/src/TransactionRequiredException.php create mode 100644 vendor/doctrine/orm/src/UnexpectedResultException.php create mode 100644 vendor/doctrine/orm/src/UnitOfWork.php create mode 100644 vendor/doctrine/orm/src/Utility/HierarchyDiscriminatorResolver.php create mode 100644 vendor/doctrine/orm/src/Utility/IdentifierFlattener.php create mode 100644 vendor/doctrine/orm/src/Utility/LockSqlHelper.php create mode 100644 vendor/doctrine/orm/src/Utility/PersisterHelper.php (limited to 'vendor/doctrine/orm/src') 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 @@ + + */ + protected ArrayCollection $parameters; + + /** + * The user-specified ResultSetMapping to use. + */ + protected ResultSetMapping|null $resultSetMapping = null; + + /** + * The map of query hints. + * + * @psalm-var array + */ + protected array $hints = []; + + /** + * The hydration mode. + * + * @psalm-var string|AbstractQuery::HYDRATE_* + */ + protected string|int $hydrationMode = self::HYDRATE_OBJECT; + + protected QueryCacheProfile|null $queryCacheProfile = null; + + /** + * Whether or not expire the result cache. + */ + protected bool $expireResultCache = false; + + protected QueryCacheProfile|null $hydrationCacheProfile = null; + + /** + * Whether to use second level cache, if available. + */ + protected bool $cacheable = false; + + protected bool $hasCache = false; + + /** + * Second level cache region name. + */ + protected string|null $cacheRegion = null; + + /** + * Second level query cache mode. + * + * @psalm-var Cache::MODE_*|null + */ + protected int|null $cacheMode = null; + + protected CacheLogger|null $cacheLogger = null; + + protected int $lifetime = 0; + + /** + * Initializes a new instance of a class derived from AbstractQuery. + */ + public function __construct( + /** + * The entity manager used by this query object. + */ + protected EntityManagerInterface $em, + ) { + $this->parameters = new ArrayCollection(); + $this->hints = $em->getConfiguration()->getDefaultQueryHints(); + $this->hasCache = $this->em->getConfiguration()->isSecondLevelCacheEnabled(); + + if ($this->hasCache) { + $this->cacheLogger = $em->getConfiguration() + ->getSecondLevelCacheConfiguration() + ->getCacheLogger(); + } + } + + /** + * Enable/disable second level query (result) caching for this query. + * + * @return $this + */ + public function setCacheable(bool $cacheable): static + { + $this->cacheable = $cacheable; + + return $this; + } + + /** @return bool TRUE if the query results are enabled for second level cache, FALSE otherwise. */ + public function isCacheable(): bool + { + return $this->cacheable; + } + + /** @return $this */ + public function setCacheRegion(string $cacheRegion): static + { + $this->cacheRegion = $cacheRegion; + + return $this; + } + + /** + * Obtain the name of the second level query cache region in which query results will be stored + * + * @return string|null The cache region name; NULL indicates the default region. + */ + public function getCacheRegion(): string|null + { + return $this->cacheRegion; + } + + /** @return bool TRUE if the query cache and second level cache are enabled, FALSE otherwise. */ + protected function isCacheEnabled(): bool + { + return $this->cacheable && $this->hasCache; + } + + public function getLifetime(): int + { + return $this->lifetime; + } + + /** + * Sets the life-time for this query into second level cache. + * + * @return $this + */ + public function setLifetime(int $lifetime): static + { + $this->lifetime = $lifetime; + + return $this; + } + + /** @psalm-return Cache::MODE_*|null */ + public function getCacheMode(): int|null + { + return $this->cacheMode; + } + + /** + * @psalm-param Cache::MODE_* $cacheMode + * + * @return $this + */ + public function setCacheMode(int $cacheMode): static + { + $this->cacheMode = $cacheMode; + + return $this; + } + + /** + * Gets the SQL query that corresponds to this query object. + * The returned SQL syntax depends on the connection driver that is used + * by this query object at the time of this method call. + * + * @return list|string SQL query + */ + abstract public function getSQL(): string|array; + + /** + * Retrieves the associated EntityManager of this Query instance. + */ + public function getEntityManager(): EntityManagerInterface + { + return $this->em; + } + + /** + * Frees the resources used by the query object. + * + * Resets Parameters, Parameter Types and Query Hints. + */ + public function free(): void + { + $this->parameters = new ArrayCollection(); + + $this->hints = $this->em->getConfiguration()->getDefaultQueryHints(); + } + + /** + * Get all defined parameters. + * + * @psalm-return ArrayCollection + */ + public function getParameters(): ArrayCollection + { + return $this->parameters; + } + + /** + * Gets a query parameter. + * + * @param int|string $key The key (index or name) of the bound parameter. + * + * @return Parameter|null The value of the bound parameter, or NULL if not available. + */ + public function getParameter(int|string $key): Parameter|null + { + $key = Parameter::normalizeName($key); + + $filteredParameters = $this->parameters->filter( + static fn (Parameter $parameter): bool => $parameter->getName() === $key + ); + + return ! $filteredParameters->isEmpty() ? $filteredParameters->first() : null; + } + + /** + * Sets a collection of query parameters. + * + * @param ArrayCollection|mixed[] $parameters + * @psalm-param ArrayCollection|mixed[] $parameters + * + * @return $this + */ + public function setParameters(ArrayCollection|array $parameters): static + { + if (is_array($parameters)) { + /** @psalm-var ArrayCollection $parameterCollection */ + $parameterCollection = new ArrayCollection(); + + foreach ($parameters as $key => $value) { + $parameterCollection->add(new Parameter($key, $value)); + } + + $parameters = $parameterCollection; + } + + $this->parameters = $parameters; + + return $this; + } + + /** + * Sets a query parameter. + * + * @param string|int $key The parameter position or name. + * @param mixed $value The parameter value. + * @param ParameterType|ArrayParameterType|string|int|null $type The parameter type. If specified, the given value + * will be run through the type conversion of this + * type. This is usually not needed for strings and + * numeric types. + * + * @return $this + */ + public function setParameter(string|int $key, mixed $value, ParameterType|ArrayParameterType|string|int|null $type = null): static + { + $existingParameter = $this->getParameter($key); + + if ($existingParameter !== null) { + $existingParameter->setValue($value, $type); + + return $this; + } + + $this->parameters->add(new Parameter($key, $value, $type)); + + return $this; + } + + /** + * Processes an individual parameter value. + * + * @throws ORMInvalidArgumentException + */ + public function processParameterValue(mixed $value): mixed + { + if (is_scalar($value)) { + return $value; + } + + if ($value instanceof Collection) { + $value = iterator_to_array($value); + } + + if (is_array($value)) { + $value = $this->processArrayParameterValue($value); + + return $value; + } + + if ($value instanceof ClassMetadata) { + return $value->name; + } + + if ($value instanceof BackedEnum) { + return $value->value; + } + + if (! is_object($value)) { + return $value; + } + + try { + $class = DefaultProxyClassNameResolver::getClass($value); + $value = $this->em->getUnitOfWork()->getSingleIdentifierValue($value); + + if ($value === null) { + throw ORMInvalidArgumentException::invalidIdentifierBindingEntity($class); + } + } catch (MappingException | ORMMappingException) { + /* Silence any mapping exceptions. These can occur if the object in + question is not a mapped entity, in which case we just don't do + any preparation on the value. + Depending on MappingDriver, either MappingException or + ORMMappingException is thrown. */ + + $value = $this->potentiallyProcessIterable($value); + } + + return $value; + } + + /** + * If no mapping is detected, trying to resolve the value as a Traversable + */ + private function potentiallyProcessIterable(mixed $value): mixed + { + if ($value instanceof Traversable) { + $value = iterator_to_array($value); + $value = $this->processArrayParameterValue($value); + } + + return $value; + } + + /** + * Process a parameter value which was previously identified as an array + * + * @param mixed[] $value + * + * @return mixed[] + */ + private function processArrayParameterValue(array $value): array + { + foreach ($value as $key => $paramValue) { + $paramValue = $this->processParameterValue($paramValue); + $value[$key] = is_array($paramValue) ? reset($paramValue) : $paramValue; + } + + return $value; + } + + /** + * Sets the ResultSetMapping that should be used for hydration. + * + * @return $this + */ + public function setResultSetMapping(ResultSetMapping $rsm): static + { + $this->translateNamespaces($rsm); + $this->resultSetMapping = $rsm; + + return $this; + } + + /** + * Gets the ResultSetMapping used for hydration. + */ + protected function getResultSetMapping(): ResultSetMapping|null + { + return $this->resultSetMapping; + } + + /** + * Allows to translate entity namespaces to full qualified names. + */ + private function translateNamespaces(ResultSetMapping $rsm): void + { + $translate = fn ($alias): string => $this->em->getClassMetadata($alias)->getName(); + + $rsm->aliasMap = array_map($translate, $rsm->aliasMap); + $rsm->declaringClasses = array_map($translate, $rsm->declaringClasses); + } + + /** + * Set a cache profile for hydration caching. + * + * If no result cache driver is set in the QueryCacheProfile, the default + * result cache driver is used from the configuration. + * + * Important: Hydration caching does NOT register entities in the + * UnitOfWork when retrieved from the cache. Never use result cached + * entities for requests that also flush the EntityManager. If you want + * some form of caching with UnitOfWork registration you should use + * {@see AbstractQuery::setResultCacheProfile()}. + * + * @return $this + * + * @example + * $lifetime = 100; + * $resultKey = "abc"; + * $query->setHydrationCacheProfile(new QueryCacheProfile()); + * $query->setHydrationCacheProfile(new QueryCacheProfile($lifetime, $resultKey)); + */ + public function setHydrationCacheProfile(QueryCacheProfile|null $profile): static + { + if ($profile === null) { + $this->hydrationCacheProfile = null; + + return $this; + } + + if (! $profile->getResultCache()) { + $defaultHydrationCacheImpl = $this->em->getConfiguration()->getHydrationCache(); + if ($defaultHydrationCacheImpl) { + $profile = $profile->setResultCache($defaultHydrationCacheImpl); + } + } + + $this->hydrationCacheProfile = $profile; + + return $this; + } + + public function getHydrationCacheProfile(): QueryCacheProfile|null + { + return $this->hydrationCacheProfile; + } + + /** + * Set a cache profile for the result cache. + * + * If no result cache driver is set in the QueryCacheProfile, the default + * result cache driver is used from the configuration. + * + * @return $this + */ + public function setResultCacheProfile(QueryCacheProfile|null $profile): static + { + if ($profile === null) { + $this->queryCacheProfile = null; + + return $this; + } + + if (! $profile->getResultCache()) { + $defaultResultCache = $this->em->getConfiguration()->getResultCache(); + if ($defaultResultCache) { + $profile = $profile->setResultCache($defaultResultCache); + } + } + + $this->queryCacheProfile = $profile; + + return $this; + } + + /** + * Defines a cache driver to be used for caching result sets and implicitly enables caching. + */ + public function setResultCache(CacheItemPoolInterface|null $resultCache): static + { + if ($resultCache === null) { + if ($this->queryCacheProfile) { + $this->queryCacheProfile = new QueryCacheProfile($this->queryCacheProfile->getLifetime(), $this->queryCacheProfile->getCacheKey()); + } + + return $this; + } + + $this->queryCacheProfile = $this->queryCacheProfile + ? $this->queryCacheProfile->setResultCache($resultCache) + : new QueryCacheProfile(0, null, $resultCache); + + return $this; + } + + /** + * Enables caching of the results of this query, for given or default amount of seconds + * and optionally specifies which ID to use for the cache entry. + * + * @param int|null $lifetime How long the cache entry is valid, in seconds. + * @param string|null $resultCacheId ID to use for the cache entry. + * + * @return $this + */ + public function enableResultCache(int|null $lifetime = null, string|null $resultCacheId = null): static + { + $this->setResultCacheLifetime($lifetime); + $this->setResultCacheId($resultCacheId); + + return $this; + } + + /** + * Disables caching of the results of this query. + * + * @return $this + */ + public function disableResultCache(): static + { + $this->queryCacheProfile = null; + + return $this; + } + + /** + * Defines how long the result cache will be active before expire. + * + * @param int|null $lifetime How long the cache entry is valid, in seconds. + * + * @return $this + */ + public function setResultCacheLifetime(int|null $lifetime): static + { + $lifetime = (int) $lifetime; + + if ($this->queryCacheProfile) { + $this->queryCacheProfile = $this->queryCacheProfile->setLifetime($lifetime); + + return $this; + } + + $this->queryCacheProfile = new QueryCacheProfile($lifetime); + + $cache = $this->em->getConfiguration()->getResultCache(); + if (! $cache) { + return $this; + } + + $this->queryCacheProfile = $this->queryCacheProfile->setResultCache($cache); + + return $this; + } + + /** + * Defines if the result cache is active or not. + * + * @param bool $expire Whether or not to force resultset cache expiration. + * + * @return $this + */ + public function expireResultCache(bool $expire = true): static + { + $this->expireResultCache = $expire; + + return $this; + } + + /** + * Retrieves if the resultset cache is active or not. + */ + public function getExpireResultCache(): bool + { + return $this->expireResultCache; + } + + public function getQueryCacheProfile(): QueryCacheProfile|null + { + return $this->queryCacheProfile; + } + + /** + * Change the default fetch mode of an association for this query. + * + * @param class-string $class + * @psalm-param Mapping\ClassMetadata::FETCH_EAGER|Mapping\ClassMetadata::FETCH_LAZY $fetchMode + */ + public function setFetchMode(string $class, string $assocName, int $fetchMode): static + { + $this->hints['fetchMode'][$class][$assocName] = $fetchMode; + + return $this; + } + + /** + * Defines the processing mode to be used during hydration / result set transformation. + * + * @param string|int $hydrationMode Doctrine processing mode to be used during hydration process. + * One of the Query::HYDRATE_* constants. + * @psalm-param string|AbstractQuery::HYDRATE_* $hydrationMode + * + * @return $this + */ + public function setHydrationMode(string|int $hydrationMode): static + { + $this->hydrationMode = $hydrationMode; + + return $this; + } + + /** + * Gets the hydration mode currently used by the query. + * + * @psalm-return string|AbstractQuery::HYDRATE_* + */ + public function getHydrationMode(): string|int + { + return $this->hydrationMode; + } + + /** + * Gets the list of results for the query. + * + * Alias for execute(null, $hydrationMode = HYDRATE_OBJECT). + * + * @psalm-param string|AbstractQuery::HYDRATE_* $hydrationMode + */ + public function getResult(string|int $hydrationMode = self::HYDRATE_OBJECT): mixed + { + return $this->execute(null, $hydrationMode); + } + + /** + * Gets the array of results for the query. + * + * Alias for execute(null, HYDRATE_ARRAY). + * + * @return mixed[] + */ + public function getArrayResult(): array + { + return $this->execute(null, self::HYDRATE_ARRAY); + } + + /** + * Gets one-dimensional array of results for the query. + * + * Alias for execute(null, HYDRATE_SCALAR_COLUMN). + * + * @return mixed[] + */ + public function getSingleColumnResult(): array + { + return $this->execute(null, self::HYDRATE_SCALAR_COLUMN); + } + + /** + * Gets the scalar results for the query. + * + * Alias for execute(null, HYDRATE_SCALAR). + * + * @return mixed[] + */ + public function getScalarResult(): array + { + return $this->execute(null, self::HYDRATE_SCALAR); + } + + /** + * Get exactly one result or null. + * + * @psalm-param string|AbstractQuery::HYDRATE_*|null $hydrationMode + * + * @throws NonUniqueResultException + */ + public function getOneOrNullResult(string|int|null $hydrationMode = null): mixed + { + try { + $result = $this->execute(null, $hydrationMode); + } catch (NoResultException) { + return null; + } + + if ($this->hydrationMode !== self::HYDRATE_SINGLE_SCALAR && ! $result) { + return null; + } + + if (! is_array($result)) { + return $result; + } + + if (count($result) > 1) { + throw new NonUniqueResultException(); + } + + return array_shift($result); + } + + /** + * Gets the single result of the query. + * + * Enforces the presence as well as the uniqueness of the result. + * + * If the result is not unique, a NonUniqueResultException is thrown. + * If there is no result, a NoResultException is thrown. + * + * @psalm-param string|AbstractQuery::HYDRATE_*|null $hydrationMode + * + * @throws NonUniqueResultException If the query result is not unique. + * @throws NoResultException If the query returned no result. + */ + public function getSingleResult(string|int|null $hydrationMode = null): mixed + { + $result = $this->execute(null, $hydrationMode); + + if ($this->hydrationMode !== self::HYDRATE_SINGLE_SCALAR && ! $result) { + throw new NoResultException(); + } + + if (! is_array($result)) { + return $result; + } + + if (count($result) > 1) { + throw new NonUniqueResultException(); + } + + return array_shift($result); + } + + /** + * Gets the single scalar result of the query. + * + * Alias for getSingleResult(HYDRATE_SINGLE_SCALAR). + * + * @return bool|float|int|string|null The scalar result. + * + * @throws NoResultException If the query returned no result. + * @throws NonUniqueResultException If the query result is not unique. + */ + public function getSingleScalarResult(): mixed + { + return $this->getSingleResult(self::HYDRATE_SINGLE_SCALAR); + } + + /** + * Sets a query hint. If the hint name is not recognized, it is silently ignored. + * + * @return $this + */ + public function setHint(string $name, mixed $value): static + { + $this->hints[$name] = $value; + + return $this; + } + + /** + * Gets the value of a query hint. If the hint name is not recognized, FALSE is returned. + * + * @return mixed The value of the hint or FALSE, if the hint name is not recognized. + */ + public function getHint(string $name): mixed + { + return $this->hints[$name] ?? false; + } + + public function hasHint(string $name): bool + { + return isset($this->hints[$name]); + } + + /** + * Return the key value map of query hints that are currently set. + * + * @return array + */ + public function getHints(): array + { + return $this->hints; + } + + /** + * Executes the query and returns an iterable that can be used to incrementally + * iterate over the result. + * + * @psalm-param ArrayCollection|mixed[] $parameters + * @psalm-param string|AbstractQuery::HYDRATE_*|null $hydrationMode + * + * @return iterable + */ + public function toIterable( + ArrayCollection|array $parameters = [], + string|int|null $hydrationMode = null, + ): iterable { + if ($hydrationMode !== null) { + $this->setHydrationMode($hydrationMode); + } + + if (count($parameters) !== 0) { + $this->setParameters($parameters); + } + + $rsm = $this->getResultSetMapping(); + if ($rsm === null) { + throw new LogicException('Uninitialized result set mapping.'); + } + + if ($rsm->isMixed && count($rsm->scalarMappings) > 0) { + throw QueryException::iterateWithMixedResultNotAllowed(); + } + + $stmt = $this->_doExecute(); + + return $this->em->newHydrator($this->hydrationMode)->toIterable($stmt, $rsm, $this->hints); + } + + /** + * Executes the query. + * + * @psalm-param ArrayCollection|mixed[]|null $parameters + * @psalm-param string|AbstractQuery::HYDRATE_*|null $hydrationMode + */ + public function execute( + ArrayCollection|array|null $parameters = null, + string|int|null $hydrationMode = null, + ): mixed { + if ($this->cacheable && $this->isCacheEnabled()) { + return $this->executeUsingQueryCache($parameters, $hydrationMode); + } + + return $this->executeIgnoreQueryCache($parameters, $hydrationMode); + } + + /** + * Execute query ignoring second level cache. + * + * @psalm-param ArrayCollection|mixed[]|null $parameters + * @psalm-param string|AbstractQuery::HYDRATE_*|null $hydrationMode + */ + private function executeIgnoreQueryCache( + ArrayCollection|array|null $parameters = null, + string|int|null $hydrationMode = null, + ): mixed { + if ($hydrationMode !== null) { + $this->setHydrationMode($hydrationMode); + } + + if (! empty($parameters)) { + $this->setParameters($parameters); + } + + $setCacheEntry = static function ($data): void { + }; + + if ($this->hydrationCacheProfile !== null) { + [$cacheKey, $realCacheKey] = $this->getHydrationCacheId(); + + $cache = $this->getHydrationCache(); + $cacheItem = $cache->getItem($cacheKey); + $result = $cacheItem->isHit() ? $cacheItem->get() : []; + + if (isset($result[$realCacheKey])) { + return $result[$realCacheKey]; + } + + if (! $result) { + $result = []; + } + + $setCacheEntry = static function ($data) use ($cache, $result, $cacheItem, $realCacheKey): void { + $cache->save($cacheItem->set($result + [$realCacheKey => $data])); + }; + } + + $stmt = $this->_doExecute(); + + if (is_numeric($stmt)) { + $setCacheEntry($stmt); + + return $stmt; + } + + $rsm = $this->getResultSetMapping(); + if ($rsm === null) { + throw new LogicException('Uninitialized result set mapping.'); + } + + $data = $this->em->newHydrator($this->hydrationMode)->hydrateAll($stmt, $rsm, $this->hints); + + $setCacheEntry($data); + + return $data; + } + + private function getHydrationCache(): CacheItemPoolInterface + { + assert($this->hydrationCacheProfile !== null); + + $cache = $this->hydrationCacheProfile->getResultCache(); + assert($cache !== null); + + return $cache; + } + + /** + * Load from second level cache or executes the query and put into cache. + * + * @psalm-param ArrayCollection|mixed[]|null $parameters + * @psalm-param string|AbstractQuery::HYDRATE_*|null $hydrationMode + */ + private function executeUsingQueryCache( + ArrayCollection|array|null $parameters = null, + string|int|null $hydrationMode = null, + ): mixed { + $rsm = $this->getResultSetMapping(); + if ($rsm === null) { + throw new LogicException('Uninitialized result set mapping.'); + } + + $queryCache = $this->em->getCache()->getQueryCache($this->cacheRegion); + $queryKey = new QueryCacheKey( + $this->getHash(), + $this->lifetime, + $this->cacheMode ?: Cache::MODE_NORMAL, + $this->getTimestampKey(), + ); + + $result = $queryCache->get($queryKey, $rsm, $this->hints); + + if ($result !== null) { + if ($this->cacheLogger) { + $this->cacheLogger->queryCacheHit($queryCache->getRegion()->getName(), $queryKey); + } + + return $result; + } + + $result = $this->executeIgnoreQueryCache($parameters, $hydrationMode); + $cached = $queryCache->put($queryKey, $rsm, $result, $this->hints); + + if ($this->cacheLogger) { + $this->cacheLogger->queryCacheMiss($queryCache->getRegion()->getName(), $queryKey); + + if ($cached) { + $this->cacheLogger->queryCachePut($queryCache->getRegion()->getName(), $queryKey); + } + } + + return $result; + } + + private function getTimestampKey(): TimestampCacheKey|null + { + assert($this->resultSetMapping !== null); + $entityName = reset($this->resultSetMapping->aliasMap); + + if (empty($entityName)) { + return null; + } + + $metadata = $this->em->getClassMetadata($entityName); + + return new TimestampCacheKey($metadata->rootEntityName); + } + + /** + * Get the result cache id to use to store the result set cache entry. + * Will return the configured id if it exists otherwise a hash will be + * automatically generated for you. + * + * @return string[] ($key, $hash) + * @psalm-return array{string, string} ($key, $hash) + */ + protected function getHydrationCacheId(): array + { + $parameters = []; + $types = []; + + foreach ($this->getParameters() as $parameter) { + $parameters[$parameter->getName()] = $this->processParameterValue($parameter->getValue()); + $types[$parameter->getName()] = $parameter->getType(); + } + + $sql = $this->getSQL(); + assert(is_string($sql)); + $queryCacheProfile = $this->getHydrationCacheProfile(); + $hints = $this->getHints(); + $hints['hydrationMode'] = $this->getHydrationMode(); + + ksort($hints); + assert($queryCacheProfile !== null); + + return $queryCacheProfile->generateCacheKeys($sql, $parameters, $types, $hints); + } + + /** + * Set the result cache id to use to store the result set cache entry. + * If this is not explicitly set by the developer then a hash is automatically + * generated for you. + */ + public function setResultCacheId(string|null $id): static + { + if (! $this->queryCacheProfile) { + return $this->setResultCacheProfile(new QueryCacheProfile(0, $id)); + } + + $this->queryCacheProfile = $this->queryCacheProfile->setCacheKey($id); + + return $this; + } + + /** + * Executes the query and returns a the resulting Statement object. + * + * @return Result|int The executed database statement that holds + * the results, or an integer indicating how + * many rows were affected. + */ + abstract protected function _doExecute(): Result|int; + + /** + * Cleanup Query resource when clone is called. + */ + public function __clone() + { + $this->parameters = new ArrayCollection(); + + $this->hints = []; + $this->hints = $this->em->getConfiguration()->getDefaultQueryHints(); + } + + /** + * Generates a string of currently query to use for the cache second level cache. + */ + protected function getHash(): string + { + $query = $this->getSQL(); + assert(is_string($query)); + $hints = $this->getHints(); + $params = array_map(function (Parameter $parameter) { + $value = $parameter->getValue(); + + // Small optimization + // Does not invoke processParameterValue for scalar value + if (is_scalar($value)) { + return $value; + } + + return $this->processParameterValue($value); + }, $this->parameters->getValues()); + + ksort($hints); + + return sha1($query . '-' . serialize($params) . '-' . serialize($hints)); + } +} 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 @@ + $identifier The entity identifier. + * @param class-string $class The entity class name + */ + public function __construct( + public readonly string $class, + public readonly array $identifier, + ) { + } + + /** + * Creates a new AssociationCacheEntry + * + * This method allow Doctrine\Common\Cache\PhpFileCache compatibility + * + * @param array $values array containing property values + */ + public static function __set_state(array $values): self + { + return new self($values['class'], $values['identifier']); + } +} 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 @@ +cacheFactory; + } + + public function setCacheFactory(CacheFactory $factory): void + { + $this->cacheFactory = $factory; + } + + public function getCacheLogger(): CacheLogger|null + { + return $this->cacheLogger; + } + + public function setCacheLogger(CacheLogger $logger): void + { + $this->cacheLogger = $logger; + } + + public function getRegionsConfiguration(): RegionsConfiguration + { + return $this->regionsConfig ??= new RegionsConfiguration(); + } + + public function setRegionsConfiguration(RegionsConfiguration $regionsConfig): void + { + $this->regionsConfig = $regionsConfig; + } + + public function getQueryValidator(): QueryCacheValidator + { + return $this->queryValidator ??= new TimestampQueryCacheValidator( + $this->cacheFactory->getTimestampRegion(), + ); + } + + public function setQueryValidator(QueryCacheValidator $validator): void + { + $this->queryValidator = $validator; + } +} 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 @@ +IMPORTANT NOTE: + * + * Fields of classes that implement CacheEntry are public for performance reason. + */ +interface CacheEntry +{ +} 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 @@ + $cache The cache configuration. + */ + public function getRegion(array $cache): Region; + + /** + * Build timestamp cache region + */ + public function getTimestampRegion(): TimestampRegion; + + /** + * Build \Doctrine\ORM\Cache + */ + public function createCache(EntityManagerInterface $entityManager): Cache; +} 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 @@ + $values array containing property values + */ + public static function __set_state(array $values): CollectionCacheEntry + { + return new self($values['identifiers']); + } +} 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 @@ + + */ + public readonly array $ownerIdentifier; + + /** + * @param array $ownerIdentifier The identifier of the owning entity. + * @param class-string $entityClass The owner entity class + */ + public function __construct( + public readonly string $entityClass, + public readonly string $association, + array $ownerIdentifier, + ) { + ksort($ownerIdentifier); + + $this->ownerIdentifier = $ownerIdentifier; + + parent::__construct(str_replace('\\', '.', strtolower($entityClass)) . '_' . implode(' ', $ownerIdentifier) . '__' . $association); + } +} 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 @@ + + */ + private array $queryCaches = []; + + private QueryCache|null $defaultQueryCache = null; + + public function __construct( + private readonly EntityManagerInterface $em, + ) { + $this->uow = $em->getUnitOfWork(); + $this->cacheFactory = $em->getConfiguration() + ->getSecondLevelCacheConfiguration() + ->getCacheFactory(); + } + + public function getEntityCacheRegion(string $className): Region|null + { + $metadata = $this->em->getClassMetadata($className); + $persister = $this->uow->getEntityPersister($metadata->rootEntityName); + + if (! ($persister instanceof CachedPersister)) { + return null; + } + + return $persister->getCacheRegion(); + } + + public function getCollectionCacheRegion(string $className, string $association): Region|null + { + $metadata = $this->em->getClassMetadata($className); + $persister = $this->uow->getCollectionPersister($metadata->getAssociationMapping($association)); + + if (! ($persister instanceof CachedPersister)) { + return null; + } + + return $persister->getCacheRegion(); + } + + public function containsEntity(string $className, mixed $identifier): bool + { + $metadata = $this->em->getClassMetadata($className); + $persister = $this->uow->getEntityPersister($metadata->rootEntityName); + + if (! ($persister instanceof CachedPersister)) { + return false; + } + + return $persister->getCacheRegion()->contains($this->buildEntityCacheKey($metadata, $identifier)); + } + + public function evictEntity(string $className, mixed $identifier): void + { + $metadata = $this->em->getClassMetadata($className); + $persister = $this->uow->getEntityPersister($metadata->rootEntityName); + + if (! ($persister instanceof CachedPersister)) { + return; + } + + $persister->getCacheRegion()->evict($this->buildEntityCacheKey($metadata, $identifier)); + } + + public function evictEntityRegion(string $className): void + { + $metadata = $this->em->getClassMetadata($className); + $persister = $this->uow->getEntityPersister($metadata->rootEntityName); + + if (! ($persister instanceof CachedPersister)) { + return; + } + + $persister->getCacheRegion()->evictAll(); + } + + public function evictEntityRegions(): void + { + $metadatas = $this->em->getMetadataFactory()->getAllMetadata(); + + foreach ($metadatas as $metadata) { + $persister = $this->uow->getEntityPersister($metadata->rootEntityName); + + if (! ($persister instanceof CachedPersister)) { + continue; + } + + $persister->getCacheRegion()->evictAll(); + } + } + + public function containsCollection(string $className, string $association, mixed $ownerIdentifier): bool + { + $metadata = $this->em->getClassMetadata($className); + $persister = $this->uow->getCollectionPersister($metadata->getAssociationMapping($association)); + + if (! ($persister instanceof CachedPersister)) { + return false; + } + + return $persister->getCacheRegion()->contains($this->buildCollectionCacheKey($metadata, $association, $ownerIdentifier)); + } + + public function evictCollection(string $className, string $association, mixed $ownerIdentifier): void + { + $metadata = $this->em->getClassMetadata($className); + $persister = $this->uow->getCollectionPersister($metadata->getAssociationMapping($association)); + + if (! ($persister instanceof CachedPersister)) { + return; + } + + $persister->getCacheRegion()->evict($this->buildCollectionCacheKey($metadata, $association, $ownerIdentifier)); + } + + public function evictCollectionRegion(string $className, string $association): void + { + $metadata = $this->em->getClassMetadata($className); + $persister = $this->uow->getCollectionPersister($metadata->getAssociationMapping($association)); + + if (! ($persister instanceof CachedPersister)) { + return; + } + + $persister->getCacheRegion()->evictAll(); + } + + public function evictCollectionRegions(): void + { + $metadatas = $this->em->getMetadataFactory()->getAllMetadata(); + + foreach ($metadatas as $metadata) { + foreach ($metadata->associationMappings as $association) { + if (! $association->isToMany()) { + continue; + } + + $persister = $this->uow->getCollectionPersister($association); + + if (! ($persister instanceof CachedPersister)) { + continue; + } + + $persister->getCacheRegion()->evictAll(); + } + } + } + + public function containsQuery(string $regionName): bool + { + return isset($this->queryCaches[$regionName]); + } + + public function evictQueryRegion(string|null $regionName = null): void + { + if ($regionName === null && $this->defaultQueryCache !== null) { + $this->defaultQueryCache->clear(); + + return; + } + + if (isset($this->queryCaches[$regionName])) { + $this->queryCaches[$regionName]->clear(); + } + } + + public function evictQueryRegions(): void + { + $this->getQueryCache()->clear(); + + foreach ($this->queryCaches as $queryCache) { + $queryCache->clear(); + } + } + + public function getQueryCache(string|null $regionName = null): QueryCache + { + if ($regionName === null) { + return $this->defaultQueryCache ??= $this->cacheFactory->buildQueryCache($this->em); + } + + return $this->queryCaches[$regionName] ??= $this->cacheFactory->buildQueryCache($this->em, $regionName); + } + + private function buildEntityCacheKey(ClassMetadata $metadata, mixed $identifier): EntityCacheKey + { + if (! is_array($identifier)) { + $identifier = $this->toIdentifierArray($metadata, $identifier); + } + + return new EntityCacheKey($metadata->rootEntityName, $identifier); + } + + private function buildCollectionCacheKey( + ClassMetadata $metadata, + string $association, + mixed $ownerIdentifier, + ): CollectionCacheKey { + if (! is_array($ownerIdentifier)) { + $ownerIdentifier = $this->toIdentifierArray($metadata, $ownerIdentifier); + } + + return new CollectionCacheKey($metadata->rootEntityName, $association, $ownerIdentifier); + } + + /** @return array */ + private function toIdentifierArray(ClassMetadata $metadata, mixed $identifier): array + { + if (is_object($identifier)) { + $class = DefaultProxyClassNameResolver::getClass($identifier); + if ($this->em->getMetadataFactory()->hasMetadataFor($class)) { + $identifier = $this->uow->getSingleIdentifierValue($identifier) + ?? throw ORMInvalidArgumentException::invalidIdentifierBindingEntity($class); + } + } + + return [$metadata->identifier[0] => $identifier]; + } +} 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 @@ +fileLockRegionDirectory = $fileLockRegionDirectory; + } + + public function getFileLockRegionDirectory(): string|null + { + return $this->fileLockRegionDirectory; + } + + public function setRegion(Region $region): void + { + $this->regions[$region->getName()] = $region; + } + + public function setTimestampRegion(TimestampRegion $region): void + { + $this->timestampRegion = $region; + } + + public function buildCachedEntityPersister(EntityManagerInterface $em, EntityPersister $persister, ClassMetadata $metadata): CachedEntityPersister + { + assert($metadata->cache !== null); + $region = $this->getRegion($metadata->cache); + $usage = $metadata->cache['usage']; + + if ($usage === ClassMetadata::CACHE_USAGE_READ_ONLY) { + return new ReadOnlyCachedEntityPersister($persister, $region, $em, $metadata); + } + + if ($usage === ClassMetadata::CACHE_USAGE_NONSTRICT_READ_WRITE) { + return new NonStrictReadWriteCachedEntityPersister($persister, $region, $em, $metadata); + } + + if ($usage === ClassMetadata::CACHE_USAGE_READ_WRITE) { + if (! $region instanceof ConcurrentRegion) { + throw new InvalidArgumentException(sprintf('Unable to use access strategy type of [%s] without a ConcurrentRegion', $usage)); + } + + return new ReadWriteCachedEntityPersister($persister, $region, $em, $metadata); + } + + throw new InvalidArgumentException(sprintf('Unrecognized access strategy type [%s]', $usage)); + } + + public function buildCachedCollectionPersister( + EntityManagerInterface $em, + CollectionPersister $persister, + AssociationMapping $mapping, + ): CachedCollectionPersister { + assert(isset($mapping->cache)); + $usage = $mapping->cache['usage']; + $region = $this->getRegion($mapping->cache); + + if ($usage === ClassMetadata::CACHE_USAGE_READ_ONLY) { + return new ReadOnlyCachedCollectionPersister($persister, $region, $em, $mapping); + } + + if ($usage === ClassMetadata::CACHE_USAGE_NONSTRICT_READ_WRITE) { + return new NonStrictReadWriteCachedCollectionPersister($persister, $region, $em, $mapping); + } + + if ($usage === ClassMetadata::CACHE_USAGE_READ_WRITE) { + if (! $region instanceof ConcurrentRegion) { + throw new InvalidArgumentException(sprintf('Unable to use access strategy type of [%s] without a ConcurrentRegion', $usage)); + } + + return new ReadWriteCachedCollectionPersister($persister, $region, $em, $mapping); + } + + throw new InvalidArgumentException(sprintf('Unrecognized access strategy type [%s]', $usage)); + } + + public function buildQueryCache(EntityManagerInterface $em, string|null $regionName = null): QueryCache + { + return new DefaultQueryCache( + $em, + $this->getRegion( + [ + 'region' => $regionName ?: Cache::DEFAULT_QUERY_REGION_NAME, + 'usage' => ClassMetadata::CACHE_USAGE_NONSTRICT_READ_WRITE, + ], + ), + ); + } + + public function buildCollectionHydrator(EntityManagerInterface $em, AssociationMapping $mapping): CollectionHydrator + { + return new DefaultCollectionHydrator($em); + } + + public function buildEntityHydrator(EntityManagerInterface $em, ClassMetadata $metadata): EntityHydrator + { + return new DefaultEntityHydrator($em); + } + + /** + * {@inheritDoc} + */ + public function getRegion(array $cache): Region + { + if (isset($this->regions[$cache['region']])) { + return $this->regions[$cache['region']]; + } + + $name = $cache['region']; + $lifetime = $this->regionsConfig->getLifetime($cache['region']); + $region = new DefaultRegion($name, $this->cacheItemPool, $lifetime); + + if ($cache['usage'] === ClassMetadata::CACHE_USAGE_READ_WRITE) { + if ( + $this->fileLockRegionDirectory === '' || + $this->fileLockRegionDirectory === null + ) { + throw new LogicException( + 'If you want to use a "READ_WRITE" cache an implementation of "Doctrine\ORM\Cache\ConcurrentRegion" is required, ' . + '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(). ', + ); + } + + $directory = $this->fileLockRegionDirectory . DIRECTORY_SEPARATOR . $cache['region']; + $region = new FileLockRegion($region, $directory, (string) $this->regionsConfig->getLockLifetime($cache['region'])); + } + + return $this->regions[$cache['region']] = $region; + } + + public function getTimestampRegion(): TimestampRegion + { + if ($this->timestampRegion === null) { + $name = Cache::DEFAULT_TIMESTAMP_REGION_NAME; + $lifetime = $this->regionsConfig->getLifetime($name); + + $this->timestampRegion = new UpdateTimestampCache($name, $this->cacheItemPool, $lifetime); + } + + return $this->timestampRegion; + } + + public function createCache(EntityManagerInterface $entityManager): Cache + { + return new DefaultCache($entityManager); + } +} 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 @@ + */ + private static array $hints = [Query::HINT_CACHE_ENABLED => true]; + + public function __construct( + private readonly EntityManagerInterface $em, + ) { + $this->uow = $em->getUnitOfWork(); + } + + public function buildCacheEntry(ClassMetadata $metadata, CollectionCacheKey $key, array|Collection $collection): CollectionCacheEntry + { + $data = []; + + foreach ($collection as $index => $entity) { + $data[$index] = new EntityCacheKey($metadata->rootEntityName, $this->uow->getEntityIdentifier($entity)); + } + + return new CollectionCacheEntry($data); + } + + public function loadCacheEntry(ClassMetadata $metadata, CollectionCacheKey $key, CollectionCacheEntry $entry, PersistentCollection $collection): array|null + { + $assoc = $metadata->associationMappings[$key->association]; + $targetPersister = $this->uow->getEntityPersister($assoc->targetEntity); + assert($targetPersister instanceof CachedPersister); + $targetRegion = $targetPersister->getCacheRegion(); + $list = []; + + /** @var EntityCacheEntry[]|null $entityEntries */ + $entityEntries = $targetRegion->getMultiple($entry); + + if ($entityEntries === null) { + return null; + } + + foreach ($entityEntries as $index => $entityEntry) { + $entity = $this->uow->createEntity( + $entityEntry->class, + $entityEntry->resolveAssociationEntries($this->em), + self::$hints, + ); + + $collection->hydrateSet($index, $entity); + + $list[$index] = $entity; + } + + $this->uow->hydrationComplete(); + + return $list; + } +} 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 @@ + */ + private static array $hints = [Query::HINT_CACHE_ENABLED => true]; + + public function __construct( + private readonly EntityManagerInterface $em, + ) { + $this->uow = $em->getUnitOfWork(); + $this->identifierFlattener = new IdentifierFlattener($em->getUnitOfWork(), $em->getMetadataFactory()); + } + + public function buildCacheEntry(ClassMetadata $metadata, EntityCacheKey $key, object $entity): EntityCacheEntry + { + $data = $this->uow->getOriginalEntityData($entity); + $data = [...$data, ...$metadata->getIdentifierValues($entity)]; // why update has no identifier values ? + + if ($metadata->requiresFetchAfterChange) { + if ($metadata->isVersioned) { + assert($metadata->versionField !== null); + $data[$metadata->versionField] = $metadata->getFieldValue($entity, $metadata->versionField); + } + + foreach ($metadata->fieldMappings as $name => $fieldMapping) { + if (isset($fieldMapping->generated)) { + $data[$name] = $metadata->getFieldValue($entity, $name); + } + } + } + + foreach ($metadata->associationMappings as $name => $assoc) { + if (! isset($data[$name])) { + continue; + } + + if (! $assoc->isToOne()) { + unset($data[$name]); + + continue; + } + + if (! isset($assoc->cache)) { + $targetClassMetadata = $this->em->getClassMetadata($assoc->targetEntity); + $owningAssociation = $this->em->getMetadataFactory()->getOwningSide($assoc); + $associationIds = $this->identifierFlattener->flattenIdentifier( + $targetClassMetadata, + $targetClassMetadata->getIdentifierValues($data[$name]), + ); + + unset($data[$name]); + + foreach ($associationIds as $fieldName => $fieldValue) { + if (isset($targetClassMetadata->fieldMappings[$fieldName])) { + assert($owningAssociation->isToOneOwningSide()); + $fieldMapping = $targetClassMetadata->fieldMappings[$fieldName]; + + $data[$owningAssociation->targetToSourceKeyColumns[$fieldMapping->columnName]] = $fieldValue; + + continue; + } + + $targetAssoc = $targetClassMetadata->associationMappings[$fieldName]; + + assert($assoc->isToOneOwningSide()); + foreach ($assoc->targetToSourceKeyColumns as $referencedColumn => $localColumn) { + if (isset($targetAssoc->sourceToTargetKeyColumns[$referencedColumn])) { + $data[$localColumn] = $fieldValue; + } + } + } + + continue; + } + + if (! isset($assoc->id)) { + $targetClass = DefaultProxyClassNameResolver::getClass($data[$name]); + $targetId = $this->uow->getEntityIdentifier($data[$name]); + $data[$name] = new AssociationCacheEntry($targetClass, $targetId); + + continue; + } + + // handle association identifier + $targetId = is_object($data[$name]) && $this->uow->isInIdentityMap($data[$name]) + ? $this->uow->getEntityIdentifier($data[$name]) + : $data[$name]; + + // @TODO - fix it ! + // handle UnitOfWork#createEntity hash generation + if (! is_array($targetId)) { + assert($assoc->isToOneOwningSide()); + $data[reset($assoc->joinColumnFieldNames)] = $targetId; + + $targetEntity = $this->em->getClassMetadata($assoc->targetEntity); + $targetId = [$targetEntity->identifier[0] => $targetId]; + } + + $data[$name] = new AssociationCacheEntry($assoc->targetEntity, $targetId); + } + + return new EntityCacheEntry($metadata->name, $data); + } + + public function loadCacheEntry(ClassMetadata $metadata, EntityCacheKey $key, EntityCacheEntry $entry, object|null $entity = null): object|null + { + $data = $entry->data; + $hints = self::$hints; + + if ($entity !== null) { + $hints[Query::HINT_REFRESH] = true; + $hints[Query::HINT_REFRESH_ENTITY] = $entity; + } + + foreach ($metadata->associationMappings as $name => $assoc) { + if (! isset($assoc->cache) || ! isset($data[$name])) { + continue; + } + + $assocClass = $data[$name]->class; + $assocId = $data[$name]->identifier; + $isEagerLoad = ($assoc->fetch === ClassMetadata::FETCH_EAGER || ($assoc->isOneToOne() && ! $assoc->isOwningSide())); + + if (! $isEagerLoad) { + $data[$name] = $this->em->getReference($assocClass, $assocId); + + continue; + } + + $assocMetadata = $this->em->getClassMetadata($assoc->targetEntity); + $assocKey = new EntityCacheKey($assocMetadata->rootEntityName, $assocId); + $assocPersister = $this->uow->getEntityPersister($assoc->targetEntity); + $assocRegion = $assocPersister->getCacheRegion(); + $assocEntry = $assocRegion->get($assocKey); + + if ($assocEntry === null) { + return null; + } + + $data[$name] = $this->uow->createEntity($assocEntry->class, $assocEntry->resolveAssociationEntries($this->em), $hints); + } + + if ($entity !== null) { + $this->uow->registerManaged($entity, $key->identifier, $data); + } + + $result = $this->uow->createEntity($entry->class, $data, $hints); + + $this->uow->hydrationComplete(); + + return $result; + } +} 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 @@ + */ + private static array $hints = [Query::HINT_CACHE_ENABLED => true]; + + public function __construct( + private readonly EntityManagerInterface $em, + private readonly Region $region, + ) { + $cacheConfig = $em->getConfiguration()->getSecondLevelCacheConfiguration(); + + $this->uow = $em->getUnitOfWork(); + $this->cacheLogger = $cacheConfig->getCacheLogger(); + $this->validator = $cacheConfig->getQueryValidator(); + } + + /** + * {@inheritDoc} + */ + public function get(QueryCacheKey $key, ResultSetMapping $rsm, array $hints = []): array|null + { + if (! ($key->cacheMode & Cache::MODE_GET)) { + return null; + } + + $cacheEntry = $this->region->get($key); + + if (! $cacheEntry instanceof QueryCacheEntry) { + return null; + } + + if (! $this->validator->isValid($key, $cacheEntry)) { + $this->region->evict($key); + + return null; + } + + $result = []; + $entityName = reset($rsm->aliasMap); + $hasRelation = ! empty($rsm->relationMap); + $persister = $this->uow->getEntityPersister($entityName); + assert($persister instanceof CachedEntityPersister); + + $region = $persister->getCacheRegion(); + $regionName = $region->getName(); + + $cm = $this->em->getClassMetadata($entityName); + + $generateKeys = static fn (array $entry): EntityCacheKey => new EntityCacheKey($cm->rootEntityName, $entry['identifier']); + + $cacheKeys = new CollectionCacheEntry(array_map($generateKeys, $cacheEntry->result)); + $entries = $region->getMultiple($cacheKeys) ?? []; + + // @TODO - move to cache hydration component + foreach ($cacheEntry->result as $index => $entry) { + $entityEntry = $entries[$index] ?? null; + + if (! $entityEntry instanceof EntityCacheEntry) { + $this->cacheLogger?->entityCacheMiss($regionName, $cacheKeys->identifiers[$index]); + + return null; + } + + $this->cacheLogger?->entityCacheHit($regionName, $cacheKeys->identifiers[$index]); + + if (! $hasRelation) { + $result[$index] = $this->uow->createEntity($entityEntry->class, $entityEntry->resolveAssociationEntries($this->em), self::$hints); + + continue; + } + + $data = $entityEntry->data; + + foreach ($entry['associations'] as $name => $assoc) { + $assocPersister = $this->uow->getEntityPersister($assoc['targetEntity']); + assert($assocPersister instanceof CachedEntityPersister); + + $assocRegion = $assocPersister->getCacheRegion(); + $assocMetadata = $this->em->getClassMetadata($assoc['targetEntity']); + + if ($assoc['type'] & ClassMetadata::TO_ONE) { + $assocKey = new EntityCacheKey($assocMetadata->rootEntityName, $assoc['identifier']); + $assocEntry = $assocRegion->get($assocKey); + + if ($assocEntry === null) { + $this->cacheLogger?->entityCacheMiss($assocRegion->getName(), $assocKey); + + $this->uow->hydrationComplete(); + + return null; + } + + $data[$name] = $this->uow->createEntity($assocEntry->class, $assocEntry->resolveAssociationEntries($this->em), self::$hints); + + $this->cacheLogger?->entityCacheHit($assocRegion->getName(), $assocKey); + + continue; + } + + if (! isset($assoc['list']) || empty($assoc['list'])) { + continue; + } + + $generateKeys = static fn (array $id): EntityCacheKey => new EntityCacheKey($assocMetadata->rootEntityName, $id); + + $collection = new PersistentCollection($this->em, $assocMetadata, new ArrayCollection()); + $assocKeys = new CollectionCacheEntry(array_map($generateKeys, $assoc['list'])); + $assocEntries = $assocRegion->getMultiple($assocKeys); + + foreach ($assoc['list'] as $assocIndex => $assocId) { + $assocEntry = is_array($assocEntries) ? ($assocEntries[$assocIndex] ?? null) : null; + + if ($assocEntry === null) { + $this->cacheLogger?->entityCacheMiss($assocRegion->getName(), $assocKeys->identifiers[$assocIndex]); + + $this->uow->hydrationComplete(); + + return null; + } + + $element = $this->uow->createEntity($assocEntry->class, $assocEntry->resolveAssociationEntries($this->em), self::$hints); + + $collection->hydrateSet($assocIndex, $element); + + $this->cacheLogger?->entityCacheHit($assocRegion->getName(), $assocKeys->identifiers[$assocIndex]); + } + + $data[$name] = $collection; + + $collection->setInitialized(true); + } + + foreach ($data as $fieldName => $unCachedAssociationData) { + // In some scenarios, such as EAGER+ASSOCIATION+ID+CACHE, the + // cache key information in `$cacheEntry` will not contain details + // for fields that are associations. + // + // This means that `$data` keys for some associations that may + // actually not be cached will not be converted to actual association + // data, yet they contain L2 cache AssociationCacheEntry objects. + // + // We need to unwrap those associations into proxy references, + // since we don't have actual data for them except for identifiers. + if ($unCachedAssociationData instanceof AssociationCacheEntry) { + $data[$fieldName] = $this->em->getReference( + $unCachedAssociationData->class, + $unCachedAssociationData->identifier, + ); + } + } + + $result[$index] = $this->uow->createEntity($entityEntry->class, $data, self::$hints); + } + + $this->uow->hydrationComplete(); + + return $result; + } + + /** + * {@inheritDoc} + */ + public function put(QueryCacheKey $key, ResultSetMapping $rsm, mixed $result, array $hints = []): bool + { + if ($rsm->scalarMappings) { + throw FeatureNotImplemented::scalarResults(); + } + + if (count($rsm->entityMappings) > 1) { + throw FeatureNotImplemented::multipleRootEntities(); + } + + if (! $rsm->isSelect) { + throw FeatureNotImplemented::nonSelectStatements(); + } + + if (! ($key->cacheMode & Cache::MODE_PUT)) { + return false; + } + + $data = []; + $entityName = reset($rsm->aliasMap); + $rootAlias = key($rsm->aliasMap); + $persister = $this->uow->getEntityPersister($entityName); + + if (! $persister instanceof CachedEntityPersister) { + throw NonCacheableEntity::fromEntity($entityName); + } + + $region = $persister->getCacheRegion(); + + $cm = $this->em->getClassMetadata($entityName); + assert($cm instanceof ClassMetadata); + + foreach ($result as $index => $entity) { + $identifier = $this->uow->getEntityIdentifier($entity); + $entityKey = new EntityCacheKey($cm->rootEntityName, $identifier); + + if (($key->cacheMode & Cache::MODE_REFRESH) || ! $region->contains($entityKey)) { + // Cancel put result if entity put fail + if (! $persister->storeEntityCache($entity, $entityKey)) { + return false; + } + } + + $data[$index]['identifier'] = $identifier; + $data[$index]['associations'] = []; + + // @TODO - move to cache hydration components + foreach ($rsm->relationMap as $alias => $name) { + $parentAlias = $rsm->parentAliasMap[$alias]; + $parentClass = $rsm->aliasMap[$parentAlias]; + $metadata = $this->em->getClassMetadata($parentClass); + $assoc = $metadata->associationMappings[$name]; + $assocValue = $this->getAssociationValue($rsm, $alias, $entity); + + if ($assocValue === null) { + continue; + } + + // root entity association + if ($rootAlias === $parentAlias) { + // Cancel put result if association put fail + $assocInfo = $this->storeAssociationCache($key, $assoc, $assocValue); + if ($assocInfo === null) { + return false; + } + + $data[$index]['associations'][$name] = $assocInfo; + + continue; + } + + // store single nested association + if (! is_array($assocValue)) { + // Cancel put result if association put fail + if ($this->storeAssociationCache($key, $assoc, $assocValue) === null) { + return false; + } + + continue; + } + + // store array of nested association + foreach ($assocValue as $aVal) { + // Cancel put result if association put fail + if ($this->storeAssociationCache($key, $assoc, $aVal) === null) { + return false; + } + } + } + } + + return $this->region->put($key, new QueryCacheEntry($data)); + } + + /** + * @return mixed[]|null + * @psalm-return array{targetEntity: class-string, type: mixed, list?: array[], identifier?: array}|null + */ + private function storeAssociationCache(QueryCacheKey $key, AssociationMapping $assoc, mixed $assocValue): array|null + { + $assocPersister = $this->uow->getEntityPersister($assoc->targetEntity); + $assocMetadata = $assocPersister->getClassMetadata(); + $assocRegion = $assocPersister->getCacheRegion(); + + // Handle *-to-one associations + if ($assoc->isToOne()) { + $assocIdentifier = $this->uow->getEntityIdentifier($assocValue); + $entityKey = new EntityCacheKey($assocMetadata->rootEntityName, $assocIdentifier); + + if (! $this->uow->isUninitializedObject($assocValue) && ($key->cacheMode & Cache::MODE_REFRESH) || ! $assocRegion->contains($entityKey)) { + // Entity put fail + if (! $assocPersister->storeEntityCache($assocValue, $entityKey)) { + return null; + } + } + + return [ + 'targetEntity' => $assocMetadata->rootEntityName, + 'identifier' => $assocIdentifier, + 'type' => $assoc->type(), + ]; + } + + // Handle *-to-many associations + $list = []; + + foreach ($assocValue as $assocItemIndex => $assocItem) { + $assocIdentifier = $this->uow->getEntityIdentifier($assocItem); + $entityKey = new EntityCacheKey($assocMetadata->rootEntityName, $assocIdentifier); + + if (($key->cacheMode & Cache::MODE_REFRESH) || ! $assocRegion->contains($entityKey)) { + // Entity put fail + if (! $assocPersister->storeEntityCache($assocItem, $entityKey)) { + return null; + } + } + + $list[$assocItemIndex] = $assocIdentifier; + } + + return [ + 'targetEntity' => $assocMetadata->rootEntityName, + 'type' => $assoc->type(), + 'list' => $list, + ]; + } + + /** @psalm-return list|object|null */ + private function getAssociationValue( + ResultSetMapping $rsm, + string $assocAlias, + object $entity, + ): array|object|null { + $path = []; + $alias = $assocAlias; + + while (isset($rsm->parentAliasMap[$alias])) { + $parent = $rsm->parentAliasMap[$alias]; + $field = $rsm->relationMap[$alias]; + $class = $rsm->aliasMap[$parent]; + + array_unshift($path, [ + 'field' => $field, + 'class' => $class, + ]); + + $alias = $parent; + } + + return $this->getAssociationPathValue($entity, $path); + } + + /** + * @psalm-param array $path + * + * @psalm-return list|object|null + */ + private function getAssociationPathValue(mixed $value, array $path): array|object|null + { + $mapping = array_shift($path); + $metadata = $this->em->getClassMetadata($mapping['class']); + $assoc = $metadata->associationMappings[$mapping['field']]; + $value = $metadata->getFieldValue($value, $mapping['field']); + + if ($value === null) { + return null; + } + + if ($path === []) { + return $value; + } + + // Handle *-to-one associations + if ($assoc->isToOne()) { + return $this->getAssociationPathValue($value, $path); + } + + $values = []; + + foreach ($value as $item) { + $values[] = $this->getAssociationPathValue($item, $path); + } + + return $values; + } + + public function clear(): bool + { + return $this->region->evictAll(); + } + + public function getRegion(): Region + { + return $this->region; + } +} 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 @@ + $data The entity map data + * @psalm-param class-string $class The entity class name + */ + public function __construct( + public readonly string $class, + public readonly array $data, + ) { + } + + /** + * Creates a new EntityCacheEntry + * + * This method allows Doctrine\Common\Cache\PhpFileCache compatibility + * + * @param array $values array containing property values + */ + public static function __set_state(array $values): self + { + return new self($values['class'], $values['data']); + } + + /** + * Retrieves the entity data resolving cache entries + * + * @return array + */ + public function resolveAssociationEntries(EntityManagerInterface $em): array + { + return array_map(static function ($value) use ($em) { + if (! ($value instanceof AssociationCacheEntry)) { + return $value; + } + + return $em->getReference($value->class, $value->identifier); + }, $this->data); + } +} 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 @@ + + */ + public readonly array $identifier; + + /** + * @param class-string $entityClass The entity class name. In a inheritance hierarchy it should always be the root entity class. + * @param array $identifier The entity identifier + */ + public function __construct( + public readonly string $entityClass, + array $identifier, + ) { + ksort($identifier); + + $this->identifier = $identifier; + + parent::__construct(str_replace('\\', '.', strtolower($entityClass) . '_' . implode(' ', $identifier))); + } +} 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 @@ +time = $time ?? time(); + } + + public static function createLockRead(): Lock + { + return new self(uniqid((string) time(), true)); + } +} 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 @@ + */ + private array $loggers = []; + + public function setLogger(string $name, CacheLogger $logger): void + { + $this->loggers[$name] = $logger; + } + + public function getLogger(string $name): CacheLogger|null + { + return $this->loggers[$name] ?? null; + } + + /** @return array */ + public function getLoggers(): array + { + return $this->loggers; + } + + public function collectionCacheHit(string $regionName, CollectionCacheKey $key): void + { + foreach ($this->loggers as $logger) { + $logger->collectionCacheHit($regionName, $key); + } + } + + public function collectionCacheMiss(string $regionName, CollectionCacheKey $key): void + { + foreach ($this->loggers as $logger) { + $logger->collectionCacheMiss($regionName, $key); + } + } + + public function collectionCachePut(string $regionName, CollectionCacheKey $key): void + { + foreach ($this->loggers as $logger) { + $logger->collectionCachePut($regionName, $key); + } + } + + public function entityCacheHit(string $regionName, EntityCacheKey $key): void + { + foreach ($this->loggers as $logger) { + $logger->entityCacheHit($regionName, $key); + } + } + + public function entityCacheMiss(string $regionName, EntityCacheKey $key): void + { + foreach ($this->loggers as $logger) { + $logger->entityCacheMiss($regionName, $key); + } + } + + public function entityCachePut(string $regionName, EntityCacheKey $key): void + { + foreach ($this->loggers as $logger) { + $logger->entityCachePut($regionName, $key); + } + } + + public function queryCacheHit(string $regionName, QueryCacheKey $key): void + { + foreach ($this->loggers as $logger) { + $logger->queryCacheHit($regionName, $key); + } + } + + public function queryCacheMiss(string $regionName, QueryCacheKey $key): void + { + foreach ($this->loggers as $logger) { + $logger->queryCacheMiss($regionName, $key); + } + } + + public function queryCachePut(string $regionName, QueryCacheKey $key): void + { + foreach ($this->loggers as $logger) { + $logger->queryCachePut($regionName, $key); + } + } +} 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 @@ + */ + private array $cacheMissCountMap = []; + + /** @var array */ + private array $cacheHitCountMap = []; + + /** @var array */ + private array $cachePutCountMap = []; + + public function collectionCacheMiss(string $regionName, CollectionCacheKey $key): void + { + $this->cacheMissCountMap[$regionName] + = ($this->cacheMissCountMap[$regionName] ?? 0) + 1; + } + + public function collectionCacheHit(string $regionName, CollectionCacheKey $key): void + { + $this->cacheHitCountMap[$regionName] + = ($this->cacheHitCountMap[$regionName] ?? 0) + 1; + } + + public function collectionCachePut(string $regionName, CollectionCacheKey $key): void + { + $this->cachePutCountMap[$regionName] + = ($this->cachePutCountMap[$regionName] ?? 0) + 1; + } + + public function entityCacheMiss(string $regionName, EntityCacheKey $key): void + { + $this->cacheMissCountMap[$regionName] + = ($this->cacheMissCountMap[$regionName] ?? 0) + 1; + } + + public function entityCacheHit(string $regionName, EntityCacheKey $key): void + { + $this->cacheHitCountMap[$regionName] + = ($this->cacheHitCountMap[$regionName] ?? 0) + 1; + } + + public function entityCachePut(string $regionName, EntityCacheKey $key): void + { + $this->cachePutCountMap[$regionName] + = ($this->cachePutCountMap[$regionName] ?? 0) + 1; + } + + public function queryCacheHit(string $regionName, QueryCacheKey $key): void + { + $this->cacheHitCountMap[$regionName] + = ($this->cacheHitCountMap[$regionName] ?? 0) + 1; + } + + public function queryCacheMiss(string $regionName, QueryCacheKey $key): void + { + $this->cacheMissCountMap[$regionName] + = ($this->cacheMissCountMap[$regionName] ?? 0) + 1; + } + + public function queryCachePut(string $regionName, QueryCacheKey $key): void + { + $this->cachePutCountMap[$regionName] + = ($this->cachePutCountMap[$regionName] ?? 0) + 1; + } + + /** + * Get the number of entries successfully retrieved from cache. + * + * @param string $regionName The name of the cache region. + */ + public function getRegionHitCount(string $regionName): int + { + return $this->cacheHitCountMap[$regionName] ?? 0; + } + + /** + * Get the number of cached entries *not* found in cache. + * + * @param string $regionName The name of the cache region. + */ + public function getRegionMissCount(string $regionName): int + { + return $this->cacheMissCountMap[$regionName] ?? 0; + } + + /** + * Get the number of cacheable entries put in cache. + * + * @param string $regionName The name of the cache region. + */ + public function getRegionPutCount(string $regionName): int + { + return $this->cachePutCountMap[$regionName] ?? 0; + } + + /** @return array */ + public function getRegionsMiss(): array + { + return $this->cacheMissCountMap; + } + + /** @return array */ + public function getRegionsHit(): array + { + return $this->cacheHitCountMap; + } + + /** @return array */ + public function getRegionsPut(): array + { + return $this->cachePutCountMap; + } + + /** + * Clear region statistics + * + * @param string $regionName The name of the cache region. + */ + public function clearRegionStats(string $regionName): void + { + $this->cachePutCountMap[$regionName] = 0; + $this->cacheHitCountMap[$regionName] = 0; + $this->cacheMissCountMap[$regionName] = 0; + } + + /** + * Clear all statistics + */ + public function clearStats(): void + { + $this->cachePutCountMap = []; + $this->cacheHitCountMap = []; + $this->cacheMissCountMap = []; + } + + /** + * Get the total number of put in cache. + */ + public function getPutCount(): int + { + return array_sum($this->cachePutCountMap); + } + + /** + * Get the total number of entries successfully retrieved from cache. + */ + public function getHitCount(): int + { + return array_sum($this->cacheHitCountMap); + } + + /** + * Get the total number of cached entries *not* found in cache. + */ + public function getMissCount(): int + { + return array_sum($this->cacheMissCountMap); + } +} 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 @@ +getConfiguration(); + $cacheConfig = $configuration->getSecondLevelCacheConfiguration(); + $cacheFactory = $cacheConfig->getCacheFactory(); + + $this->regionName = $region->getName(); + $this->uow = $em->getUnitOfWork(); + $this->metadataFactory = $em->getMetadataFactory(); + $this->cacheLogger = $cacheConfig->getCacheLogger(); + $this->hydrator = $cacheFactory->buildCollectionHydrator($em, $association); + $this->sourceEntity = $em->getClassMetadata($association->sourceEntity); + $this->targetEntity = $em->getClassMetadata($association->targetEntity); + } + + public function getCacheRegion(): Region + { + return $this->region; + } + + public function getSourceEntityMetadata(): ClassMetadata + { + return $this->sourceEntity; + } + + public function getTargetEntityMetadata(): ClassMetadata + { + return $this->targetEntity; + } + + public function loadCollectionCache(PersistentCollection $collection, CollectionCacheKey $key): array|null + { + $cache = $this->region->get($key); + + if ($cache === null) { + return null; + } + + return $this->hydrator->loadCacheEntry($this->sourceEntity, $key, $cache, $collection); + } + + public function storeCollectionCache(CollectionCacheKey $key, Collection|array $elements): void + { + $associationMapping = $this->sourceEntity->associationMappings[$key->association]; + $targetPersister = $this->uow->getEntityPersister($this->targetEntity->rootEntityName); + assert($targetPersister instanceof CachedEntityPersister); + $targetRegion = $targetPersister->getCacheRegion(); + $targetHydrator = $targetPersister->getEntityHydrator(); + + // Only preserve ordering if association configured it + if (! $associationMapping->isIndexed()) { + // Elements may be an array or a Collection + $elements = array_values($elements instanceof Collection ? $elements->getValues() : $elements); + } + + $entry = $this->hydrator->buildCacheEntry($this->targetEntity, $key, $elements); + + foreach ($entry->identifiers as $index => $entityKey) { + if ($targetRegion->contains($entityKey)) { + continue; + } + + $class = $this->targetEntity; + $className = DefaultProxyClassNameResolver::getClass($elements[$index]); + + if ($className !== $this->targetEntity->name) { + $class = $this->metadataFactory->getMetadataFor($className); + } + + $entity = $elements[$index]; + $entityEntry = $targetHydrator->buildCacheEntry($class, $entityKey, $entity); + + $targetRegion->put($entityKey, $entityEntry); + } + + if ($this->region->put($key, $entry)) { + $this->cacheLogger?->collectionCachePut($this->regionName, $key); + } + } + + public function contains(PersistentCollection $collection, object $element): bool + { + return $this->persister->contains($collection, $element); + } + + public function containsKey(PersistentCollection $collection, mixed $key): bool + { + return $this->persister->containsKey($collection, $key); + } + + public function count(PersistentCollection $collection): int + { + $ownerId = $this->uow->getEntityIdentifier($collection->getOwner()); + $key = new CollectionCacheKey($this->sourceEntity->rootEntityName, $this->association->fieldName, $ownerId); + $entry = $this->region->get($key); + + if ($entry !== null) { + return count($entry->identifiers); + } + + return $this->persister->count($collection); + } + + public function get(PersistentCollection $collection, mixed $index): mixed + { + return $this->persister->get($collection, $index); + } + + /** + * {@inheritDoc} + */ + public function slice(PersistentCollection $collection, int $offset, int|null $length = null): array + { + return $this->persister->slice($collection, $offset, $length); + } + + /** + * {@inheritDoc} + */ + public function loadCriteria(PersistentCollection $collection, Criteria $criteria): array + { + return $this->persister->loadCriteria($collection, $criteria); + } +} 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 @@ +queuedCache['update'])) { + foreach ($this->queuedCache['update'] as $item) { + $this->storeCollectionCache($item['key'], $item['list']); + } + } + + if (isset($this->queuedCache['delete'])) { + foreach ($this->queuedCache['delete'] as $key) { + $this->region->evict($key); + } + } + + $this->queuedCache = []; + } + + public function afterTransactionRolledBack(): void + { + $this->queuedCache = []; + } + + public function delete(PersistentCollection $collection): void + { + $ownerId = $this->uow->getEntityIdentifier($collection->getOwner()); + $key = new CollectionCacheKey($this->sourceEntity->rootEntityName, $this->association->fieldName, $ownerId); + + $this->persister->delete($collection); + + $this->queuedCache['delete'][spl_object_id($collection)] = $key; + } + + public function update(PersistentCollection $collection): void + { + $isInitialized = $collection->isInitialized(); + $isDirty = $collection->isDirty(); + + if (! $isInitialized && ! $isDirty) { + return; + } + + $ownerId = $this->uow->getEntityIdentifier($collection->getOwner()); + $key = new CollectionCacheKey($this->sourceEntity->rootEntityName, $this->association->fieldName, $ownerId); + + // Invalidate non initialized collections OR ordered collection + if ($isDirty && ! $isInitialized || $this->association->isOrdered()) { + $this->persister->update($collection); + + $this->queuedCache['delete'][spl_object_id($collection)] = $key; + + return; + } + + $this->persister->update($collection); + + $this->queuedCache['update'][spl_object_id($collection)] = [ + 'key' => $key, + 'list' => $collection, + ]; + } +} 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 @@ +isDirty() && $collection->getSnapshot()) { + throw CannotUpdateReadOnlyCollection::fromEntityAndField( + DefaultProxyClassNameResolver::getClass($collection->getOwner()), + $this->association->fieldName, + ); + } + + parent::update($collection); + } +} 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 @@ +queuedCache['update'])) { + foreach ($this->queuedCache['update'] as $item) { + $this->region->evict($item['key']); + } + } + + if (isset($this->queuedCache['delete'])) { + foreach ($this->queuedCache['delete'] as $item) { + $this->region->evict($item['key']); + } + } + + $this->queuedCache = []; + } + + public function afterTransactionRolledBack(): void + { + if (isset($this->queuedCache['update'])) { + foreach ($this->queuedCache['update'] as $item) { + $this->region->evict($item['key']); + } + } + + if (isset($this->queuedCache['delete'])) { + foreach ($this->queuedCache['delete'] as $item) { + $this->region->evict($item['key']); + } + } + + $this->queuedCache = []; + } + + public function delete(PersistentCollection $collection): void + { + $ownerId = $this->uow->getEntityIdentifier($collection->getOwner()); + $key = new CollectionCacheKey($this->sourceEntity->rootEntityName, $this->association->fieldName, $ownerId); + $lock = $this->region->lock($key); + + $this->persister->delete($collection); + + if ($lock === null) { + return; + } + + $this->queuedCache['delete'][spl_object_id($collection)] = [ + 'key' => $key, + 'lock' => $lock, + ]; + } + + public function update(PersistentCollection $collection): void + { + $isInitialized = $collection->isInitialized(); + $isDirty = $collection->isDirty(); + + if (! $isInitialized && ! $isDirty) { + return; + } + + $this->persister->update($collection); + + $ownerId = $this->uow->getEntityIdentifier($collection->getOwner()); + $key = new CollectionCacheKey($this->sourceEntity->rootEntityName, $this->association->fieldName, $ownerId); + $lock = $this->region->lock($key); + + if ($lock === null) { + return; + } + + $this->queuedCache['update'][spl_object_id($collection)] = [ + 'key' => $key, + 'lock' => $lock, + ]; + } +} 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 @@ +|null + */ + protected array|null $joinedAssociations = null; + + public function __construct( + protected EntityPersister $persister, + protected Region $region, + EntityManagerInterface $em, + protected ClassMetadata $class, + ) { + $configuration = $em->getConfiguration(); + $cacheConfig = $configuration->getSecondLevelCacheConfiguration(); + $cacheFactory = $cacheConfig->getCacheFactory(); + + $this->cache = $em->getCache(); + $this->regionName = $region->getName(); + $this->uow = $em->getUnitOfWork(); + $this->metadataFactory = $em->getMetadataFactory(); + $this->cacheLogger = $cacheConfig->getCacheLogger(); + $this->timestampRegion = $cacheFactory->getTimestampRegion(); + $this->hydrator = $cacheFactory->buildEntityHydrator($em, $class); + $this->timestampKey = new TimestampCacheKey($this->class->rootEntityName); + } + + public function addInsert(object $entity): void + { + $this->persister->addInsert($entity); + } + + /** + * {@inheritDoc} + */ + public function getInserts(): array + { + return $this->persister->getInserts(); + } + + public function getSelectSQL( + array|Criteria $criteria, + AssociationMapping|null $assoc = null, + LockMode|int|null $lockMode = null, + int|null $limit = null, + int|null $offset = null, + array|null $orderBy = null, + ): string { + return $this->persister->getSelectSQL($criteria, $assoc, $lockMode, $limit, $offset, $orderBy); + } + + public function getCountSQL(array|Criteria $criteria = []): string + { + return $this->persister->getCountSQL($criteria); + } + + public function getInsertSQL(): string + { + return $this->persister->getInsertSQL(); + } + + public function getResultSetMapping(): ResultSetMapping + { + return $this->persister->getResultSetMapping(); + } + + public function getSelectConditionStatementSQL( + string $field, + mixed $value, + AssociationMapping|null $assoc = null, + string|null $comparison = null, + ): string { + return $this->persister->getSelectConditionStatementSQL($field, $value, $assoc, $comparison); + } + + public function exists(object $entity, Criteria|null $extraConditions = null): bool + { + if ($extraConditions === null) { + $key = new EntityCacheKey($this->class->rootEntityName, $this->class->getIdentifierValues($entity)); + + if ($this->region->contains($key)) { + return true; + } + } + + return $this->persister->exists($entity, $extraConditions); + } + + public function getCacheRegion(): Region + { + return $this->region; + } + + public function getEntityHydrator(): EntityHydrator + { + return $this->hydrator; + } + + public function storeEntityCache(object $entity, EntityCacheKey $key): bool + { + $class = $this->class; + $className = DefaultProxyClassNameResolver::getClass($entity); + + if ($className !== $this->class->name) { + $class = $this->metadataFactory->getMetadataFor($className); + } + + $entry = $this->hydrator->buildCacheEntry($class, $key, $entity); + $cached = $this->region->put($key, $entry); + + if ($cached) { + $this->cacheLogger?->entityCachePut($this->regionName, $key); + } + + return $cached; + } + + private function storeJoinedAssociations(object $entity): void + { + if ($this->joinedAssociations === null) { + $associations = []; + + foreach ($this->class->associationMappings as $name => $assoc) { + if ( + isset($assoc->cache) && + ($assoc->isToOne()) && + ($assoc->fetch === ClassMetadata::FETCH_EAGER || ! $assoc->isOwningSide()) + ) { + $associations[] = $name; + } + } + + $this->joinedAssociations = $associations; + } + + foreach ($this->joinedAssociations as $name) { + $assoc = $this->class->associationMappings[$name]; + $assocEntity = $this->class->getFieldValue($entity, $name); + + if ($assocEntity === null) { + continue; + } + + $assocId = $this->uow->getEntityIdentifier($assocEntity); + $assocMetadata = $this->metadataFactory->getMetadataFor($assoc->targetEntity); + $assocKey = new EntityCacheKey($assocMetadata->rootEntityName, $assocId); + $assocPersister = $this->uow->getEntityPersister($assoc->targetEntity); + + $assocPersister->storeEntityCache($assocEntity, $assocKey); + } + } + + /** + * Generates a string of currently query + * + * @param string[]|Criteria $criteria + * @param array|null $orderBy + */ + protected function getHash( + string $query, + array|Criteria $criteria, + array|null $orderBy = null, + int|null $limit = null, + int|null $offset = null, + ): string { + [$params] = $criteria instanceof Criteria + ? $this->persister->expandCriteriaParameters($criteria) + : $this->persister->expandParameters($criteria); + + return sha1($query . serialize($params) . serialize($orderBy) . $limit . $offset); + } + + /** + * {@inheritDoc} + */ + public function expandParameters(array $criteria): array + { + return $this->persister->expandParameters($criteria); + } + + /** + * {@inheritDoc} + */ + public function expandCriteriaParameters(Criteria $criteria): array + { + return $this->persister->expandCriteriaParameters($criteria); + } + + public function getClassMetadata(): ClassMetadata + { + return $this->persister->getClassMetadata(); + } + + /** + * {@inheritDoc} + */ + public function getManyToManyCollection( + AssociationMapping $assoc, + object $sourceEntity, + int|null $offset = null, + int|null $limit = null, + ): array { + return $this->persister->getManyToManyCollection($assoc, $sourceEntity, $offset, $limit); + } + + /** + * {@inheritDoc} + */ + public function getOneToManyCollection( + AssociationMapping $assoc, + object $sourceEntity, + int|null $offset = null, + int|null $limit = null, + ): array { + return $this->persister->getOneToManyCollection($assoc, $sourceEntity, $offset, $limit); + } + + public function getOwningTable(string $fieldName): string + { + return $this->persister->getOwningTable($fieldName); + } + + public function executeInserts(): void + { + // The commit order/foreign key relationships may make it necessary that multiple calls to executeInsert() + // are performed, so collect all the new entities. + $newInserts = $this->persister->getInserts(); + + if ($newInserts) { + $this->queuedCache['insert'] = array_merge($this->queuedCache['insert'] ?? [], $newInserts); + } + + $this->persister->executeInserts(); + } + + /** + * {@inheritDoc} + */ + public function load( + array $criteria, + object|null $entity = null, + AssociationMapping|null $assoc = null, + array $hints = [], + LockMode|int|null $lockMode = null, + int|null $limit = null, + array|null $orderBy = null, + ): object|null { + if ($entity !== null || $assoc !== null || $hints !== [] || $lockMode !== null) { + return $this->persister->load($criteria, $entity, $assoc, $hints, $lockMode, $limit, $orderBy); + } + + //handle only EntityRepository#findOneBy + $query = $this->persister->getSelectSQL($criteria, null, null, $limit, null, $orderBy); + $hash = $this->getHash($query, $criteria); + $rsm = $this->getResultSetMapping(); + $queryKey = new QueryCacheKey($hash, 0, Cache::MODE_NORMAL, $this->timestampKey); + $queryCache = $this->cache->getQueryCache($this->regionName); + $result = $queryCache->get($queryKey, $rsm); + + if ($result !== null) { + $this->cacheLogger?->queryCacheHit($this->regionName, $queryKey); + + return $result[0]; + } + + $result = $this->persister->load($criteria, $entity, $assoc, $hints, $lockMode, $limit, $orderBy); + + if ($result === null) { + return null; + } + + $cached = $queryCache->put($queryKey, $rsm, [$result]); + + $this->cacheLogger?->queryCacheMiss($this->regionName, $queryKey); + + if ($cached) { + $this->cacheLogger?->queryCachePut($this->regionName, $queryKey); + } + + return $result; + } + + /** + * {@inheritDoc} + */ + public function loadAll( + array $criteria = [], + array|null $orderBy = null, + int|null $limit = null, + int|null $offset = null, + ): array { + $query = $this->persister->getSelectSQL($criteria, null, null, $limit, $offset, $orderBy); + $hash = $this->getHash($query, $criteria); + $rsm = $this->getResultSetMapping(); + $queryKey = new QueryCacheKey($hash, 0, Cache::MODE_NORMAL, $this->timestampKey); + $queryCache = $this->cache->getQueryCache($this->regionName); + $result = $queryCache->get($queryKey, $rsm); + + if ($result !== null) { + $this->cacheLogger?->queryCacheHit($this->regionName, $queryKey); + + return $result; + } + + $result = $this->persister->loadAll($criteria, $orderBy, $limit, $offset); + $cached = $queryCache->put($queryKey, $rsm, $result); + + if ($result) { + $this->cacheLogger?->queryCacheMiss($this->regionName, $queryKey); + } + + if ($cached) { + $this->cacheLogger?->queryCachePut($this->regionName, $queryKey); + } + + return $result; + } + + /** + * {@inheritDoc} + */ + public function loadById(array $identifier, object|null $entity = null): object|null + { + $cacheKey = new EntityCacheKey($this->class->rootEntityName, $identifier); + $cacheEntry = $this->region->get($cacheKey); + $class = $this->class; + + if ($cacheEntry !== null) { + if ($cacheEntry->class !== $this->class->name) { + $class = $this->metadataFactory->getMetadataFor($cacheEntry->class); + } + + $cachedEntity = $this->hydrator->loadCacheEntry($class, $cacheKey, $cacheEntry, $entity); + + if ($cachedEntity !== null) { + $this->cacheLogger?->entityCacheHit($this->regionName, $cacheKey); + + return $cachedEntity; + } + } + + $entity = $this->persister->loadById($identifier, $entity); + + if ($entity === null) { + return null; + } + + $class = $this->class; + $className = DefaultProxyClassNameResolver::getClass($entity); + + if ($className !== $this->class->name) { + $class = $this->metadataFactory->getMetadataFor($className); + } + + $cacheEntry = $this->hydrator->buildCacheEntry($class, $cacheKey, $entity); + $cached = $this->region->put($cacheKey, $cacheEntry); + + if ($cached && ($this->joinedAssociations === null || $this->joinedAssociations)) { + $this->storeJoinedAssociations($entity); + } + + if ($cached) { + $this->cacheLogger?->entityCachePut($this->regionName, $cacheKey); + } + + $this->cacheLogger?->entityCacheMiss($this->regionName, $cacheKey); + + return $entity; + } + + public function count(array|Criteria $criteria = []): int + { + return $this->persister->count($criteria); + } + + /** + * {@inheritDoc} + */ + public function loadCriteria(Criteria $criteria): array + { + $orderBy = $criteria->orderings(); + $limit = $criteria->getMaxResults(); + $offset = $criteria->getFirstResult(); + $query = $this->persister->getSelectSQL($criteria); + $hash = $this->getHash($query, $criteria, $orderBy, $limit, $offset); + $rsm = $this->getResultSetMapping(); + $queryKey = new QueryCacheKey($hash, 0, Cache::MODE_NORMAL, $this->timestampKey); + $queryCache = $this->cache->getQueryCache($this->regionName); + $cacheResult = $queryCache->get($queryKey, $rsm); + + if ($cacheResult !== null) { + $this->cacheLogger?->queryCacheHit($this->regionName, $queryKey); + + return $cacheResult; + } + + $result = $this->persister->loadCriteria($criteria); + $cached = $queryCache->put($queryKey, $rsm, $result); + + if ($result) { + $this->cacheLogger?->queryCacheMiss($this->regionName, $queryKey); + } + + if ($cached) { + $this->cacheLogger?->queryCachePut($this->regionName, $queryKey); + } + + return $result; + } + + /** + * {@inheritDoc} + */ + public function loadManyToManyCollection( + AssociationMapping $assoc, + object $sourceEntity, + PersistentCollection $collection, + ): array { + $persister = $this->uow->getCollectionPersister($assoc); + $hasCache = ($persister instanceof CachedPersister); + + if (! $hasCache) { + return $this->persister->loadManyToManyCollection($assoc, $sourceEntity, $collection); + } + + $ownerId = $this->uow->getEntityIdentifier($collection->getOwner()); + $key = $this->buildCollectionCacheKey($assoc, $ownerId); + $list = $persister->loadCollectionCache($collection, $key); + + if ($list !== null) { + $this->cacheLogger?->collectionCacheHit($persister->getCacheRegion()->getName(), $key); + + return $list; + } + + $list = $this->persister->loadManyToManyCollection($assoc, $sourceEntity, $collection); + + $persister->storeCollectionCache($key, $list); + + $this->cacheLogger?->collectionCacheMiss($persister->getCacheRegion()->getName(), $key); + + return $list; + } + + public function loadOneToManyCollection( + AssociationMapping $assoc, + object $sourceEntity, + PersistentCollection $collection, + ): mixed { + $persister = $this->uow->getCollectionPersister($assoc); + $hasCache = ($persister instanceof CachedPersister); + + if (! $hasCache) { + return $this->persister->loadOneToManyCollection($assoc, $sourceEntity, $collection); + } + + $ownerId = $this->uow->getEntityIdentifier($collection->getOwner()); + $key = $this->buildCollectionCacheKey($assoc, $ownerId); + $list = $persister->loadCollectionCache($collection, $key); + + if ($list !== null) { + $this->cacheLogger?->collectionCacheHit($persister->getCacheRegion()->getName(), $key); + + return $list; + } + + $list = $this->persister->loadOneToManyCollection($assoc, $sourceEntity, $collection); + + $persister->storeCollectionCache($key, $list); + + $this->cacheLogger?->collectionCacheMiss($persister->getCacheRegion()->getName(), $key); + + return $list; + } + + /** + * {@inheritDoc} + */ + public function loadOneToOneEntity(AssociationMapping $assoc, object $sourceEntity, array $identifier = []): object|null + { + return $this->persister->loadOneToOneEntity($assoc, $sourceEntity, $identifier); + } + + /** + * {@inheritDoc} + */ + public function lock(array $criteria, LockMode|int $lockMode): void + { + $this->persister->lock($criteria, $lockMode); + } + + /** + * {@inheritDoc} + */ + public function refresh(array $id, object $entity, LockMode|int|null $lockMode = null): void + { + $this->persister->refresh($id, $entity, $lockMode); + } + + /** @param array $ownerId */ + protected function buildCollectionCacheKey(AssociationMapping $association, array $ownerId): CollectionCacheKey + { + $metadata = $this->metadataFactory->getMetadataFor($association->sourceEntity); + assert($metadata instanceof ClassMetadata); + + return new CollectionCacheKey($metadata->rootEntityName, $association->fieldName, $ownerId); + } +} 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 @@ +queuedCache['insert'])) { + foreach ($this->queuedCache['insert'] as $entity) { + $isChanged = $this->updateCache($entity, $isChanged); + } + } + + if (isset($this->queuedCache['update'])) { + foreach ($this->queuedCache['update'] as $entity) { + $isChanged = $this->updateCache($entity, $isChanged); + } + } + + if (isset($this->queuedCache['delete'])) { + foreach ($this->queuedCache['delete'] as $key) { + $this->region->evict($key); + + $isChanged = true; + } + } + + if ($isChanged) { + $this->timestampRegion->update($this->timestampKey); + } + + $this->queuedCache = []; + } + + public function afterTransactionRolledBack(): void + { + $this->queuedCache = []; + } + + public function delete(object $entity): bool + { + $key = new EntityCacheKey($this->class->rootEntityName, $this->uow->getEntityIdentifier($entity)); + $deleted = $this->persister->delete($entity); + + if ($deleted) { + $this->region->evict($key); + } + + $this->queuedCache['delete'][] = $key; + + return $deleted; + } + + public function update(object $entity): void + { + $this->persister->update($entity); + + $this->queuedCache['update'][] = $entity; + } + + private function updateCache(object $entity, bool $isChanged): bool + { + $class = $this->metadataFactory->getMetadataFor($entity::class); + $key = new EntityCacheKey($class->rootEntityName, $this->uow->getEntityIdentifier($entity)); + $entry = $this->hydrator->buildCacheEntry($class, $key, $entity); + $cached = $this->region->put($key, $entry); + $isChanged = $isChanged || $cached; + + if ($cached) { + $this->cacheLogger?->entityCachePut($this->regionName, $key); + } + + return $isChanged; + } +} 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 @@ +queuedCache['update'])) { + foreach ($this->queuedCache['update'] as $item) { + $this->region->evict($item['key']); + + $isChanged = true; + } + } + + if (isset($this->queuedCache['delete'])) { + foreach ($this->queuedCache['delete'] as $item) { + $this->region->evict($item['key']); + + $isChanged = true; + } + } + + if ($isChanged) { + $this->timestampRegion->update($this->timestampKey); + } + + $this->queuedCache = []; + } + + public function afterTransactionRolledBack(): void + { + if (isset($this->queuedCache['update'])) { + foreach ($this->queuedCache['update'] as $item) { + $this->region->evict($item['key']); + } + } + + if (isset($this->queuedCache['delete'])) { + foreach ($this->queuedCache['delete'] as $item) { + $this->region->evict($item['key']); + } + } + + $this->queuedCache = []; + } + + public function delete(object $entity): bool + { + $key = new EntityCacheKey($this->class->rootEntityName, $this->uow->getEntityIdentifier($entity)); + $lock = $this->region->lock($key); + $deleted = $this->persister->delete($entity); + + if ($deleted) { + $this->region->evict($key); + } + + if ($lock === null) { + return $deleted; + } + + $this->queuedCache['delete'][] = [ + 'lock' => $lock, + 'key' => $key, + ]; + + return $deleted; + } + + public function update(object $entity): void + { + $key = new EntityCacheKey($this->class->rootEntityName, $this->uow->getEntityIdentifier($entity)); + $lock = $this->region->lock($key); + + $this->persister->update($entity); + + if ($lock === null) { + return; + } + + $this->queuedCache['update'][] = [ + 'lock' => $lock, + 'key' => $key, + ]; + } +} 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 @@ + $result List of entity identifiers */ + public function __construct( + public readonly array $result, + float|null $time = null, + ) { + $this->time = $time ?: microtime(true); + } + + /** @param array $values */ + public static function __set_state(array $values): self + { + return new self($values['result'], $values['time']); + } +} 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 @@ +name; + } + + public function contains(CacheKey $key): bool + { + return $this->cacheItemPool->hasItem($this->getCacheEntryKey($key)); + } + + public function get(CacheKey $key): CacheEntry|null + { + $item = $this->cacheItemPool->getItem($this->getCacheEntryKey($key)); + $entry = $item->isHit() ? $item->get() : null; + + if (! $entry instanceof CacheEntry) { + return null; + } + + return $entry; + } + + public function getMultiple(CollectionCacheEntry $collection): array|null + { + $keys = array_map( + $this->getCacheEntryKey(...), + $collection->identifiers, + ); + /** @var iterable $items */ + $items = $this->cacheItemPool->getItems($keys); + if ($items instanceof Traversable) { + $items = iterator_to_array($items); + } + + $result = []; + foreach ($keys as $arrayKey => $cacheKey) { + if (! isset($items[$cacheKey]) || ! $items[$cacheKey]->isHit()) { + return null; + } + + $entry = $items[$cacheKey]->get(); + if (! $entry instanceof CacheEntry) { + return null; + } + + $result[$arrayKey] = $entry; + } + + return $result; + } + + public function put(CacheKey $key, CacheEntry $entry, Lock|null $lock = null): bool + { + $item = $this->cacheItemPool + ->getItem($this->getCacheEntryKey($key)) + ->set($entry); + + if ($this->lifetime > 0) { + $item->expiresAfter($this->lifetime); + } + + return $this->cacheItemPool->save($item); + } + + public function evict(CacheKey $key): bool + { + return $this->cacheItemPool->deleteItem($this->getCacheEntryKey($key)); + } + + public function evictAll(): bool + { + return $this->cacheItemPool->clear(self::REGION_PREFIX . $this->name); + } + + private function getCacheEntryKey(CacheKey $key): string + { + return self::REGION_PREFIX . $this->name . self::REGION_KEY_SEPARATOR . strtr($key->hash, '{}()/\@:', '________'); + } +} 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 @@ +getLockFileName($key); + + if (! is_file($filename)) { + return false; + } + + $time = $this->getLockTime($filename); + $content = $this->getLockContent($filename); + + if ($content === false || $time === false) { + @unlink($filename); + + return false; + } + + if ($lock && $content === $lock->value) { + return false; + } + + // outdated lock + if ($time + $this->lockLifetime <= time()) { + @unlink($filename); + + return false; + } + + return true; + } + + private function getLockFileName(CacheKey $key): string + { + return $this->directory . DIRECTORY_SEPARATOR . $key->hash . '.' . self::LOCK_EXTENSION; + } + + private function getLockContent(string $filename): string|false + { + return @file_get_contents($filename); + } + + private function getLockTime(string $filename): int|false + { + return @fileatime($filename); + } + + public function getName(): string + { + return $this->region->getName(); + } + + public function contains(CacheKey $key): bool + { + if ($this->isLocked($key)) { + return false; + } + + return $this->region->contains($key); + } + + public function get(CacheKey $key): CacheEntry|null + { + if ($this->isLocked($key)) { + return null; + } + + return $this->region->get($key); + } + + public function getMultiple(CollectionCacheEntry $collection): array|null + { + if (array_filter(array_map($this->isLocked(...), $collection->identifiers))) { + return null; + } + + return $this->region->getMultiple($collection); + } + + public function put(CacheKey $key, CacheEntry $entry, Lock|null $lock = null): bool + { + if ($this->isLocked($key, $lock)) { + return false; + } + + return $this->region->put($key, $entry); + } + + public function evict(CacheKey $key): bool + { + if ($this->isLocked($key)) { + @unlink($this->getLockFileName($key)); + } + + return $this->region->evict($key); + } + + public function evictAll(): bool + { + // The check below is necessary because on some platforms glob returns false + // when nothing matched (even though no errors occurred) + $filenames = glob(sprintf('%s/*.%s', $this->directory, self::LOCK_EXTENSION)) ?: []; + + foreach ($filenames as $filename) { + @unlink($filename); + } + + return $this->region->evictAll(); + } + + public function lock(CacheKey $key): Lock|null + { + if ($this->isLocked($key)) { + return null; + } + + $lock = Lock::createLockRead(); + $filename = $this->getLockFileName($key); + + if (@file_put_contents($filename, $lock->value, LOCK_EX) === false) { + return null; + } + + chmod($filename, 0664); + + return $lock; + } + + public function unlock(CacheKey $key, Lock $lock): bool + { + if ($this->isLocked($key, $lock)) { + return false; + } + + return @unlink($this->getLockFileName($key)); + } +} 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 @@ +put($key, new TimestampCacheEntry()); + } +} 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 @@ + */ + private array $lifetimes = []; + + /** @var array */ + private array $lockLifetimes = []; + + public function __construct( + private int $defaultLifetime = 3600, + private int $defaultLockLifetime = 60, + ) { + } + + public function getDefaultLifetime(): int + { + return $this->defaultLifetime; + } + + public function setDefaultLifetime(int $defaultLifetime): void + { + $this->defaultLifetime = $defaultLifetime; + } + + public function getDefaultLockLifetime(): int + { + return $this->defaultLockLifetime; + } + + public function setDefaultLockLifetime(int $defaultLockLifetime): void + { + $this->defaultLockLifetime = $defaultLockLifetime; + } + + public function getLifetime(string $regionName): int + { + return $this->lifetimes[$regionName] ?? $this->defaultLifetime; + } + + public function setLifetime(string $name, int $lifetime): void + { + $this->lifetimes[$name] = $lifetime; + } + + public function getLockLifetime(string $regionName): int + { + return $this->lockLifetimes[$regionName] ?? $this->defaultLockLifetime; + } + + public function setLockLifetime(string $name, int $lifetime): void + { + $this->lockLifetimes[$name] = $lifetime; + } +} 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 @@ +time = $time ?? microtime(true); + } + + /** + * Creates a new TimestampCacheEntry + * + * This method allow Doctrine\Common\Cache\PhpFileCache compatibility + * + * @param array $values array containing property values + */ + public static function __set_state(array $values): TimestampCacheEntry + { + return new self($values['time']); + } +} 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 @@ +regionUpdated($key, $entry)) { + return false; + } + + if ($key->lifetime === 0) { + return true; + } + + return $entry->time + $key->lifetime > microtime(true); + } + + private function regionUpdated(QueryCacheKey $key, QueryCacheEntry $entry): bool + { + if ($key->timestampKey === null) { + return false; + } + + $timestamp = $this->timestampRegion->get($key->timestampKey); + + return $timestamp && $timestamp->time > $entry->time; + } +} 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 @@ +, ClassMetadata::GENERATOR_TYPE_*> */ + private $identityGenerationPreferences = []; + + /** @psalm-param array, ClassMetadata::GENERATOR_TYPE_*> $value */ + public function setIdentityGenerationPreferences(array $value): void + { + $this->identityGenerationPreferences = $value; + } + + /** @psalm-return array, ClassMetadata::GENERATOR_TYPE_*> $value */ + public function getIdentityGenerationPreferences(): array + { + return $this->identityGenerationPreferences; + } + + /** + * Sets the directory where Doctrine generates any necessary proxy class files. + */ + public function setProxyDir(string $dir): void + { + $this->attributes['proxyDir'] = $dir; + } + + /** + * Gets the directory where Doctrine generates any necessary proxy class files. + */ + public function getProxyDir(): string|null + { + return $this->attributes['proxyDir'] ?? null; + } + + /** + * Gets the strategy for automatically generating proxy classes. + * + * @return ProxyFactory::AUTOGENERATE_* + */ + public function getAutoGenerateProxyClasses(): int + { + return $this->attributes['autoGenerateProxyClasses'] ?? ProxyFactory::AUTOGENERATE_ALWAYS; + } + + /** + * Sets the strategy for automatically generating proxy classes. + * + * @param bool|ProxyFactory::AUTOGENERATE_* $autoGenerate True is converted to AUTOGENERATE_ALWAYS, false to AUTOGENERATE_NEVER. + */ + public function setAutoGenerateProxyClasses(bool|int $autoGenerate): void + { + $this->attributes['autoGenerateProxyClasses'] = (int) $autoGenerate; + } + + /** + * Gets the namespace where proxy classes reside. + */ + public function getProxyNamespace(): string|null + { + return $this->attributes['proxyNamespace'] ?? null; + } + + /** + * Sets the namespace where proxy classes reside. + */ + public function setProxyNamespace(string $ns): void + { + $this->attributes['proxyNamespace'] = $ns; + } + + /** + * Sets the cache driver implementation that is used for metadata caching. + * + * @todo Force parameter to be a Closure to ensure lazy evaluation + * (as soon as a metadata cache is in effect, the driver never needs to initialize). + */ + public function setMetadataDriverImpl(MappingDriver $driverImpl): void + { + $this->attributes['metadataDriverImpl'] = $driverImpl; + } + + /** + * Sets the entity alias map. + * + * @psalm-param array $entityNamespaces + */ + public function setEntityNamespaces(array $entityNamespaces): void + { + $this->attributes['entityNamespaces'] = $entityNamespaces; + } + + /** + * Retrieves the list of registered entity namespace aliases. + * + * @psalm-return array + */ + public function getEntityNamespaces(): array + { + return $this->attributes['entityNamespaces']; + } + + /** + * Gets the cache driver implementation that is used for the mapping metadata. + */ + public function getMetadataDriverImpl(): MappingDriver|null + { + return $this->attributes['metadataDriverImpl'] ?? null; + } + + /** + * Gets the cache driver implementation that is used for the query cache (SQL cache). + */ + public function getQueryCache(): CacheItemPoolInterface|null + { + return $this->attributes['queryCache'] ?? null; + } + + /** + * Sets the cache driver implementation that is used for the query cache (SQL cache). + */ + public function setQueryCache(CacheItemPoolInterface $cache): void + { + $this->attributes['queryCache'] = $cache; + } + + public function getHydrationCache(): CacheItemPoolInterface|null + { + return $this->attributes['hydrationCache'] ?? null; + } + + public function setHydrationCache(CacheItemPoolInterface $cache): void + { + $this->attributes['hydrationCache'] = $cache; + } + + public function getMetadataCache(): CacheItemPoolInterface|null + { + return $this->attributes['metadataCache'] ?? null; + } + + public function setMetadataCache(CacheItemPoolInterface $cache): void + { + $this->attributes['metadataCache'] = $cache; + } + + /** + * Registers a custom DQL function that produces a string value. + * Such a function can then be used in any DQL statement in any place where string + * functions are allowed. + * + * DQL function names are case-insensitive. + * + * @param class-string|callable $className Class name or a callable that returns the function. + * @psalm-param class-string|callable(string):FunctionNode $className + */ + public function addCustomStringFunction(string $name, string|callable $className): void + { + $this->attributes['customStringFunctions'][strtolower($name)] = $className; + } + + /** + * Gets the implementation class name of a registered custom string DQL function. + * + * @psalm-return class-string|callable(string):FunctionNode|null + */ + public function getCustomStringFunction(string $name): string|callable|null + { + $name = strtolower($name); + + return $this->attributes['customStringFunctions'][$name] ?? null; + } + + /** + * Sets a map of custom DQL string functions. + * + * Keys must be function names and values the FQCN of the implementing class. + * The function names will be case-insensitive in DQL. + * + * Any previously added string functions are discarded. + * + * @psalm-param array|callable(string):FunctionNode> $functions The map of custom + * DQL string functions. + */ + public function setCustomStringFunctions(array $functions): void + { + foreach ($functions as $name => $className) { + $this->addCustomStringFunction($name, $className); + } + } + + /** + * Registers a custom DQL function that produces a numeric value. + * Such a function can then be used in any DQL statement in any place where numeric + * functions are allowed. + * + * DQL function names are case-insensitive. + * + * @param class-string|callable $className Class name or a callable that returns the function. + * @psalm-param class-string|callable(string):FunctionNode $className + */ + public function addCustomNumericFunction(string $name, string|callable $className): void + { + $this->attributes['customNumericFunctions'][strtolower($name)] = $className; + } + + /** + * Gets the implementation class name of a registered custom numeric DQL function. + * + * @psalm-return ?class-string|callable(string):FunctionNode + */ + public function getCustomNumericFunction(string $name): string|callable|null + { + $name = strtolower($name); + + return $this->attributes['customNumericFunctions'][$name] ?? null; + } + + /** + * Sets a map of custom DQL numeric functions. + * + * Keys must be function names and values the FQCN of the implementing class. + * The function names will be case-insensitive in DQL. + * + * Any previously added numeric functions are discarded. + * + * @psalm-param array $functions The map of custom + * DQL numeric functions. + */ + public function setCustomNumericFunctions(array $functions): void + { + foreach ($functions as $name => $className) { + $this->addCustomNumericFunction($name, $className); + } + } + + /** + * Registers a custom DQL function that produces a date/time value. + * Such a function can then be used in any DQL statement in any place where date/time + * functions are allowed. + * + * DQL function names are case-insensitive. + * + * @param string|callable $className Class name or a callable that returns the function. + * @psalm-param class-string|callable(string):FunctionNode $className + */ + public function addCustomDatetimeFunction(string $name, string|callable $className): void + { + $this->attributes['customDatetimeFunctions'][strtolower($name)] = $className; + } + + /** + * Gets the implementation class name of a registered custom date/time DQL function. + * + * @psalm-return class-string|callable|null + */ + public function getCustomDatetimeFunction(string $name): string|callable|null + { + $name = strtolower($name); + + return $this->attributes['customDatetimeFunctions'][$name] ?? null; + } + + /** + * Sets a map of custom DQL date/time functions. + * + * Keys must be function names and values the FQCN of the implementing class. + * The function names will be case-insensitive in DQL. + * + * Any previously added date/time functions are discarded. + * + * @param array $functions The map of custom DQL date/time functions. + * @psalm-param array|callable(string):FunctionNode> $functions + */ + public function setCustomDatetimeFunctions(array $functions): void + { + foreach ($functions as $name => $className) { + $this->addCustomDatetimeFunction($name, $className); + } + } + + /** + * Sets a TypedFieldMapper for php typed fields to DBAL types auto-completion. + */ + public function setTypedFieldMapper(TypedFieldMapper|null $typedFieldMapper): void + { + $this->attributes['typedFieldMapper'] = $typedFieldMapper; + } + + /** + * Gets a TypedFieldMapper for php typed fields to DBAL types auto-completion. + */ + public function getTypedFieldMapper(): TypedFieldMapper|null + { + return $this->attributes['typedFieldMapper'] ?? null; + } + + /** + * Sets the custom hydrator modes in one pass. + * + * @param array> $modes An array of ($modeName => $hydrator). + */ + public function setCustomHydrationModes(array $modes): void + { + $this->attributes['customHydrationModes'] = []; + + foreach ($modes as $modeName => $hydrator) { + $this->addCustomHydrationMode($modeName, $hydrator); + } + } + + /** + * Gets the hydrator class for the given hydration mode name. + * + * @psalm-return class-string|null + */ + public function getCustomHydrationMode(string $modeName): string|null + { + return $this->attributes['customHydrationModes'][$modeName] ?? null; + } + + /** + * Adds a custom hydration mode. + * + * @psalm-param class-string $hydrator + */ + public function addCustomHydrationMode(string $modeName, string $hydrator): void + { + $this->attributes['customHydrationModes'][$modeName] = $hydrator; + } + + /** + * Sets a class metadata factory. + * + * @psalm-param class-string $cmfName + */ + public function setClassMetadataFactoryName(string $cmfName): void + { + $this->attributes['classMetadataFactoryName'] = $cmfName; + } + + /** @psalm-return class-string */ + public function getClassMetadataFactoryName(): string + { + if (! isset($this->attributes['classMetadataFactoryName'])) { + $this->attributes['classMetadataFactoryName'] = ClassMetadataFactory::class; + } + + return $this->attributes['classMetadataFactoryName']; + } + + /** + * Adds a filter to the list of possible filters. + * + * @param string $className The class name of the filter. + * @psalm-param class-string $className + */ + public function addFilter(string $name, string $className): void + { + $this->attributes['filters'][$name] = $className; + } + + /** + * Gets the class name for a given filter name. + * + * @return string|null The class name of the filter, or null if it is not + * defined. + * @psalm-return class-string|null + */ + public function getFilterClassName(string $name): string|null + { + return $this->attributes['filters'][$name] ?? null; + } + + /** + * Sets default repository class. + * + * @psalm-param class-string $className + * + * @throws InvalidEntityRepository If $classname is not an ObjectRepository. + */ + public function setDefaultRepositoryClassName(string $className): void + { + if (! class_exists($className) || ! is_a($className, EntityRepository::class, true)) { + throw InvalidEntityRepository::fromClassName($className); + } + + $this->attributes['defaultRepositoryClassName'] = $className; + } + + /** + * Get default repository class. + * + * @psalm-return class-string + */ + public function getDefaultRepositoryClassName(): string + { + return $this->attributes['defaultRepositoryClassName'] ?? EntityRepository::class; + } + + /** + * Sets naming strategy. + */ + public function setNamingStrategy(NamingStrategy $namingStrategy): void + { + $this->attributes['namingStrategy'] = $namingStrategy; + } + + /** + * Gets naming strategy.. + */ + public function getNamingStrategy(): NamingStrategy + { + if (! isset($this->attributes['namingStrategy'])) { + $this->attributes['namingStrategy'] = new DefaultNamingStrategy(); + } + + return $this->attributes['namingStrategy']; + } + + /** + * Sets quote strategy. + */ + public function setQuoteStrategy(QuoteStrategy $quoteStrategy): void + { + $this->attributes['quoteStrategy'] = $quoteStrategy; + } + + /** + * Gets quote strategy. + */ + public function getQuoteStrategy(): QuoteStrategy + { + if (! isset($this->attributes['quoteStrategy'])) { + $this->attributes['quoteStrategy'] = new DefaultQuoteStrategy(); + } + + return $this->attributes['quoteStrategy']; + } + + /** + * Set the entity listener resolver. + */ + public function setEntityListenerResolver(EntityListenerResolver $resolver): void + { + $this->attributes['entityListenerResolver'] = $resolver; + } + + /** + * Get the entity listener resolver. + */ + public function getEntityListenerResolver(): EntityListenerResolver + { + if (! isset($this->attributes['entityListenerResolver'])) { + $this->attributes['entityListenerResolver'] = new DefaultEntityListenerResolver(); + } + + return $this->attributes['entityListenerResolver']; + } + + /** + * Set the entity repository factory. + */ + public function setRepositoryFactory(RepositoryFactory $repositoryFactory): void + { + $this->attributes['repositoryFactory'] = $repositoryFactory; + } + + /** + * Get the entity repository factory. + */ + public function getRepositoryFactory(): RepositoryFactory + { + return $this->attributes['repositoryFactory'] ?? new DefaultRepositoryFactory(); + } + + public function isSecondLevelCacheEnabled(): bool + { + return $this->attributes['isSecondLevelCacheEnabled'] ?? false; + } + + public function setSecondLevelCacheEnabled(bool $flag = true): void + { + $this->attributes['isSecondLevelCacheEnabled'] = $flag; + } + + public function setSecondLevelCacheConfiguration(CacheConfiguration $cacheConfig): void + { + $this->attributes['secondLevelCacheConfiguration'] = $cacheConfig; + } + + public function getSecondLevelCacheConfiguration(): CacheConfiguration|null + { + if (! isset($this->attributes['secondLevelCacheConfiguration']) && $this->isSecondLevelCacheEnabled()) { + $this->attributes['secondLevelCacheConfiguration'] = new CacheConfiguration(); + } + + return $this->attributes['secondLevelCacheConfiguration'] ?? null; + } + + /** + * Returns query hints, which will be applied to every query in application + * + * @psalm-return array + */ + public function getDefaultQueryHints(): array + { + return $this->attributes['defaultQueryHints'] ?? []; + } + + /** + * Sets array of query hints, which will be applied to every query in application + * + * @psalm-param array $defaultQueryHints + */ + public function setDefaultQueryHints(array $defaultQueryHints): void + { + $this->attributes['defaultQueryHints'] = $defaultQueryHints; + } + + /** + * Gets the value of a default query hint. If the hint name is not recognized, FALSE is returned. + * + * @return mixed The value of the hint or FALSE, if the hint name is not recognized. + */ + public function getDefaultQueryHint(string $name): mixed + { + return $this->attributes['defaultQueryHints'][$name] ?? false; + } + + /** + * Sets a default query hint. If the hint name is not recognized, it is silently ignored. + */ + public function setDefaultQueryHint(string $name, mixed $value): void + { + $this->attributes['defaultQueryHints'][$name] = $value; + } + + /** + * Gets a list of entity class names to be ignored by the SchemaTool + * + * @return list + */ + public function getSchemaIgnoreClasses(): array + { + return $this->attributes['schemaIgnoreClasses'] ?? []; + } + + /** + * Sets a list of entity class names to be ignored by the SchemaTool + * + * @param list $schemaIgnoreClasses List of entity class names + */ + public function setSchemaIgnoreClasses(array $schemaIgnoreClasses): void + { + $this->attributes['schemaIgnoreClasses'] = $schemaIgnoreClasses; + } + + /** + * To be deprecated in 3.1.0 + * + * @return true + */ + public function isLazyGhostObjectEnabled(): bool + { + return true; + } + + /** To be deprecated in 3.1.0 */ + public function setLazyGhostObjectEnabled(bool $flag): void + { + if (! $flag) { + throw new LogicException(<<<'EXCEPTION' + The lazy ghost object feature cannot be disabled anymore. + Please remove the call to setLazyGhostObjectEnabled(false). + EXCEPTION); + } + } + + /** To be deprecated in 3.1.0 */ + public function setRejectIdCollisionInIdentityMap(bool $flag): void + { + if (! $flag) { + throw new LogicException(<<<'EXCEPTION' + Rejecting ID collisions in the identity map cannot be disabled anymore. + Please remove the call to setRejectIdCollisionInIdentityMap(false). + EXCEPTION); + } + } + + /** + * To be deprecated in 3.1.0 + * + * @return true + */ + public function isRejectIdCollisionInIdentityMapEnabled(): bool + { + return true; + } + + public function setEagerFetchBatchSize(int $batchSize = 100): void + { + $this->attributes['fetchModeSubselectBatchSize'] = $batchSize; + } + + public function getEagerFetchBatchSize(): int + { + return $this->attributes['fetchModeSubselectBatchSize'] ?? 100; + } +} 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 @@ + + */ +abstract class EntityManagerDecorator extends ObjectManagerDecorator implements EntityManagerInterface +{ + public function __construct(EntityManagerInterface $wrapped) + { + $this->wrapped = $wrapped; + } + + public function getRepository(string $className): EntityRepository + { + return $this->wrapped->getRepository($className); + } + + public function getMetadataFactory(): ClassMetadataFactory + { + return $this->wrapped->getMetadataFactory(); + } + + public function getClassMetadata(string $className): ClassMetadata + { + return $this->wrapped->getClassMetadata($className); + } + + public function getConnection(): Connection + { + return $this->wrapped->getConnection(); + } + + public function getExpressionBuilder(): Expr + { + return $this->wrapped->getExpressionBuilder(); + } + + public function beginTransaction(): void + { + $this->wrapped->beginTransaction(); + } + + public function wrapInTransaction(callable $func): mixed + { + return $this->wrapped->wrapInTransaction($func); + } + + public function commit(): void + { + $this->wrapped->commit(); + } + + public function rollback(): void + { + $this->wrapped->rollback(); + } + + public function createQuery(string $dql = ''): Query + { + return $this->wrapped->createQuery($dql); + } + + public function createNativeQuery(string $sql, ResultSetMapping $rsm): NativeQuery + { + return $this->wrapped->createNativeQuery($sql, $rsm); + } + + public function createQueryBuilder(): QueryBuilder + { + return $this->wrapped->createQueryBuilder(); + } + + public function getReference(string $entityName, mixed $id): object|null + { + return $this->wrapped->getReference($entityName, $id); + } + + public function close(): void + { + $this->wrapped->close(); + } + + public function lock(object $entity, LockMode|int $lockMode, DateTimeInterface|int|null $lockVersion = null): void + { + $this->wrapped->lock($entity, $lockMode, $lockVersion); + } + + public function find(string $className, mixed $id, LockMode|int|null $lockMode = null, int|null $lockVersion = null): object|null + { + return $this->wrapped->find($className, $id, $lockMode, $lockVersion); + } + + public function refresh(object $object, LockMode|int|null $lockMode = null): void + { + $this->wrapped->refresh($object, $lockMode); + } + + public function getEventManager(): EventManager + { + return $this->wrapped->getEventManager(); + } + + public function getConfiguration(): Configuration + { + return $this->wrapped->getConfiguration(); + } + + public function isOpen(): bool + { + return $this->wrapped->isOpen(); + } + + public function getUnitOfWork(): UnitOfWork + { + return $this->wrapped->getUnitOfWork(); + } + + public function newHydrator(string|int $hydrationMode): AbstractHydrator + { + return $this->wrapped->newHydrator($hydrationMode); + } + + public function getProxyFactory(): ProxyFactory + { + return $this->wrapped->getProxyFactory(); + } + + public function getFilters(): FilterCollection + { + return $this->wrapped->getFilters(); + } + + public function isFiltersStateClean(): bool + { + return $this->wrapped->isFiltersStateClean(); + } + + public function hasFilters(): bool + { + return $this->wrapped->hasFilters(); + } + + public function getCache(): Cache|null + { + return $this->wrapped->getCache(); + } +} 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 @@ + 'pdo_sqlite', 'memory' => true], $config); + * $entityManager = new EntityManager($connection, $config); + * + * For more information see + * {@link http://docs.doctrine-project.org/projects/doctrine-orm/en/stable/reference/configuration.html} + * + * You should never attempt to inherit from the EntityManager: Inheritance + * is not a valid extension point for the EntityManager. Instead you + * should take a look at the {@see \Doctrine\ORM\Decorator\EntityManagerDecorator} + * and wrap your entity manager in a decorator. + * + * @final + */ +class EntityManager implements EntityManagerInterface +{ + /** + * The metadata factory, used to retrieve the ORM metadata of entity classes. + */ + private ClassMetadataFactory $metadataFactory; + + /** + * The UnitOfWork used to coordinate object-level transactions. + */ + private UnitOfWork $unitOfWork; + + /** + * The event manager that is the central point of the event system. + */ + private EventManager $eventManager; + + /** + * The proxy factory used to create dynamic proxies. + */ + private ProxyFactory $proxyFactory; + + /** + * The repository factory used to create dynamic repositories. + */ + private RepositoryFactory $repositoryFactory; + + /** + * The expression builder instance used to generate query expressions. + */ + private Expr|null $expressionBuilder = null; + + /** + * Whether the EntityManager is closed or not. + */ + private bool $closed = false; + + /** + * Collection of query filters. + */ + private FilterCollection|null $filterCollection = null; + + /** + * The second level cache regions API. + */ + private Cache|null $cache = null; + + /** + * Creates a new EntityManager that operates on the given database connection + * and uses the given Configuration and EventManager implementations. + * + * @param Connection $conn The database connection used by the EntityManager. + */ + public function __construct( + private Connection $conn, + private Configuration $config, + EventManager|null $eventManager = null, + ) { + if (! $config->getMetadataDriverImpl()) { + throw MissingMappingDriverImplementation::create(); + } + + $this->eventManager = $eventManager + ?? (method_exists($conn, 'getEventManager') + ? $conn->getEventManager() + : new EventManager() + ); + + $metadataFactoryClassName = $config->getClassMetadataFactoryName(); + + $this->metadataFactory = new $metadataFactoryClassName(); + $this->metadataFactory->setEntityManager($this); + + $this->configureMetadataCache(); + + $this->repositoryFactory = $config->getRepositoryFactory(); + $this->unitOfWork = new UnitOfWork($this); + $this->proxyFactory = new ProxyFactory( + $this, + $config->getProxyDir(), + $config->getProxyNamespace(), + $config->getAutoGenerateProxyClasses(), + ); + + if ($config->isSecondLevelCacheEnabled()) { + $cacheConfig = $config->getSecondLevelCacheConfiguration(); + $cacheFactory = $cacheConfig->getCacheFactory(); + $this->cache = $cacheFactory->createCache($this); + } + } + + public function getConnection(): Connection + { + return $this->conn; + } + + public function getMetadataFactory(): ClassMetadataFactory + { + return $this->metadataFactory; + } + + public function getExpressionBuilder(): Expr + { + return $this->expressionBuilder ??= new Expr(); + } + + public function beginTransaction(): void + { + $this->conn->beginTransaction(); + } + + public function getCache(): Cache|null + { + return $this->cache; + } + + public function wrapInTransaction(callable $func): mixed + { + $this->conn->beginTransaction(); + + try { + $return = $func($this); + + $this->flush(); + $this->conn->commit(); + + return $return; + } catch (Throwable $e) { + $this->close(); + $this->conn->rollBack(); + + throw $e; + } + } + + public function commit(): void + { + $this->conn->commit(); + } + + public function rollback(): void + { + $this->conn->rollBack(); + } + + /** + * Returns the ORM metadata descriptor for a class. + * + * Internal note: Performance-sensitive method. + * + * {@inheritDoc} + */ + public function getClassMetadata(string $className): Mapping\ClassMetadata + { + return $this->metadataFactory->getMetadataFor($className); + } + + public function createQuery(string $dql = ''): Query + { + $query = new Query($this); + + if (! empty($dql)) { + $query->setDQL($dql); + } + + return $query; + } + + public function createNativeQuery(string $sql, ResultSetMapping $rsm): NativeQuery + { + $query = new NativeQuery($this); + + $query->setSQL($sql); + $query->setResultSetMapping($rsm); + + return $query; + } + + public function createQueryBuilder(): QueryBuilder + { + return new QueryBuilder($this); + } + + /** + * Flushes all changes to objects that have been queued up to now to the database. + * This effectively synchronizes the in-memory state of managed objects with the + * database. + * + * If an entity is explicitly passed to this method only this entity and + * the cascade-persist semantics + scheduled inserts/removals are synchronized. + * + * @throws OptimisticLockException If a version check on an entity that + * makes use of optimistic locking fails. + * @throws ORMException + */ + public function flush(): void + { + $this->errorIfClosed(); + $this->unitOfWork->commit(); + } + + /** + * {@inheritDoc} + */ + public function find($className, mixed $id, LockMode|int|null $lockMode = null, int|null $lockVersion = null): object|null + { + $class = $this->metadataFactory->getMetadataFor(ltrim($className, '\\')); + + if ($lockMode !== null) { + $this->checkLockRequirements($lockMode, $class); + } + + if (! is_array($id)) { + if ($class->isIdentifierComposite) { + throw ORMInvalidArgumentException::invalidCompositeIdentifier(); + } + + $id = [$class->identifier[0] => $id]; + } + + foreach ($id as $i => $value) { + if (is_object($value)) { + $className = DefaultProxyClassNameResolver::getClass($value); + if ($this->metadataFactory->hasMetadataFor($className)) { + $id[$i] = $this->unitOfWork->getSingleIdentifierValue($value); + + if ($id[$i] === null) { + throw ORMInvalidArgumentException::invalidIdentifierBindingEntity($className); + } + } + } + } + + $sortedId = []; + + foreach ($class->identifier as $identifier) { + if (! isset($id[$identifier])) { + throw MissingIdentifierField::fromFieldAndClass($identifier, $class->name); + } + + if ($id[$identifier] instanceof BackedEnum) { + $sortedId[$identifier] = $id[$identifier]->value; + } else { + $sortedId[$identifier] = $id[$identifier]; + } + + unset($id[$identifier]); + } + + if ($id) { + throw UnrecognizedIdentifierFields::fromClassAndFieldNames($class->name, array_keys($id)); + } + + $unitOfWork = $this->getUnitOfWork(); + + $entity = $unitOfWork->tryGetById($sortedId, $class->rootEntityName); + + // Check identity map first + if ($entity !== false) { + if (! ($entity instanceof $class->name)) { + return null; + } + + switch (true) { + case $lockMode === LockMode::OPTIMISTIC: + $this->lock($entity, $lockMode, $lockVersion); + break; + + case $lockMode === LockMode::NONE: + case $lockMode === LockMode::PESSIMISTIC_READ: + case $lockMode === LockMode::PESSIMISTIC_WRITE: + $persister = $unitOfWork->getEntityPersister($class->name); + $persister->refresh($sortedId, $entity, $lockMode); + break; + } + + return $entity; // Hit! + } + + $persister = $unitOfWork->getEntityPersister($class->name); + + switch (true) { + case $lockMode === LockMode::OPTIMISTIC: + $entity = $persister->load($sortedId); + + if ($entity !== null) { + $unitOfWork->lock($entity, $lockMode, $lockVersion); + } + + return $entity; + + case $lockMode === LockMode::PESSIMISTIC_READ: + case $lockMode === LockMode::PESSIMISTIC_WRITE: + return $persister->load($sortedId, null, null, [], $lockMode); + + default: + return $persister->loadById($sortedId); + } + } + + public function getReference(string $entityName, mixed $id): object|null + { + $class = $this->metadataFactory->getMetadataFor(ltrim($entityName, '\\')); + + if (! is_array($id)) { + $id = [$class->identifier[0] => $id]; + } + + $sortedId = []; + + foreach ($class->identifier as $identifier) { + if (! isset($id[$identifier])) { + throw MissingIdentifierField::fromFieldAndClass($identifier, $class->name); + } + + $sortedId[$identifier] = $id[$identifier]; + unset($id[$identifier]); + } + + if ($id) { + throw UnrecognizedIdentifierFields::fromClassAndFieldNames($class->name, array_keys($id)); + } + + $entity = $this->unitOfWork->tryGetById($sortedId, $class->rootEntityName); + + // Check identity map first, if its already in there just return it. + if ($entity !== false) { + return $entity instanceof $class->name ? $entity : null; + } + + if ($class->subClasses) { + return $this->find($entityName, $sortedId); + } + + $entity = $this->proxyFactory->getProxy($class->name, $sortedId); + + $this->unitOfWork->registerManaged($entity, $sortedId, []); + + return $entity; + } + + /** + * Clears the EntityManager. All entities that are currently managed + * by this EntityManager become detached. + */ + public function clear(): void + { + $this->unitOfWork->clear(); + } + + public function close(): void + { + $this->clear(); + + $this->closed = true; + } + + /** + * Tells the EntityManager to make an instance managed and persistent. + * + * The entity will be entered into the database at or before transaction + * commit or as a result of the flush operation. + * + * NOTE: The persist operation always considers entities that are not yet known to + * this EntityManager as NEW. Do not pass detached entities to the persist operation. + * + * @throws ORMInvalidArgumentException + * @throws ORMException + */ + public function persist(object $object): void + { + $this->errorIfClosed(); + + $this->unitOfWork->persist($object); + } + + /** + * Removes an entity instance. + * + * A removed entity will be removed from the database at or before transaction commit + * or as a result of the flush operation. + * + * @throws ORMInvalidArgumentException + * @throws ORMException + */ + public function remove(object $object): void + { + $this->errorIfClosed(); + + $this->unitOfWork->remove($object); + } + + public function refresh(object $object, LockMode|int|null $lockMode = null): void + { + $this->errorIfClosed(); + + $this->unitOfWork->refresh($object, $lockMode); + } + + /** + * Detaches an entity from the EntityManager, causing a managed entity to + * become detached. Unflushed changes made to the entity if any + * (including removal of the entity), will not be synchronized to the database. + * Entities which previously referenced the detached entity will continue to + * reference it. + * + * @throws ORMInvalidArgumentException + */ + public function detach(object $object): void + { + $this->unitOfWork->detach($object); + } + + public function lock(object $entity, LockMode|int $lockMode, DateTimeInterface|int|null $lockVersion = null): void + { + $this->unitOfWork->lock($entity, $lockMode, $lockVersion); + } + + /** + * Gets the repository for an entity class. + * + * @psalm-param class-string $className + * + * @psalm-return EntityRepository + * + * @template T of object + */ + public function getRepository(string $className): EntityRepository + { + return $this->repositoryFactory->getRepository($this, $className); + } + + /** + * Determines whether an entity instance is managed in this EntityManager. + * + * @return bool TRUE if this EntityManager currently manages the given entity, FALSE otherwise. + */ + public function contains(object $object): bool + { + return $this->unitOfWork->isScheduledForInsert($object) + || $this->unitOfWork->isInIdentityMap($object) + && ! $this->unitOfWork->isScheduledForDelete($object); + } + + public function getEventManager(): EventManager + { + return $this->eventManager; + } + + public function getConfiguration(): Configuration + { + return $this->config; + } + + /** + * Throws an exception if the EntityManager is closed or currently not active. + * + * @throws EntityManagerClosed If the EntityManager is closed. + */ + private function errorIfClosed(): void + { + if ($this->closed) { + throw EntityManagerClosed::create(); + } + } + + public function isOpen(): bool + { + return ! $this->closed; + } + + public function getUnitOfWork(): UnitOfWork + { + return $this->unitOfWork; + } + + public function newHydrator(string|int $hydrationMode): AbstractHydrator + { + return match ($hydrationMode) { + Query::HYDRATE_OBJECT => new Internal\Hydration\ObjectHydrator($this), + Query::HYDRATE_ARRAY => new Internal\Hydration\ArrayHydrator($this), + Query::HYDRATE_SCALAR => new Internal\Hydration\ScalarHydrator($this), + Query::HYDRATE_SINGLE_SCALAR => new Internal\Hydration\SingleScalarHydrator($this), + Query::HYDRATE_SIMPLEOBJECT => new Internal\Hydration\SimpleObjectHydrator($this), + Query::HYDRATE_SCALAR_COLUMN => new Internal\Hydration\ScalarColumnHydrator($this), + default => $this->createCustomHydrator((string) $hydrationMode), + }; + } + + public function getProxyFactory(): ProxyFactory + { + return $this->proxyFactory; + } + + public function initializeObject(object $obj): void + { + $this->unitOfWork->initializeObject($obj); + } + + /** + * {@inheritDoc} + */ + public function isUninitializedObject($obj): bool + { + return $this->unitOfWork->isUninitializedObject($obj); + } + + public function getFilters(): FilterCollection + { + return $this->filterCollection ??= new FilterCollection($this); + } + + public function isFiltersStateClean(): bool + { + return $this->filterCollection === null || $this->filterCollection->isClean(); + } + + public function hasFilters(): bool + { + return $this->filterCollection !== null; + } + + /** + * @psalm-param LockMode::* $lockMode + * + * @throws OptimisticLockException + * @throws TransactionRequiredException + */ + private function checkLockRequirements(LockMode|int $lockMode, ClassMetadata $class): void + { + switch ($lockMode) { + case LockMode::OPTIMISTIC: + if (! $class->isVersioned) { + throw OptimisticLockException::notVersioned($class->name); + } + + break; + case LockMode::PESSIMISTIC_READ: + case LockMode::PESSIMISTIC_WRITE: + if (! $this->getConnection()->isTransactionActive()) { + throw TransactionRequiredException::transactionRequired(); + } + } + } + + private function configureMetadataCache(): void + { + $metadataCache = $this->config->getMetadataCache(); + if (! $metadataCache) { + return; + } + + $this->metadataFactory->setCache($metadataCache); + } + + private function createCustomHydrator(string $hydrationMode): AbstractHydrator + { + $class = $this->config->getCustomHydrationMode($hydrationMode); + + if ($class !== null) { + return new $class($this); + } + + throw InvalidHydrationMode::fromMode($hydrationMode); + } +} 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 @@ + $className + * + * @psalm-return EntityRepository + * + * @template T of object + */ + public function getRepository(string $className): EntityRepository; + + /** + * Returns the cache API for managing the second level cache regions or NULL if the cache is not enabled. + */ + public function getCache(): Cache|null; + + /** + * Gets the database connection object used by the EntityManager. + */ + public function getConnection(): Connection; + + public function getMetadataFactory(): ClassMetadataFactory; + + /** + * Gets an ExpressionBuilder used for object-oriented construction of query expressions. + * + * Example: + * + * + * $qb = $em->createQueryBuilder(); + * $expr = $em->getExpressionBuilder(); + * $qb->select('u')->from('User', 'u') + * ->where($expr->orX($expr->eq('u.id', 1), $expr->eq('u.id', 2))); + * + */ + public function getExpressionBuilder(): Expr; + + /** + * Starts a transaction on the underlying database connection. + */ + public function beginTransaction(): void; + + /** + * Executes a function in a transaction. + * + * The function gets passed this EntityManager instance as an (optional) parameter. + * + * {@link flush} is invoked prior to transaction commit. + * + * If an exception occurs during execution of the function or flushing or transaction commit, + * the transaction is rolled back, the EntityManager closed and the exception re-thrown. + * + * @psalm-param callable(self): T $func The function to execute transactionally. + * + * @return mixed The value returned from the closure. + * @psalm-return T + * + * @template T + */ + public function wrapInTransaction(callable $func): mixed; + + /** + * Commits a transaction on the underlying database connection. + */ + public function commit(): void; + + /** + * Performs a rollback on the underlying database connection. + */ + public function rollback(): void; + + /** + * Creates a new Query object. + * + * @param string $dql The DQL string. + */ + public function createQuery(string $dql = ''): Query; + + /** + * Creates a native SQL query. + */ + public function createNativeQuery(string $sql, ResultSetMapping $rsm): NativeQuery; + + /** + * Create a QueryBuilder instance + */ + public function createQueryBuilder(): QueryBuilder; + + /** + * Finds an Entity by its identifier. + * + * @param string $className The class name of the entity to find. + * @param mixed $id The identity of the entity to find. + * @param LockMode|int|null $lockMode One of the \Doctrine\DBAL\LockMode::* constants + * or NULL if no specific lock mode should be used + * during the search. + * @param int|null $lockVersion The version of the entity to find when using + * optimistic locking. + * @psalm-param class-string $className + * @psalm-param LockMode::*|null $lockMode + * + * @return object|null The entity instance or NULL if the entity can not be found. + * @psalm-return T|null + * + * @throws OptimisticLockException + * @throws ORMInvalidArgumentException + * @throws TransactionRequiredException + * @throws ORMException + * + * @template T of object + */ + public function find(string $className, mixed $id, LockMode|int|null $lockMode = null, int|null $lockVersion = null): object|null; + + /** + * Refreshes the persistent state of an object from the database, + * overriding any local changes that have not yet been persisted. + * + * @param LockMode|int|null $lockMode One of the \Doctrine\DBAL\LockMode::* constants + * or NULL if no specific lock mode should be used + * during the search. + * @psalm-param LockMode::*|null $lockMode + * + * @throws ORMInvalidArgumentException + * @throws ORMException + * @throws TransactionRequiredException + */ + public function refresh(object $object, LockMode|int|null $lockMode = null): void; + + /** + * Gets a reference to the entity identified by the given type and identifier + * without actually loading it, if the entity is not yet loaded. + * + * @param string $entityName The name of the entity type. + * @param mixed $id The entity identifier. + * @psalm-param class-string $entityName + * + * @psalm-return T|null + * + * @throws ORMException + * + * @template T of object + */ + public function getReference(string $entityName, mixed $id): object|null; + + /** + * Closes the EntityManager. All entities that are currently managed + * by this EntityManager become detached. The EntityManager may no longer + * be used after it is closed. + */ + public function close(): void; + + /** + * Acquire a lock on the given entity. + * + * @psalm-param LockMode::* $lockMode + * + * @throws OptimisticLockException + * @throws PessimisticLockException + */ + public function lock(object $entity, LockMode|int $lockMode, DateTimeInterface|int|null $lockVersion = null): void; + + /** + * Gets the EventManager used by the EntityManager. + */ + public function getEventManager(): EventManager; + + /** + * Gets the Configuration used by the EntityManager. + */ + public function getConfiguration(): Configuration; + + /** + * Check if the Entity manager is open or closed. + */ + public function isOpen(): bool; + + /** + * Gets the UnitOfWork used by the EntityManager to coordinate operations. + */ + public function getUnitOfWork(): UnitOfWork; + + /** + * Create a new instance for the given hydration mode. + * + * @psalm-param string|AbstractQuery::HYDRATE_* $hydrationMode + * + * @throws ORMException + */ + public function newHydrator(string|int $hydrationMode): AbstractHydrator; + + /** + * Gets the proxy factory used by the EntityManager to create entity proxies. + */ + public function getProxyFactory(): ProxyFactory; + + /** + * Gets the enabled filters. + */ + public function getFilters(): FilterCollection; + + /** + * Checks whether the state of the filter collection is clean. + */ + public function isFiltersStateClean(): bool; + + /** + * Checks whether the Entity Manager has filters. + */ + public function hasFilters(): bool; + + /** + * {@inheritDoc} + * + * @psalm-param string|class-string $className + * + * @psalm-return ($className is class-string ? Mapping\ClassMetadata : Mapping\ClassMetadata) + * + * @psalm-template T of object + */ + public function getClassMetadata(string $className): Mapping\ClassMetadata; +} 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 @@ + $value) { + $ids[] = $key . '(' . $value . ')'; + } + + return new self( + 'Entity of type \'' . $className . '\'' . ($ids ? ' for IDs ' . implode(', ', $ids) : '') . ' was not found', + ); + } + + /** + * Instance for which no identifier can be found + */ + public static function noIdentifierFound(string $className): self + { + return new self(sprintf( + 'Unable to find "%s" entity identifier associated with the UnitOfWork', + $className, + )); + } +} 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 @@ + + * @template-implements ObjectRepository + */ +class EntityRepository implements ObjectRepository, Selectable +{ + /** @psalm-var class-string */ + private readonly string $entityName; + private static Inflector|null $inflector = null; + + /** @psalm-param ClassMetadata $class */ + public function __construct( + private readonly EntityManagerInterface $em, + private readonly ClassMetadata $class, + ) { + $this->entityName = $class->name; + } + + /** + * Creates a new QueryBuilder instance that is prepopulated for this entity name. + */ + public function createQueryBuilder(string $alias, string|null $indexBy = null): QueryBuilder + { + return $this->em->createQueryBuilder() + ->select($alias) + ->from($this->entityName, $alias, $indexBy); + } + + /** + * Creates a new result set mapping builder for this entity. + * + * The column naming strategy is "INCREMENT". + */ + public function createResultSetMappingBuilder(string $alias): ResultSetMappingBuilder + { + $rsm = new ResultSetMappingBuilder($this->em, ResultSetMappingBuilder::COLUMN_RENAMING_INCREMENT); + $rsm->addRootEntityFromClassMetadata($this->entityName, $alias); + + return $rsm; + } + + /** + * Finds an entity by its primary key / identifier. + * + * @param LockMode|int|null $lockMode One of the \Doctrine\DBAL\LockMode::* constants + * or NULL if no specific lock mode should be used + * during the search. + * @psalm-param LockMode::*|null $lockMode + * + * @return object|null The entity instance or NULL if the entity can not be found. + * @psalm-return ?T + */ + public function find(mixed $id, LockMode|int|null $lockMode = null, int|null $lockVersion = null): object|null + { + return $this->em->find($this->entityName, $id, $lockMode, $lockVersion); + } + + /** + * Finds all entities in the repository. + * + * @psalm-return list The entities. + */ + public function findAll(): array + { + return $this->findBy([]); + } + + /** + * Finds entities by a set of criteria. + * + * {@inheritDoc} + * + * @psalm-return list + */ + public function findBy(array $criteria, array|null $orderBy = null, int|null $limit = null, int|null $offset = null): array + { + $persister = $this->em->getUnitOfWork()->getEntityPersister($this->entityName); + + return $persister->loadAll($criteria, $orderBy, $limit, $offset); + } + + /** + * Finds a single entity by a set of criteria. + * + * @psalm-param array $criteria + * @psalm-param array|null $orderBy + * + * @psalm-return T|null + */ + public function findOneBy(array $criteria, array|null $orderBy = null): object|null + { + $persister = $this->em->getUnitOfWork()->getEntityPersister($this->entityName); + + return $persister->load($criteria, null, null, [], null, 1, $orderBy); + } + + /** + * Counts entities by a set of criteria. + * + * @psalm-param array $criteria + * + * @return int The cardinality of the objects that match the given criteria. + * + * @todo Add this method to `ObjectRepository` interface in the next major release + */ + public function count(array $criteria = []): int + { + return $this->em->getUnitOfWork()->getEntityPersister($this->entityName)->count($criteria); + } + + /** + * Adds support for magic method calls. + * + * @param mixed[] $arguments + * @psalm-param list $arguments + * + * @throws BadMethodCallException If the method called is invalid. + */ + public function __call(string $method, array $arguments): mixed + { + if (str_starts_with($method, 'findBy')) { + return $this->resolveMagicCall('findBy', substr($method, 6), $arguments); + } + + if (str_starts_with($method, 'findOneBy')) { + return $this->resolveMagicCall('findOneBy', substr($method, 9), $arguments); + } + + if (str_starts_with($method, 'countBy')) { + return $this->resolveMagicCall('count', substr($method, 7), $arguments); + } + + throw new BadMethodCallException(sprintf( + 'Undefined method "%s". The method name must start with ' . + 'either findBy, findOneBy or countBy!', + $method, + )); + } + + /** @psalm-return class-string */ + protected function getEntityName(): string + { + return $this->entityName; + } + + public function getClassName(): string + { + return $this->getEntityName(); + } + + protected function getEntityManager(): EntityManagerInterface + { + return $this->em; + } + + /** @psalm-return ClassMetadata */ + protected function getClassMetadata(): ClassMetadata + { + return $this->class; + } + + /** + * Select all elements from a selectable that match the expression and + * return a new collection containing these elements. + * + * @psalm-return AbstractLazyCollection&Selectable + */ + public function matching(Criteria $criteria): AbstractLazyCollection&Selectable + { + $persister = $this->em->getUnitOfWork()->getEntityPersister($this->entityName); + + return new LazyCriteriaCollection($persister, $criteria); + } + + /** + * Resolves a magic method call to the proper existent method at `EntityRepository`. + * + * @param string $method The method to call + * @param string $by The property name used as condition + * @psalm-param list $arguments The arguments to pass at method call + * + * @throws InvalidMagicMethodCall If the method called is invalid or the + * requested field/association does not exist. + */ + private function resolveMagicCall(string $method, string $by, array $arguments): mixed + { + if (! $arguments) { + throw InvalidMagicMethodCall::onMissingParameter($method . $by); + } + + self::$inflector ??= InflectorFactory::create()->build(); + + $fieldName = lcfirst(self::$inflector->classify($by)); + + if (! ($this->class->hasField($fieldName) || $this->class->hasAssociation($fieldName))) { + throw InvalidMagicMethodCall::becauseFieldNotFoundIn( + $this->entityName, + $fieldName, + $method . $by, + ); + } + + return $this->$method([$fieldName => $arguments[0]], ...array_slice($arguments, 1)); + } +} 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 @@ +eventManager = $em->getEventManager(); + $this->resolver = $em->getConfiguration()->getEntityListenerResolver(); + } + + /** + * Get the subscribed event systems + * + * @param ClassMetadata $metadata The entity metadata. + * @param string $eventName The entity lifecycle event. + * + * @psalm-return int-mask-of Bitmask of subscribed event systems. + */ + public function getSubscribedSystems(ClassMetadata $metadata, string $eventName): int + { + $invoke = self::INVOKE_NONE; + + if (isset($metadata->lifecycleCallbacks[$eventName])) { + $invoke |= self::INVOKE_CALLBACKS; + } + + if (isset($metadata->entityListeners[$eventName])) { + $invoke |= self::INVOKE_LISTENERS; + } + + if ($this->eventManager->hasListeners($eventName)) { + $invoke |= self::INVOKE_MANAGER; + } + + return $invoke; + } + + /** + * Dispatches the lifecycle event of the given entity. + * + * @param ClassMetadata $metadata The entity metadata. + * @param string $eventName The entity lifecycle event. + * @param object $entity The Entity on which the event occurred. + * @param EventArgs $event The Event args. + * @psalm-param int-mask-of $invoke Bitmask to invoke listeners. + */ + public function invoke( + ClassMetadata $metadata, + string $eventName, + object $entity, + EventArgs $event, + int $invoke, + ): void { + if ($invoke & self::INVOKE_CALLBACKS) { + foreach ($metadata->lifecycleCallbacks[$eventName] as $callback) { + $entity->$callback($event); + } + } + + if ($invoke & self::INVOKE_LISTENERS) { + foreach ($metadata->entityListeners[$eventName] as $listener) { + $class = $listener['class']; + $method = $listener['method']; + $instance = $this->resolver->resolve($class); + + $instance->$method($entity, $event); + } + } + + if ($invoke & self::INVOKE_MANAGER) { + $this->eventManager->dispatchEvent($eventName, $event); + } + } +} 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 @@ +, EntityManagerInterface> + */ +class LoadClassMetadataEventArgs extends BaseLoadClassMetadataEventArgs +{ + /** + * Retrieve associated EntityManager. + */ + public function getEntityManager(): EntityManagerInterface + { + return $this->getObjectManager(); + } +} 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 @@ + + */ +class OnClassMetadataNotFoundEventArgs extends ManagerEventArgs +{ + private ClassMetadata|null $foundMetadata = null; + + /** @param EntityManagerInterface $objectManager */ + public function __construct( + private readonly string $className, + ObjectManager $objectManager, + ) { + parent::__construct($objectManager); + } + + public function setFoundMetadata(ClassMetadata|null $classMetadata): void + { + $this->foundMetadata = $classMetadata; + } + + public function getFoundMetadata(): ClassMetadata|null + { + return $this->foundMetadata; + } + + /** + * Retrieve class name for which a failed metadata fetch attempt was executed + */ + public function getClassName(): string + { + return $this->className; + } +} 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 @@ + + */ +class OnClearEventArgs extends BaseOnClearEventArgs +{ +} 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 @@ + + */ +class OnFlushEventArgs extends ManagerEventArgs +{ +} 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 @@ + + */ +class PostFlushEventArgs extends ManagerEventArgs +{ +} 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 @@ + */ +final class PostLoadEventArgs extends LifecycleEventArgs +{ +} 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 @@ + */ +final class PostPersistEventArgs extends LifecycleEventArgs +{ +} 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 @@ + */ +final class PostRemoveEventArgs extends LifecycleEventArgs +{ +} 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 @@ + */ +final class PostUpdateEventArgs extends LifecycleEventArgs +{ +} 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 @@ + + */ +class PreFlushEventArgs extends ManagerEventArgs +{ +} 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 @@ + */ +final class PrePersistEventArgs extends LifecycleEventArgs +{ +} 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 @@ + */ +final class PreRemoveEventArgs extends LifecycleEventArgs +{ +} 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 @@ + + */ +class PreUpdateEventArgs extends LifecycleEventArgs +{ + /** @var array */ + private array $entityChangeSet; + + /** + * @param mixed[][] $changeSet + * @psalm-param array $changeSet + */ + public function __construct(object $entity, EntityManagerInterface $em, array &$changeSet) + { + parent::__construct($entity, $em); + + $this->entityChangeSet = &$changeSet; + } + + /** + * Retrieves entity changeset. + * + * @return mixed[][] + * @psalm-return array + */ + public function getEntityChangeSet(): array + { + return $this->entityChangeSet; + } + + /** + * Checks if field has a changeset. + */ + public function hasChangedField(string $field): bool + { + return isset($this->entityChangeSet[$field]); + } + + /** + * Gets the old value of the changeset of the changed field. + */ + public function getOldValue(string $field): mixed + { + $this->assertValidField($field); + + return $this->entityChangeSet[$field][0]; + } + + /** + * Gets the new value of the changeset of the changed field. + */ + public function getNewValue(string $field): mixed + { + $this->assertValidField($field); + + return $this->entityChangeSet[$field][1]; + } + + /** + * Sets the new value of this field. + */ + public function setNewValue(string $field, mixed $value): void + { + $this->assertValidField($field); + + $this->entityChangeSet[$field][1] = $value; + } + + /** + * Asserts the field exists in changeset. + * + * @throws InvalidArgumentException + */ + private function assertValidField(string $field): void + { + if (! isset($this->entityChangeSet[$field])) { + throw new InvalidArgumentException(sprintf( + 'Field "%s" is not a valid field of the entity "%s" in PreUpdateEventArgs.', + $field, + get_debug_type($this->getObject()), + )); + } + } +} 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 @@ +getClassMetadata($entity::class); + $idFields = $class->getIdentifierFieldNames(); + $identifier = []; + + foreach ($idFields as $idField) { + $value = $class->getFieldValue($entity, $idField); + + if (! isset($value)) { + throw EntityMissingAssignedId::forField($entity, $idField); + } + + if (isset($class->associationMappings[$idField])) { + // NOTE: Single Columns as associated identifiers only allowed - this constraint it is enforced. + $value = $em->getUnitOfWork()->getSingleIdentifierValue($value); + } + + $identifier[$idField] = $value; + } + + return $identifier; + } +} 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 @@ +getConnection()->lastInsertId(); + } + + public function isPostInsertGenerator(): bool + { + return true; + } +} 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 @@ +getConnection()->lastInsertId(); + } + + public function isPostInsertGenerator(): bool + { + return true; + } +} 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 @@ +maxValue === null || $this->nextValue === $this->maxValue) { + // Allocate new values + $connection = $em->getConnection(); + $sql = $connection->getDatabasePlatform()->getSequenceNextValSQL($this->sequenceName); + + if ($connection instanceof PrimaryReadReplicaConnection) { + $connection->ensureConnectedToPrimary(); + } + + $this->nextValue = (int) $connection->fetchOne($sql); + $this->maxValue = $this->nextValue + $this->allocationSize; + } + + return $this->nextValue++; + } + + /** + * Gets the maximum value of the currently allocated bag of values. + */ + public function getCurrentMaxValue(): int|null + { + return $this->maxValue; + } + + /** + * Gets the next value that will be returned by generate(). + */ + public function getNextValue(): int + { + return $this->nextValue; + } + + /** @deprecated without replacement. */ + final public function serialize(): string + { + Deprecation::trigger( + 'doctrine/orm', + 'https://github.com/doctrine/orm/pull/11468', + '%s() is deprecated, use __serialize() instead. %s won\'t implement the Serializable interface anymore in ORM 4.', + __METHOD__, + self::class, + ); + + return serialize($this->__serialize()); + } + + /** @return array */ + public function __serialize(): array + { + return [ + 'allocationSize' => $this->allocationSize, + 'sequenceName' => $this->sequenceName, + ]; + } + + /** @deprecated without replacement. */ + final public function unserialize(string $serialized): void + { + Deprecation::trigger( + 'doctrine/orm', + 'https://github.com/doctrine/orm/pull/11468', + '%s() is deprecated, use __unserialize() instead. %s won\'t implement the Serializable interface anymore in ORM 4.', + __METHOD__, + self::class, + ); + + $this->__unserialize(unserialize($serialized)); + } + + /** @param array $data */ + public function __unserialize(array $data): void + { + $this->sequenceName = $data['sequenceName']; + $this->allocationSize = $data['allocationSize']; + } +} 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 @@ +> + */ + protected array $metadataCache = []; + + /** + * The cache used during row-by-row hydration. + * + * @var array + */ + protected array $cache = []; + + /** + * The statement that provides the data to hydrate. + */ + protected Result|null $stmt = null; + + /** + * The query hints. + * + * @var array + */ + protected array $hints = []; + + /** + * Initializes a new instance of a class derived from AbstractHydrator. + */ + public function __construct(protected EntityManagerInterface $em) + { + $this->platform = $em->getConnection()->getDatabasePlatform(); + $this->uow = $em->getUnitOfWork(); + } + + /** + * Initiates a row-by-row hydration. + * + * @psalm-param array $hints + * + * @return Generator + * + * @final + */ + final public function toIterable(Result $stmt, ResultSetMapping $resultSetMapping, array $hints = []): Generator + { + $this->stmt = $stmt; + $this->rsm = $resultSetMapping; + $this->hints = $hints; + + $evm = $this->em->getEventManager(); + + $evm->addEventListener([Events::onClear], $this); + + $this->prepare(); + + try { + while (true) { + $row = $this->statement()->fetchAssociative(); + + if ($row === false) { + break; + } + + $result = []; + + $this->hydrateRowData($row, $result); + + $this->cleanupAfterRowIteration(); + if (count($result) === 1) { + if (count($resultSetMapping->indexByMap) === 0) { + yield end($result); + } else { + yield from $result; + } + } else { + yield $result; + } + } + } finally { + $this->cleanup(); + } + } + + final protected function statement(): Result + { + if ($this->stmt === null) { + throw new LogicException('Uninitialized _stmt property'); + } + + return $this->stmt; + } + + final protected function resultSetMapping(): ResultSetMapping + { + if ($this->rsm === null) { + throw new LogicException('Uninitialized _rsm property'); + } + + return $this->rsm; + } + + /** + * Hydrates all rows returned by the passed statement instance at once. + * + * @psalm-param array $hints + */ + public function hydrateAll(Result $stmt, ResultSetMapping $resultSetMapping, array $hints = []): mixed + { + $this->stmt = $stmt; + $this->rsm = $resultSetMapping; + $this->hints = $hints; + + $this->em->getEventManager()->addEventListener([Events::onClear], $this); + $this->prepare(); + + try { + $result = $this->hydrateAllData(); + } finally { + $this->cleanup(); + } + + return $result; + } + + /** + * When executed in a hydrate() loop we have to clear internal state to + * decrease memory consumption. + */ + public function onClear(mixed $eventArgs): void + { + } + + /** + * Executes one-time preparation tasks, once each time hydration is started + * through {@link hydrateAll} or {@link toIterable()}. + */ + protected function prepare(): void + { + } + + /** + * Executes one-time cleanup tasks at the end of a hydration that was initiated + * through {@link hydrateAll} or {@link toIterable()}. + */ + protected function cleanup(): void + { + $this->statement()->free(); + + $this->stmt = null; + $this->rsm = null; + $this->cache = []; + $this->metadataCache = []; + + $this + ->em + ->getEventManager() + ->removeEventListener([Events::onClear], $this); + } + + protected function cleanupAfterRowIteration(): void + { + } + + /** + * Hydrates a single row from the current statement instance. + * + * Template method. + * + * @param mixed[] $row The row data. + * @param mixed[] $result The result to fill. + * + * @throws HydrationException + */ + protected function hydrateRowData(array $row, array &$result): void + { + throw new HydrationException('hydrateRowData() not implemented by this hydrator.'); + } + + /** + * Hydrates all rows from the current statement instance at once. + */ + abstract protected function hydrateAllData(): mixed; + + /** + * Processes a row of the result set. + * + * Used for identity-based hydration (HYDRATE_OBJECT and HYDRATE_ARRAY). + * Puts the elements of a result row into a new array, grouped by the dql alias + * they belong to. The column names in the result set are mapped to their + * field names during this procedure as well as any necessary conversions on + * the values applied. Scalar values are kept in a specific key 'scalars'. + * + * @param mixed[] $data SQL Result Row. + * @psalm-param array $id Dql-Alias => ID-Hash. + * @psalm-param array $nonemptyComponents Does this DQL-Alias has at least one non NULL value? + * + * @return array> An array with all the fields + * (name => value) of the data + * row, grouped by their + * component alias. + * @psalm-return array{ + * data: array, + * newObjects?: array, + * scalars?: array + * } + */ + protected function gatherRowData(array $data, array &$id, array &$nonemptyComponents): array + { + $rowData = ['data' => []]; + + foreach ($data as $key => $value) { + $cacheKeyInfo = $this->hydrateColumnInfo($key); + if ($cacheKeyInfo === null) { + continue; + } + + $fieldName = $cacheKeyInfo['fieldName']; + + switch (true) { + case isset($cacheKeyInfo['isNewObjectParameter']): + $argIndex = $cacheKeyInfo['argIndex']; + $objIndex = $cacheKeyInfo['objIndex']; + $type = $cacheKeyInfo['type']; + $value = $type->convertToPHPValue($value, $this->platform); + + if ($value !== null && isset($cacheKeyInfo['enumType'])) { + $value = $this->buildEnum($value, $cacheKeyInfo['enumType']); + } + + $rowData['newObjects'][$objIndex]['class'] = $cacheKeyInfo['class']; + $rowData['newObjects'][$objIndex]['args'][$argIndex] = $value; + break; + + case isset($cacheKeyInfo['isScalar']): + $type = $cacheKeyInfo['type']; + $value = $type->convertToPHPValue($value, $this->platform); + + if ($value !== null && isset($cacheKeyInfo['enumType'])) { + $value = $this->buildEnum($value, $cacheKeyInfo['enumType']); + } + + $rowData['scalars'][$fieldName] = $value; + + break; + + //case (isset($cacheKeyInfo['isMetaColumn'])): + default: + $dqlAlias = $cacheKeyInfo['dqlAlias']; + $type = $cacheKeyInfo['type']; + + // If there are field name collisions in the child class, then we need + // to only hydrate if we are looking at the correct discriminator value + if ( + isset($cacheKeyInfo['discriminatorColumn'], $data[$cacheKeyInfo['discriminatorColumn']]) + && ! in_array((string) $data[$cacheKeyInfo['discriminatorColumn']], $cacheKeyInfo['discriminatorValues'], true) + ) { + break; + } + + // in an inheritance hierarchy the same field could be defined several times. + // We overwrite this value so long we don't have a non-null value, that value we keep. + // Per definition it cannot be that a field is defined several times and has several values. + if (isset($rowData['data'][$dqlAlias][$fieldName])) { + break; + } + + $rowData['data'][$dqlAlias][$fieldName] = $type + ? $type->convertToPHPValue($value, $this->platform) + : $value; + + if ($rowData['data'][$dqlAlias][$fieldName] !== null && isset($cacheKeyInfo['enumType'])) { + $rowData['data'][$dqlAlias][$fieldName] = $this->buildEnum($rowData['data'][$dqlAlias][$fieldName], $cacheKeyInfo['enumType']); + } + + if ($cacheKeyInfo['isIdentifier'] && $value !== null) { + $id[$dqlAlias] .= '|' . $value; + $nonemptyComponents[$dqlAlias] = true; + } + + break; + } + } + + return $rowData; + } + + /** + * Processes a row of the result set. + * + * Used for HYDRATE_SCALAR. This is a variant of _gatherRowData() that + * simply converts column names to field names and properly converts the + * values according to their types. The resulting row has the same number + * of elements as before. + * + * @param mixed[] $data + * @psalm-param array $data + * + * @return mixed[] The processed row. + * @psalm-return array + */ + protected function gatherScalarRowData(array &$data): array + { + $rowData = []; + + foreach ($data as $key => $value) { + $cacheKeyInfo = $this->hydrateColumnInfo($key); + if ($cacheKeyInfo === null) { + continue; + } + + $fieldName = $cacheKeyInfo['fieldName']; + + // WARNING: BC break! We know this is the desired behavior to type convert values, but this + // erroneous behavior exists since 2.0 and we're forced to keep compatibility. + if (! isset($cacheKeyInfo['isScalar'])) { + $type = $cacheKeyInfo['type']; + $value = $type ? $type->convertToPHPValue($value, $this->platform) : $value; + + $fieldName = $cacheKeyInfo['dqlAlias'] . '_' . $fieldName; + } + + $rowData[$fieldName] = $value; + } + + return $rowData; + } + + /** + * Retrieve column information from ResultSetMapping. + * + * @param string $key Column name + * + * @return mixed[]|null + * @psalm-return array|null + */ + protected function hydrateColumnInfo(string $key): array|null + { + if (isset($this->cache[$key])) { + return $this->cache[$key]; + } + + switch (true) { + // NOTE: Most of the times it's a field mapping, so keep it first!!! + case isset($this->rsm->fieldMappings[$key]): + $classMetadata = $this->getClassMetadata($this->rsm->declaringClasses[$key]); + $fieldName = $this->rsm->fieldMappings[$key]; + $fieldMapping = $classMetadata->fieldMappings[$fieldName]; + $ownerMap = $this->rsm->columnOwnerMap[$key]; + $columnInfo = [ + 'isIdentifier' => in_array($fieldName, $classMetadata->identifier, true), + 'fieldName' => $fieldName, + 'type' => Type::getType($fieldMapping->type), + 'dqlAlias' => $ownerMap, + 'enumType' => $this->rsm->enumMappings[$key] ?? null, + ]; + + // the current discriminator value must be saved in order to disambiguate fields hydration, + // should there be field name collisions + if ($classMetadata->parentClasses && isset($this->rsm->discriminatorColumns[$ownerMap])) { + return $this->cache[$key] = array_merge( + $columnInfo, + [ + 'discriminatorColumn' => $this->rsm->discriminatorColumns[$ownerMap], + 'discriminatorValue' => $classMetadata->discriminatorValue, + 'discriminatorValues' => $this->getDiscriminatorValues($classMetadata), + ], + ); + } + + return $this->cache[$key] = $columnInfo; + + case isset($this->rsm->newObjectMappings[$key]): + // WARNING: A NEW object is also a scalar, so it must be declared before! + $mapping = $this->rsm->newObjectMappings[$key]; + + return $this->cache[$key] = [ + 'isScalar' => true, + 'isNewObjectParameter' => true, + 'fieldName' => $this->rsm->scalarMappings[$key], + 'type' => Type::getType($this->rsm->typeMappings[$key]), + 'argIndex' => $mapping['argIndex'], + 'objIndex' => $mapping['objIndex'], + 'class' => new ReflectionClass($mapping['className']), + 'enumType' => $this->rsm->enumMappings[$key] ?? null, + ]; + + case isset($this->rsm->scalarMappings[$key], $this->hints[LimitSubqueryWalker::FORCE_DBAL_TYPE_CONVERSION]): + return $this->cache[$key] = [ + 'fieldName' => $this->rsm->scalarMappings[$key], + 'type' => Type::getType($this->rsm->typeMappings[$key]), + 'dqlAlias' => '', + 'enumType' => $this->rsm->enumMappings[$key] ?? null, + ]; + + case isset($this->rsm->scalarMappings[$key]): + return $this->cache[$key] = [ + 'isScalar' => true, + 'fieldName' => $this->rsm->scalarMappings[$key], + 'type' => Type::getType($this->rsm->typeMappings[$key]), + 'enumType' => $this->rsm->enumMappings[$key] ?? null, + ]; + + case isset($this->rsm->metaMappings[$key]): + // Meta column (has meaning in relational schema only, i.e. foreign keys or discriminator columns). + $fieldName = $this->rsm->metaMappings[$key]; + $dqlAlias = $this->rsm->columnOwnerMap[$key]; + $type = isset($this->rsm->typeMappings[$key]) + ? Type::getType($this->rsm->typeMappings[$key]) + : null; + + // Cache metadata fetch + $this->getClassMetadata($this->rsm->aliasMap[$dqlAlias]); + + return $this->cache[$key] = [ + 'isIdentifier' => isset($this->rsm->isIdentifierColumn[$dqlAlias][$key]), + 'isMetaColumn' => true, + 'fieldName' => $fieldName, + 'type' => $type, + 'dqlAlias' => $dqlAlias, + 'enumType' => $this->rsm->enumMappings[$key] ?? null, + ]; + } + + // this column is a left over, maybe from a LIMIT query hack for example in Oracle or DB2 + // maybe from an additional column that has not been defined in a NativeQuery ResultSetMapping. + return null; + } + + /** + * @return string[] + * @psalm-return non-empty-list + */ + private function getDiscriminatorValues(ClassMetadata $classMetadata): array + { + $values = array_map( + fn (string $subClass): string => (string) $this->getClassMetadata($subClass)->discriminatorValue, + $classMetadata->subClasses, + ); + + $values[] = (string) $classMetadata->discriminatorValue; + + return $values; + } + + /** + * Retrieve ClassMetadata associated to entity class name. + */ + protected function getClassMetadata(string $className): ClassMetadata + { + if (! isset($this->metadataCache[$className])) { + $this->metadataCache[$className] = $this->em->getClassMetadata($className); + } + + return $this->metadataCache[$className]; + } + + /** + * Register entity as managed in UnitOfWork. + * + * @param mixed[] $data + * + * @todo The "$id" generation is the same of UnitOfWork#createEntity. Remove this duplication somehow + */ + protected function registerManaged(ClassMetadata $class, object $entity, array $data): void + { + if ($class->isIdentifierComposite) { + $id = []; + + foreach ($class->identifier as $fieldName) { + $id[$fieldName] = isset($class->associationMappings[$fieldName]) && $class->associationMappings[$fieldName]->isToOneOwningSide() + ? $data[$class->associationMappings[$fieldName]->joinColumns[0]->name] + : $data[$fieldName]; + } + } else { + $fieldName = $class->identifier[0]; + $id = [ + $fieldName => isset($class->associationMappings[$fieldName]) && $class->associationMappings[$fieldName]->isToOneOwningSide() + ? $data[$class->associationMappings[$fieldName]->joinColumns[0]->name] + : $data[$fieldName], + ]; + } + + $this->em->getUnitOfWork()->registerManaged($entity, $id, $data); + } + + /** + * @param class-string $enumType + * + * @return BackedEnum|array + */ + final protected function buildEnum(mixed $value, string $enumType): BackedEnum|array + { + if (is_array($value)) { + return array_map( + static fn ($value) => $enumType::from($value), + $value, + ); + } + + return $enumType::from($value); + } +} 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 @@ + */ + private array $rootAliases = []; + + private bool $isSimpleQuery = false; + + /** @var mixed[] */ + private array $identifierMap = []; + + /** @var mixed[] */ + private array $resultPointers = []; + + /** @var array */ + private array $idTemplate = []; + + private int $resultCounter = 0; + + protected function prepare(): void + { + $this->isSimpleQuery = count($this->resultSetMapping()->aliasMap) <= 1; + + foreach ($this->resultSetMapping()->aliasMap as $dqlAlias => $className) { + $this->identifierMap[$dqlAlias] = []; + $this->resultPointers[$dqlAlias] = []; + $this->idTemplate[$dqlAlias] = ''; + } + } + + /** + * {@inheritDoc} + */ + protected function hydrateAllData(): array + { + $result = []; + + while ($data = $this->statement()->fetchAssociative()) { + $this->hydrateRowData($data, $result); + } + + return $result; + } + + /** + * {@inheritDoc} + */ + protected function hydrateRowData(array $row, array &$result): void + { + // 1) Initialize + $id = $this->idTemplate; // initialize the id-memory + $nonemptyComponents = []; + $rowData = $this->gatherRowData($row, $id, $nonemptyComponents); + + // 2) Now hydrate the data found in the current row. + foreach ($rowData['data'] as $dqlAlias => $data) { + $index = false; + + if (isset($this->resultSetMapping()->parentAliasMap[$dqlAlias])) { + // It's a joined result + + $parent = $this->resultSetMapping()->parentAliasMap[$dqlAlias]; + $path = $parent . '.' . $dqlAlias; + + // missing parent data, skipping as RIGHT JOIN hydration is not supported. + if (! isset($nonemptyComponents[$parent])) { + continue; + } + + // Get a reference to the right element in the result tree. + // This element will get the associated element attached. + if ($this->resultSetMapping()->isMixed && isset($this->rootAliases[$parent])) { + $first = reset($this->resultPointers); + // TODO: Exception if $key === null ? + $baseElement =& $this->resultPointers[$parent][key($first)]; + } elseif (isset($this->resultPointers[$parent])) { + $baseElement =& $this->resultPointers[$parent]; + } else { + unset($this->resultPointers[$dqlAlias]); // Ticket #1228 + + continue; + } + + $relationAlias = $this->resultSetMapping()->relationMap[$dqlAlias]; + $parentClass = $this->metadataCache[$this->resultSetMapping()->aliasMap[$parent]]; + $relation = $parentClass->associationMappings[$relationAlias]; + + // Check the type of the relation (many or single-valued) + if (! $relation->isToOne()) { + $oneToOne = false; + + if (! isset($baseElement[$relationAlias])) { + $baseElement[$relationAlias] = []; + } + + if (isset($nonemptyComponents[$dqlAlias])) { + $indexExists = isset($this->identifierMap[$path][$id[$parent]][$id[$dqlAlias]]); + $index = $indexExists ? $this->identifierMap[$path][$id[$parent]][$id[$dqlAlias]] : false; + $indexIsValid = $index !== false ? isset($baseElement[$relationAlias][$index]) : false; + + if (! $indexExists || ! $indexIsValid) { + $element = $data; + + if (isset($this->resultSetMapping()->indexByMap[$dqlAlias])) { + $baseElement[$relationAlias][$row[$this->resultSetMapping()->indexByMap[$dqlAlias]]] = $element; + } else { + $baseElement[$relationAlias][] = $element; + } + + $this->identifierMap[$path][$id[$parent]][$id[$dqlAlias]] = array_key_last($baseElement[$relationAlias]); + } + } + } else { + $oneToOne = true; + + if ( + ! isset($nonemptyComponents[$dqlAlias]) && + ( ! isset($baseElement[$relationAlias])) + ) { + $baseElement[$relationAlias] = null; + } elseif (! isset($baseElement[$relationAlias])) { + $baseElement[$relationAlias] = $data; + } + } + + $coll =& $baseElement[$relationAlias]; + + if (is_array($coll)) { + $this->updateResultPointer($coll, $index, $dqlAlias, $oneToOne); + } + } else { + // It's a root result element + + $this->rootAliases[$dqlAlias] = true; // Mark as root + $entityKey = $this->resultSetMapping()->entityMappings[$dqlAlias] ?: 0; + + // if this row has a NULL value for the root result id then make it a null result. + if (! isset($nonemptyComponents[$dqlAlias])) { + $result[] = $this->resultSetMapping()->isMixed + ? [$entityKey => null] + : null; + + $resultKey = $this->resultCounter; + ++$this->resultCounter; + + continue; + } + + // Check for an existing element + if ($this->isSimpleQuery || ! isset($this->identifierMap[$dqlAlias][$id[$dqlAlias]])) { + $element = $this->resultSetMapping()->isMixed + ? [$entityKey => $data] + : $data; + + if (isset($this->resultSetMapping()->indexByMap[$dqlAlias])) { + $resultKey = $row[$this->resultSetMapping()->indexByMap[$dqlAlias]]; + $result[$resultKey] = $element; + } else { + $resultKey = $this->resultCounter; + $result[] = $element; + + ++$this->resultCounter; + } + + $this->identifierMap[$dqlAlias][$id[$dqlAlias]] = $resultKey; + } else { + $index = $this->identifierMap[$dqlAlias][$id[$dqlAlias]]; + $resultKey = $index; + } + + $this->updateResultPointer($result, $index, $dqlAlias, false); + } + } + + if (! isset($resultKey)) { + $this->resultCounter++; + } + + // Append scalar values to mixed result sets + if (isset($rowData['scalars'])) { + if (! isset($resultKey)) { + // this only ever happens when no object is fetched (scalar result only) + $resultKey = isset($this->resultSetMapping()->indexByMap['scalars']) + ? $row[$this->resultSetMapping()->indexByMap['scalars']] + : $this->resultCounter - 1; + } + + foreach ($rowData['scalars'] as $name => $value) { + $result[$resultKey][$name] = $value; + } + } + + // Append new object to mixed result sets + if (isset($rowData['newObjects'])) { + if (! isset($resultKey)) { + $resultKey = $this->resultCounter - 1; + } + + $scalarCount = (isset($rowData['scalars']) ? count($rowData['scalars']) : 0); + + foreach ($rowData['newObjects'] as $objIndex => $newObject) { + $class = $newObject['class']; + $args = $newObject['args']; + $obj = $class->newInstanceArgs($args); + + if (count($args) === $scalarCount || ($scalarCount === 0 && count($rowData['newObjects']) === 1)) { + $result[$resultKey] = $obj; + + continue; + } + + $result[$resultKey][$objIndex] = $obj; + } + } + } + + /** + * Updates the result pointer for an Entity. The result pointers point to the + * last seen instance of each Entity type. This is used for graph construction. + * + * @param mixed[]|null $coll The element. + * @param string|int|false $index Index of the element in the collection. + * @param bool $oneToOne Whether it is a single-valued association or not. + */ + private function updateResultPointer( + array|null &$coll, + string|int|false $index, + string $dqlAlias, + bool $oneToOne, + ): void { + if ($coll === null) { + unset($this->resultPointers[$dqlAlias]); // Ticket #1228 + + return; + } + + if ($oneToOne) { + $this->resultPointers[$dqlAlias] =& $coll; + + return; + } + + if ($index !== false) { + $this->resultPointers[$dqlAlias] =& $coll[$index]; + + return; + } + + if (! $coll) { + return; + } + + $this->resultPointers[$dqlAlias] =& $coll[array_key_last($coll)]; + } +} 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 @@ + $discrValues */ + public static function invalidDiscriminatorValue(string $discrValue, array $discrValues): self + { + return new self(sprintf( + 'The discriminator value "%s" is invalid. It must be one of "%s".', + $discrValue, + implode('", "', $discrValues), + )); + } +} 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 @@ + */ + private array $uninitializedCollections = []; + + /** @var mixed[] */ + private array $existingCollections = []; + + protected function prepare(): void + { + if (! isset($this->hints[UnitOfWork::HINT_DEFEREAGERLOAD])) { + $this->hints[UnitOfWork::HINT_DEFEREAGERLOAD] = true; + } + + foreach ($this->resultSetMapping()->aliasMap as $dqlAlias => $className) { + $this->identifierMap[$dqlAlias] = []; + $this->idTemplate[$dqlAlias] = ''; + + // Remember which associations are "fetch joined", so that we know where to inject + // collection stubs or proxies and where not. + if (! isset($this->resultSetMapping()->relationMap[$dqlAlias])) { + continue; + } + + $parent = $this->resultSetMapping()->parentAliasMap[$dqlAlias]; + + if (! isset($this->resultSetMapping()->aliasMap[$parent])) { + throw HydrationException::parentObjectOfRelationNotFound($dqlAlias, $parent); + } + + $sourceClassName = $this->resultSetMapping()->aliasMap[$parent]; + $sourceClass = $this->getClassMetadata($sourceClassName); + $assoc = $sourceClass->associationMappings[$this->resultSetMapping()->relationMap[$dqlAlias]]; + + $this->hints['fetched'][$parent][$assoc->fieldName] = true; + + if ($assoc->isManyToMany()) { + continue; + } + + // Mark any non-collection opposite sides as fetched, too. + if (! $assoc->isOwningSide()) { + $this->hints['fetched'][$dqlAlias][$assoc->mappedBy] = true; + + continue; + } + + // handle fetch-joined owning side bi-directional one-to-one associations + if ($assoc->inversedBy !== null) { + $class = $this->getClassMetadata($className); + $inverseAssoc = $class->associationMappings[$assoc->inversedBy]; + + if (! $inverseAssoc->isToOne()) { + continue; + } + + $this->hints['fetched'][$dqlAlias][$inverseAssoc->fieldName] = true; + } + } + } + + protected function cleanup(): void + { + $eagerLoad = isset($this->hints[UnitOfWork::HINT_DEFEREAGERLOAD]) && $this->hints[UnitOfWork::HINT_DEFEREAGERLOAD] === true; + + parent::cleanup(); + + $this->identifierMap = + $this->initializedCollections = + $this->uninitializedCollections = + $this->existingCollections = + $this->resultPointers = []; + + if ($eagerLoad) { + $this->uow->triggerEagerLoads(); + } + + $this->uow->hydrationComplete(); + } + + protected function cleanupAfterRowIteration(): void + { + $this->identifierMap = + $this->initializedCollections = + $this->uninitializedCollections = + $this->existingCollections = + $this->resultPointers = []; + } + + /** + * {@inheritDoc} + */ + protected function hydrateAllData(): array + { + $result = []; + + while ($row = $this->statement()->fetchAssociative()) { + $this->hydrateRowData($row, $result); + } + + // Take snapshots from all newly initialized collections + foreach ($this->initializedCollections as $coll) { + $coll->takeSnapshot(); + } + + foreach ($this->uninitializedCollections as $coll) { + if (! $coll->isInitialized()) { + $coll->setInitialized(true); + } + } + + return $result; + } + + /** + * Initializes a related collection. + * + * @param string $fieldName The name of the field on the entity that holds the collection. + * @param string $parentDqlAlias Alias of the parent fetch joining this collection. + */ + private function initRelatedCollection( + object $entity, + ClassMetadata $class, + string $fieldName, + string $parentDqlAlias, + ): PersistentCollection { + $oid = spl_object_id($entity); + $relation = $class->associationMappings[$fieldName]; + $value = $class->reflFields[$fieldName]->getValue($entity); + + if ($value === null || is_array($value)) { + $value = new ArrayCollection((array) $value); + } + + if (! $value instanceof PersistentCollection) { + assert($relation->isToMany()); + $value = new PersistentCollection( + $this->em, + $this->metadataCache[$relation->targetEntity], + $value, + ); + $value->setOwner($entity, $relation); + + $class->reflFields[$fieldName]->setValue($entity, $value); + $this->uow->setOriginalEntityProperty($oid, $fieldName, $value); + + $this->initializedCollections[$oid . $fieldName] = $value; + } elseif ( + isset($this->hints[Query::HINT_REFRESH]) || + isset($this->hints['fetched'][$parentDqlAlias][$fieldName]) && + ! $value->isInitialized() + ) { + // Is already PersistentCollection, but either REFRESH or FETCH-JOIN and UNINITIALIZED! + $value->setDirty(false); + $value->setInitialized(true); + $value->unwrap()->clear(); + + $this->initializedCollections[$oid . $fieldName] = $value; + } else { + // Is already PersistentCollection, and DON'T REFRESH or FETCH-JOIN! + $this->existingCollections[$oid . $fieldName] = $value; + } + + return $value; + } + + /** + * Gets an entity instance. + * + * @param string $dqlAlias The DQL alias of the entity's class. + * @psalm-param array $data The instance data. + * + * @throws HydrationException + */ + private function getEntity(array $data, string $dqlAlias): object + { + $className = $this->resultSetMapping()->aliasMap[$dqlAlias]; + + if (isset($this->resultSetMapping()->discriminatorColumns[$dqlAlias])) { + $fieldName = $this->resultSetMapping()->discriminatorColumns[$dqlAlias]; + + if (! isset($this->resultSetMapping()->metaMappings[$fieldName])) { + throw HydrationException::missingDiscriminatorMetaMappingColumn($className, $fieldName, $dqlAlias); + } + + $discrColumn = $this->resultSetMapping()->metaMappings[$fieldName]; + + if (! isset($data[$discrColumn])) { + throw HydrationException::missingDiscriminatorColumn($className, $discrColumn, $dqlAlias); + } + + if ($data[$discrColumn] === '') { + throw HydrationException::emptyDiscriminatorValue($dqlAlias); + } + + $discrMap = $this->metadataCache[$className]->discriminatorMap; + $discriminatorValue = $data[$discrColumn]; + if ($discriminatorValue instanceof BackedEnum) { + $discriminatorValue = $discriminatorValue->value; + } + + $discriminatorValue = (string) $discriminatorValue; + + if (! isset($discrMap[$discriminatorValue])) { + throw HydrationException::invalidDiscriminatorValue($discriminatorValue, array_keys($discrMap)); + } + + $className = $discrMap[$discriminatorValue]; + + unset($data[$discrColumn]); + } + + if (isset($this->hints[Query::HINT_REFRESH_ENTITY], $this->rootAliases[$dqlAlias])) { + $this->registerManaged($this->metadataCache[$className], $this->hints[Query::HINT_REFRESH_ENTITY], $data); + } + + $this->hints['fetchAlias'] = $dqlAlias; + + return $this->uow->createEntity($className, $data, $this->hints); + } + + /** + * @psalm-param class-string $className + * @psalm-param array $data + */ + private function getEntityFromIdentityMap(string $className, array $data): object|bool + { + // TODO: Abstract this code and UnitOfWork::createEntity() equivalent? + $class = $this->metadataCache[$className]; + + if ($class->isIdentifierComposite) { + $idHash = UnitOfWork::getIdHashByIdentifier( + array_map( + /** @return mixed */ + static fn (string $fieldName) => isset($class->associationMappings[$fieldName]) && assert($class->associationMappings[$fieldName]->isToOneOwningSide()) + ? $data[$class->associationMappings[$fieldName]->joinColumns[0]->name] + : $data[$fieldName], + $class->identifier, + ), + ); + + return $this->uow->tryGetByIdHash(ltrim($idHash), $class->rootEntityName); + } elseif (isset($class->associationMappings[$class->identifier[0]])) { + $association = $class->associationMappings[$class->identifier[0]]; + assert($association->isToOneOwningSide()); + + return $this->uow->tryGetByIdHash($data[$association->joinColumns[0]->name], $class->rootEntityName); + } + + return $this->uow->tryGetByIdHash($data[$class->identifier[0]], $class->rootEntityName); + } + + /** + * Hydrates a single row in an SQL result set. + * + * @internal + * First, the data of the row is split into chunks where each chunk contains data + * that belongs to a particular component/class. Afterwards, all these chunks + * are processed, one after the other. For each chunk of class data only one of the + * following code paths is executed: + * Path A: The data chunk belongs to a joined/associated object and the association + * is collection-valued. + * Path B: The data chunk belongs to a joined/associated object and the association + * is single-valued. + * Path C: The data chunk belongs to a root result element/object that appears in the topmost + * level of the hydrated result. A typical example are the objects of the type + * specified by the FROM clause in a DQL query. + * + * @param mixed[] $row The data of the row to process. + * @param mixed[] $result The result array to fill. + */ + protected function hydrateRowData(array $row, array &$result): void + { + // Initialize + $id = $this->idTemplate; // initialize the id-memory + $nonemptyComponents = []; + // Split the row data into chunks of class data. + $rowData = $this->gatherRowData($row, $id, $nonemptyComponents); + + // reset result pointers for each data row + $this->resultPointers = []; + + // Hydrate the data chunks + foreach ($rowData['data'] as $dqlAlias => $data) { + $entityName = $this->resultSetMapping()->aliasMap[$dqlAlias]; + + if (isset($this->resultSetMapping()->parentAliasMap[$dqlAlias])) { + // It's a joined result + + $parentAlias = $this->resultSetMapping()->parentAliasMap[$dqlAlias]; + // we need the $path to save into the identifier map which entities were already + // seen for this parent-child relationship + $path = $parentAlias . '.' . $dqlAlias; + + // We have a RIGHT JOIN result here. Doctrine cannot hydrate RIGHT JOIN Object-Graphs + if (! isset($nonemptyComponents[$parentAlias])) { + // TODO: Add special case code where we hydrate the right join objects into identity map at least + continue; + } + + $parentClass = $this->metadataCache[$this->resultSetMapping()->aliasMap[$parentAlias]]; + $relationField = $this->resultSetMapping()->relationMap[$dqlAlias]; + $relation = $parentClass->associationMappings[$relationField]; + $reflField = $parentClass->reflFields[$relationField]; + + // Get a reference to the parent object to which the joined element belongs. + if ($this->resultSetMapping()->isMixed && isset($this->rootAliases[$parentAlias])) { + $objectClass = $this->resultPointers[$parentAlias]; + $parentObject = $objectClass[key($objectClass)]; + } elseif (isset($this->resultPointers[$parentAlias])) { + $parentObject = $this->resultPointers[$parentAlias]; + } else { + // Parent object of relation not found, mark as not-fetched again + if (isset($nonemptyComponents[$dqlAlias])) { + $element = $this->getEntity($data, $dqlAlias); + + // Update result pointer and provide initial fetch data for parent + $this->resultPointers[$dqlAlias] = $element; + $rowData['data'][$parentAlias][$relationField] = $element; + } else { + $element = null; + } + + // Mark as not-fetched again + unset($this->hints['fetched'][$parentAlias][$relationField]); + continue; + } + + $oid = spl_object_id($parentObject); + + // Check the type of the relation (many or single-valued) + if (! $relation->isToOne()) { + // PATH A: Collection-valued association + $reflFieldValue = $reflField->getValue($parentObject); + + if (isset($nonemptyComponents[$dqlAlias])) { + $collKey = $oid . $relationField; + if (isset($this->initializedCollections[$collKey])) { + $reflFieldValue = $this->initializedCollections[$collKey]; + } elseif (! isset($this->existingCollections[$collKey])) { + $reflFieldValue = $this->initRelatedCollection($parentObject, $parentClass, $relationField, $parentAlias); + } + + $indexExists = isset($this->identifierMap[$path][$id[$parentAlias]][$id[$dqlAlias]]); + $index = $indexExists ? $this->identifierMap[$path][$id[$parentAlias]][$id[$dqlAlias]] : false; + $indexIsValid = $index !== false ? isset($reflFieldValue[$index]) : false; + + if (! $indexExists || ! $indexIsValid) { + if (isset($this->existingCollections[$collKey])) { + // Collection exists, only look for the element in the identity map. + $element = $this->getEntityFromIdentityMap($entityName, $data); + if ($element) { + $this->resultPointers[$dqlAlias] = $element; + } else { + unset($this->resultPointers[$dqlAlias]); + } + } else { + $element = $this->getEntity($data, $dqlAlias); + + if (isset($this->resultSetMapping()->indexByMap[$dqlAlias])) { + $indexValue = $row[$this->resultSetMapping()->indexByMap[$dqlAlias]]; + $reflFieldValue->hydrateSet($indexValue, $element); + $this->identifierMap[$path][$id[$parentAlias]][$id[$dqlAlias]] = $indexValue; + } else { + if (! $reflFieldValue->contains($element)) { + $reflFieldValue->hydrateAdd($element); + $reflFieldValue->last(); + } + + $this->identifierMap[$path][$id[$parentAlias]][$id[$dqlAlias]] = $reflFieldValue->key(); + } + + // Update result pointer + $this->resultPointers[$dqlAlias] = $element; + } + } else { + // Update result pointer + $this->resultPointers[$dqlAlias] = $reflFieldValue[$index]; + } + } elseif (! $reflFieldValue) { + $this->initRelatedCollection($parentObject, $parentClass, $relationField, $parentAlias); + } elseif ($reflFieldValue instanceof PersistentCollection && $reflFieldValue->isInitialized() === false && ! isset($this->uninitializedCollections[$oid . $relationField])) { + $this->uninitializedCollections[$oid . $relationField] = $reflFieldValue; + } + } else { + // PATH B: Single-valued association + $reflFieldValue = $reflField->getValue($parentObject); + + if (! $reflFieldValue || isset($this->hints[Query::HINT_REFRESH]) || $this->uow->isUninitializedObject($reflFieldValue)) { + // we only need to take action if this value is null, + // we refresh the entity or its an uninitialized proxy. + if (isset($nonemptyComponents[$dqlAlias])) { + $element = $this->getEntity($data, $dqlAlias); + $reflField->setValue($parentObject, $element); + $this->uow->setOriginalEntityProperty($oid, $relationField, $element); + $targetClass = $this->metadataCache[$relation->targetEntity]; + + if ($relation->isOwningSide()) { + // TODO: Just check hints['fetched'] here? + // If there is an inverse mapping on the target class its bidirectional + if ($relation->inversedBy !== null) { + $inverseAssoc = $targetClass->associationMappings[$relation->inversedBy]; + if ($inverseAssoc->isToOne()) { + $targetClass->reflFields[$inverseAssoc->fieldName]->setValue($element, $parentObject); + $this->uow->setOriginalEntityProperty(spl_object_id($element), $inverseAssoc->fieldName, $parentObject); + } + } + } else { + // For sure bidirectional, as there is no inverse side in unidirectional mappings + $targetClass->reflFields[$relation->mappedBy]->setValue($element, $parentObject); + $this->uow->setOriginalEntityProperty(spl_object_id($element), $relation->mappedBy, $parentObject); + } + + // Update result pointer + $this->resultPointers[$dqlAlias] = $element; + } else { + $this->uow->setOriginalEntityProperty($oid, $relationField, null); + $reflField->setValue($parentObject, null); + } + // else leave $reflFieldValue null for single-valued associations + } else { + // Update result pointer + $this->resultPointers[$dqlAlias] = $reflFieldValue; + } + } + } else { + // PATH C: Its a root result element + $this->rootAliases[$dqlAlias] = true; // Mark as root alias + $entityKey = $this->resultSetMapping()->entityMappings[$dqlAlias] ?: 0; + + // if this row has a NULL value for the root result id then make it a null result. + if (! isset($nonemptyComponents[$dqlAlias])) { + if ($this->resultSetMapping()->isMixed) { + $result[] = [$entityKey => null]; + } else { + $result[] = null; + } + + $resultKey = $this->resultCounter; + ++$this->resultCounter; + continue; + } + + // check for existing result from the iterations before + if (! isset($this->identifierMap[$dqlAlias][$id[$dqlAlias]])) { + $element = $this->getEntity($data, $dqlAlias); + + if ($this->resultSetMapping()->isMixed) { + $element = [$entityKey => $element]; + } + + if (isset($this->resultSetMapping()->indexByMap[$dqlAlias])) { + $resultKey = $row[$this->resultSetMapping()->indexByMap[$dqlAlias]]; + + if (isset($this->hints['collection'])) { + $this->hints['collection']->hydrateSet($resultKey, $element); + } + + $result[$resultKey] = $element; + } else { + $resultKey = $this->resultCounter; + ++$this->resultCounter; + + if (isset($this->hints['collection'])) { + $this->hints['collection']->hydrateAdd($element); + } + + $result[] = $element; + } + + $this->identifierMap[$dqlAlias][$id[$dqlAlias]] = $resultKey; + + // Update result pointer + $this->resultPointers[$dqlAlias] = $element; + } else { + // Update result pointer + $index = $this->identifierMap[$dqlAlias][$id[$dqlAlias]]; + $this->resultPointers[$dqlAlias] = $result[$index]; + $resultKey = $index; + } + } + + if (isset($this->hints[Query::HINT_INTERNAL_ITERATION]) && $this->hints[Query::HINT_INTERNAL_ITERATION]) { + $this->uow->hydrationComplete(); + } + } + + if (! isset($resultKey)) { + $this->resultCounter++; + } + + // Append scalar values to mixed result sets + if (isset($rowData['scalars'])) { + if (! isset($resultKey)) { + $resultKey = isset($this->resultSetMapping()->indexByMap['scalars']) + ? $row[$this->resultSetMapping()->indexByMap['scalars']] + : $this->resultCounter - 1; + } + + foreach ($rowData['scalars'] as $name => $value) { + $result[$resultKey][$name] = $value; + } + } + + // Append new object to mixed result sets + if (isset($rowData['newObjects'])) { + if (! isset($resultKey)) { + $resultKey = $this->resultCounter - 1; + } + + $scalarCount = (isset($rowData['scalars']) ? count($rowData['scalars']) : 0); + + foreach ($rowData['newObjects'] as $objIndex => $newObject) { + $class = $newObject['class']; + $args = $newObject['args']; + $obj = $class->newInstanceArgs($args); + + if ($scalarCount === 0 && count($rowData['newObjects']) === 1) { + $result[$resultKey] = $obj; + + continue; + } + + $result[$resultKey][$objIndex] = $obj; + } + } + } + + /** + * When executed in a hydrate() loop we may have to clear internal state to + * decrease memory consumption. + */ + public function onClear(mixed $eventArgs): void + { + parent::onClear($eventArgs); + + $aliases = array_keys($this->identifierMap); + + $this->identifierMap = array_fill_keys($aliases, []); + } +} 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 @@ +resultSetMapping()->fieldMappings) > 1) { + throw MultipleSelectorsFoundException::create($this->resultSetMapping()->fieldMappings); + } + + $result = $this->statement()->fetchAllNumeric(); + + return array_column($result, 0); + } +} 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 @@ +statement()->fetchAssociative()) { + $this->hydrateRowData($data, $result); + } + + return $result; + } + + /** + * {@inheritDoc} + */ + protected function hydrateRowData(array $row, array &$result): void + { + $result[] = $this->gatherScalarRowData($row); + } +} 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 @@ +resultSetMapping()->aliasMap) !== 1) { + throw new RuntimeException('Cannot use SimpleObjectHydrator with a ResultSetMapping that contains more than one object result.'); + } + + if ($this->resultSetMapping()->scalarMappings) { + throw new RuntimeException('Cannot use SimpleObjectHydrator with a ResultSetMapping that contains scalar mappings.'); + } + + $this->class = $this->getClassMetadata(reset($this->resultSetMapping()->aliasMap)); + } + + protected function cleanup(): void + { + parent::cleanup(); + + $this->uow->triggerEagerLoads(); + $this->uow->hydrationComplete(); + } + + /** + * {@inheritDoc} + */ + protected function hydrateAllData(): array + { + $result = []; + + while ($row = $this->statement()->fetchAssociative()) { + $this->hydrateRowData($row, $result); + } + + $this->em->getUnitOfWork()->triggerEagerLoads(); + + return $result; + } + + /** + * {@inheritDoc} + */ + protected function hydrateRowData(array $row, array &$result): void + { + assert($this->class !== null); + $entityName = $this->class->name; + $data = []; + $discrColumnValue = null; + + // We need to find the correct entity class name if we have inheritance in resultset + if ($this->class->inheritanceType !== ClassMetadata::INHERITANCE_TYPE_NONE) { + $discrColumn = $this->class->getDiscriminatorColumn(); + $discrColumnName = $this->getSQLResultCasing($this->platform, $discrColumn->name); + + // Find mapped discriminator column from the result set. + $metaMappingDiscrColumnName = array_search($discrColumnName, $this->resultSetMapping()->metaMappings, true); + if ($metaMappingDiscrColumnName) { + $discrColumnName = $metaMappingDiscrColumnName; + } + + if (! isset($row[$discrColumnName])) { + throw HydrationException::missingDiscriminatorColumn( + $entityName, + $discrColumnName, + key($this->resultSetMapping()->aliasMap), + ); + } + + if ($row[$discrColumnName] === '') { + throw HydrationException::emptyDiscriminatorValue(key( + $this->resultSetMapping()->aliasMap, + )); + } + + $discrMap = $this->class->discriminatorMap; + + if (! isset($discrMap[$row[$discrColumnName]])) { + throw HydrationException::invalidDiscriminatorValue($row[$discrColumnName], array_keys($discrMap)); + } + + $entityName = $discrMap[$row[$discrColumnName]]; + $discrColumnValue = $row[$discrColumnName]; + + unset($row[$discrColumnName]); + } + + foreach ($row as $column => $value) { + // An ObjectHydrator should be used instead of SimpleObjectHydrator + if (isset($this->resultSetMapping()->relationMap[$column])) { + throw new Exception(sprintf('Unable to retrieve association information for column "%s"', $column)); + } + + $cacheKeyInfo = $this->hydrateColumnInfo($column); + + if (! $cacheKeyInfo) { + continue; + } + + // If we have inheritance in resultset, make sure the field belongs to the correct class + if (isset($cacheKeyInfo['discriminatorValues']) && ! in_array((string) $discrColumnValue, $cacheKeyInfo['discriminatorValues'], true)) { + continue; + } + + // Check if value is null before conversion (because some types convert null to something else) + $valueIsNull = $value === null; + + // Convert field to a valid PHP value + if (isset($cacheKeyInfo['type'])) { + $type = $cacheKeyInfo['type']; + $value = $type->convertToPHPValue($value, $this->platform); + } + + if ($value !== null && isset($cacheKeyInfo['enumType'])) { + $originalValue = $value; + try { + $value = $this->buildEnum($originalValue, $cacheKeyInfo['enumType']); + } catch (ValueError $e) { + throw MappingException::invalidEnumValue( + $entityName, + $cacheKeyInfo['fieldName'], + (string) $originalValue, + $cacheKeyInfo['enumType'], + $e, + ); + } + } + + $fieldName = $cacheKeyInfo['fieldName']; + + // Prevent overwrite in case of inherit classes using same property name (See AbstractHydrator) + if (! isset($data[$fieldName]) || ! $valueIsNull) { + $data[$fieldName] = $value; + } + } + + if (isset($this->hints[Query::HINT_REFRESH_ENTITY])) { + $this->registerManaged($this->class, $this->hints[Query::HINT_REFRESH_ENTITY], $data); + } + + $uow = $this->em->getUnitOfWork(); + $entity = $uow->createEntity($entityName, $data, $this->hints); + + $result[] = $entity; + + if (isset($this->hints[Query::HINT_INTERNAL_ITERATION]) && $this->hints[Query::HINT_INTERNAL_ITERATION]) { + $this->uow->hydrationComplete(); + } + } +} 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 @@ +statement()->fetchAllAssociative(); + $numRows = count($data); + + if ($numRows === 0) { + throw new NoResultException(); + } + + if ($numRows > 1) { + throw new NonUniqueResultException('The query returned multiple rows. Change the query or use a different result function like getScalarResult().'); + } + + $result = $this->gatherScalarRowData($data[key($data)]); + + if (count($result) > 1) { + throw new NonUniqueResultException('The query returned a row containing multiple columns. Change the query or use a different result function like getScalarResult().'); + } + + return array_shift($result); + } +} 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 @@ +listenersInvoker->getSubscribedSystems($class, Events::postLoad); + + if ($invoke === ListenersInvoker::INVOKE_NONE) { + return; + } + + $this->deferredPostLoadInvocations[] = [$class, $invoke, $entity]; + } + + /** + * This method should be called after any hydration cycle completed. + * + * Method fires all deferred invocations of postLoad events + */ + public function hydrationComplete(): void + { + $toInvoke = $this->deferredPostLoadInvocations; + $this->deferredPostLoadInvocations = []; + + foreach ($toInvoke as $classAndEntity) { + [$class, $invoke, $entity] = $classAndEntity; + + $this->listenersInvoker->invoke( + $class, + Events::postLoad, + $entity, + new PostLoadEventArgs($entity, $this->em), + $invoke, + ); + } + } +} 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 @@ + $parameter + */ + private static function validateVariadicParameter(array $parameter): void + { + if (array_is_list($parameter)) { + return; + } + + [, $trace] = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); + assert(isset($trace['class'])); + + $additionalArguments = array_values(array_filter( + array_keys($parameter), + is_string(...), + )); + + throw new BadMethodCallException(sprintf( + 'Invalid call to %s::%s(), unknown named arguments: %s', + $trace['class'], + $trace['function'], + implode(', ', $additionalArguments), + )); + } +} 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 @@ + + */ + private array $nodes = []; + + /** + * DFS state for the different nodes, indexed by node object id and using one of + * this class' constants as value. + * + * @var array + */ + private array $states = []; + + /** + * Edges between the nodes. The first-level key is the object id of the outgoing + * node; the second array maps the destination node by object id as key. + * + * @var array> + */ + private array $edges = []; + + /** + * DFS numbers, by object ID + * + * @var array + */ + private array $dfs = []; + + /** + * lowlink numbers, by object ID + * + * @var array + */ + private array $lowlink = []; + + private int $maxdfs = 0; + + /** + * Nodes representing the SCC another node is in, indexed by lookup-node object ID + * + * @var array + */ + private array $representingNodes = []; + + /** + * Stack with OIDs of nodes visited in the current state of the DFS + * + * @var list + */ + private array $stack = []; + + public function addNode(object $node): void + { + $id = spl_object_id($node); + $this->nodes[$id] = $node; + $this->states[$id] = self::NOT_VISITED; + $this->edges[$id] = []; + } + + public function hasNode(object $node): bool + { + return isset($this->nodes[spl_object_id($node)]); + } + + /** + * Adds a new edge between two nodes to the graph + */ + public function addEdge(object $from, object $to): void + { + $fromId = spl_object_id($from); + $toId = spl_object_id($to); + + $this->edges[$fromId][$toId] = true; + } + + public function findStronglyConnectedComponents(): void + { + foreach (array_keys($this->nodes) as $oid) { + if ($this->states[$oid] === self::NOT_VISITED) { + $this->tarjan($oid); + } + } + } + + private function tarjan(int $oid): void + { + $this->dfs[$oid] = $this->lowlink[$oid] = $this->maxdfs++; + $this->states[$oid] = self::IN_PROGRESS; + array_push($this->stack, $oid); + + foreach ($this->edges[$oid] as $adjacentId => $ignored) { + if ($this->states[$adjacentId] === self::NOT_VISITED) { + $this->tarjan($adjacentId); + $this->lowlink[$oid] = min($this->lowlink[$oid], $this->lowlink[$adjacentId]); + } elseif ($this->states[$adjacentId] === self::IN_PROGRESS) { + $this->lowlink[$oid] = min($this->lowlink[$oid], $this->dfs[$adjacentId]); + } + } + + $lowlink = $this->lowlink[$oid]; + if ($lowlink === $this->dfs[$oid]) { + $representingNode = null; + do { + $unwindOid = array_pop($this->stack); + + if (! $representingNode) { + $representingNode = $this->nodes[$unwindOid]; + } + + $this->representingNodes[$unwindOid] = $representingNode; + $this->states[$unwindOid] = self::VISITED; + } while ($unwindOid !== $oid); + } + } + + public function getNodeRepresentingStronglyConnectedComponent(object $node): object + { + $oid = spl_object_id($node); + + if (! isset($this->representingNodes[$oid])) { + throw new InvalidArgumentException('unknown node'); + } + + return $this->representingNodes[$oid]; + } +} 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 @@ + + */ + private array $nodes = []; + + /** + * DFS state for the different nodes, indexed by node object id and using one of + * this class' constants as value. + * + * @var array + */ + private array $states = []; + + /** + * Edges between the nodes. The first-level key is the object id of the outgoing + * node; the second array maps the destination node by object id as key. The final + * boolean value indicates whether the edge is optional or not. + * + * @var array> + */ + private array $edges = []; + + /** + * Builds up the result during the DFS. + * + * @var list + */ + private array $sortResult = []; + + public function addNode(object $node): void + { + $id = spl_object_id($node); + $this->nodes[$id] = $node; + $this->states[$id] = self::NOT_VISITED; + $this->edges[$id] = []; + } + + public function hasNode(object $node): bool + { + return isset($this->nodes[spl_object_id($node)]); + } + + /** + * Adds a new edge between two nodes to the graph + * + * @param bool $optional This indicates whether the edge may be ignored during the topological sort if it is necessary to break cycles. + */ + public function addEdge(object $from, object $to, bool $optional): void + { + $fromId = spl_object_id($from); + $toId = spl_object_id($to); + + if (isset($this->edges[$fromId][$toId]) && $this->edges[$fromId][$toId] === false) { + return; // we already know about this dependency, and it is not optional + } + + $this->edges[$fromId][$toId] = $optional; + } + + /** + * Returns a topological sort of all nodes. When we have an edge A->B between two nodes + * A and B, then B will be listed before A in the result. Visually speaking, when ordering + * the nodes in the result order from left to right, all edges point to the left. + * + * @return list + */ + public function sort(): array + { + foreach (array_keys($this->nodes) as $oid) { + if ($this->states[$oid] === self::NOT_VISITED) { + $this->visit($oid); + } + } + + return $this->sortResult; + } + + private function visit(int $oid): void + { + if ($this->states[$oid] === self::IN_PROGRESS) { + // This node is already on the current DFS stack. We've found a cycle! + throw new CycleDetectedException($this->nodes[$oid]); + } + + if ($this->states[$oid] === self::VISITED) { + // We've reached a node that we've already seen, including all + // other nodes that are reachable from here. We're done here, return. + return; + } + + $this->states[$oid] = self::IN_PROGRESS; + + // Continue the DFS downwards the edge list + foreach ($this->edges[$oid] as $adjacentId => $optional) { + try { + $this->visit($adjacentId); + } catch (CycleDetectedException $exception) { + if ($exception->isCycleCollected()) { + // There is a complete cycle downstream of the current node. We cannot + // do anything about that anymore. + throw $exception; + } + + if ($optional) { + // The current edge is part of a cycle, but it is optional and the closest + // such edge while backtracking. Break the cycle here by skipping the edge + // and continuing with the next one. + continue; + } + + // We have found a cycle and cannot break it at $edge. Best we can do + // is to backtrack from the current vertex, hoping that somewhere up the + // stack this can be salvaged. + $this->states[$oid] = self::NOT_VISITED; + $exception->addToCycle($this->nodes[$oid]); + + throw $exception; + } + } + + // We have traversed all edges and visited all other nodes reachable from here. + // So we're done with this vertex as well. + + $this->states[$oid] = self::VISITED; + $this->sortResult[] = $this->nodes[$oid]; + } +} 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 @@ + */ + private array $cycle; + + /** + * Do we have the complete cycle collected? + */ + private bool $cycleCollected = false; + + public function __construct(private readonly object $startNode) + { + 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.'); + + $this->cycle = [$startNode]; + } + + /** @return list */ + public function getCycle(): array + { + return $this->cycle; + } + + public function addToCycle(object $node): void + { + array_unshift($this->cycle, $node); + + if ($node === $this->startNode) { + $this->cycleCollected = true; + } + } + + public function isCycleCollected(): bool + { + return $this->cycleCollected; + } +} 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 @@ + + * @implements Selectable + */ +class LazyCriteriaCollection extends AbstractLazyCollection implements Selectable +{ + private int|null $count = null; + + public function __construct( + protected EntityPersister $entityPersister, + protected Criteria $criteria, + ) { + } + + /** + * Do an efficient count on the collection + */ + public function count(): int + { + if ($this->isInitialized()) { + return $this->collection->count(); + } + + // Return cached result in case count query was already executed + if ($this->count !== null) { + return $this->count; + } + + return $this->count = $this->entityPersister->count($this->criteria); + } + + /** + * check if collection is empty without loading it + */ + public function isEmpty(): bool + { + if ($this->isInitialized()) { + return $this->collection->isEmpty(); + } + + return ! $this->count(); + } + + /** + * Do an optimized search of an element + * + * @param mixed $element The element to search for. + * + * @return bool TRUE if the collection contains $element, FALSE otherwise. + */ + public function contains(mixed $element): bool + { + if ($this->isInitialized()) { + return $this->collection->contains($element); + } + + return $this->entityPersister->exists($element, $this->criteria); + } + + /** @return ReadableCollection&Selectable */ + public function matching(Criteria $criteria): ReadableCollection&Selectable + { + $this->initialize(); + assert($this->collection instanceof Selectable); + + return $this->collection->matching($criteria); + } + + protected function doInitialize(): void + { + $elements = $this->entityPersister->loadCriteria($this->criteria); + $this->collection = new ArrayCollection($elements); + } +} 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 @@ +fieldMappings[$fieldName]->columnName; + } + + public function getTableName(ClassMetadata $class, AbstractPlatform $platform): string + { + return $class->table['name']; + } + + /** + * {@inheritDoc} + */ + public function getSequenceName(array $definition, ClassMetadata $class, AbstractPlatform $platform): string + { + return $definition['sequenceName']; + } + + public function getJoinColumnName(JoinColumnMapping $joinColumn, ClassMetadata $class, AbstractPlatform $platform): string + { + return $joinColumn->name; + } + + public function getReferencedJoinColumnName( + JoinColumnMapping $joinColumn, + ClassMetadata $class, + AbstractPlatform $platform, + ): string { + return $joinColumn->referencedColumnName; + } + + public function getJoinTableName( + ManyToManyOwningSideMapping $association, + ClassMetadata $class, + AbstractPlatform $platform, + ): string { + return $association->joinTable->name; + } + + /** + * {@inheritDoc} + */ + public function getIdentifierColumnNames(ClassMetadata $class, AbstractPlatform $platform): array + { + return $class->identifier; + } + + public function getColumnAlias( + string $columnName, + int $counter, + AbstractPlatform $platform, + ClassMetadata|null $class = null, + ): string { + return $this->getSQLResultCasing($platform, $columnName . '_' . $counter); + } +} 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 @@ +$offset); + } + + /** @param string $offset */ + public function offsetGet(mixed $offset): mixed + { + Deprecation::trigger( + 'doctrine/orm', + 'https://github.com/doctrine/orm/pull/11211', + 'Using ArrayAccess on %s is deprecated and will not be possible in Doctrine ORM 4.0. Use the corresponding property instead.', + static::class, + ); + + if (! property_exists($this, $offset)) { + throw new InvalidArgumentException('Undefined property: ' . $offset); + } + + return $this->$offset; + } + + /** @param string $offset */ + public function offsetSet(mixed $offset, mixed $value): void + { + Deprecation::trigger( + 'doctrine/orm', + 'https://github.com/doctrine/orm/pull/11211', + 'Using ArrayAccess on %s is deprecated and will not be possible in Doctrine ORM 4.0. Use the corresponding property instead.', + static::class, + ); + + $this->$offset = $value; + } + + /** @param string $offset */ + public function offsetUnset(mixed $offset): void + { + Deprecation::trigger( + 'doctrine/orm', + 'https://github.com/doctrine/orm/pull/11211', + 'Using ArrayAccess on %s is deprecated and will not be possible in Doctrine ORM 4.0. Use the corresponding property instead.', + static::class, + ); + + $this->$offset = null; + } +} 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 @@ + */ +abstract class AssociationMapping implements ArrayAccess +{ + /** + * The names of persistence operations to cascade on the association. + * + * @var list<'persist'|'remove'|'detach'|'refresh'|'all'> + */ + public array $cascade = []; + + /** + * The fetching strategy to use for the association, usually defaults to FETCH_LAZY. + * + * @var ClassMetadata::FETCH_*|null + */ + public int|null $fetch = null; + + /** + * This is set when the association is inherited by this class from another + * (inheritance) parent entity class. The value is the FQCN of the + * topmost entity class that contains this association. (If there are + * transient classes in the class hierarchy, these are ignored, so the + * class property may in fact come from a class further up in the PHP class + * hierarchy.) To-many associations initially declared in mapped + * superclasses are not considered 'inherited' in the nearest + * entity subclasses. + * + * @var class-string|null + */ + public string|null $inherited = null; + + /** + * This is set when the association does not appear in the current class + * for the first time, but is initially declared in another parent + * entity or mapped superclass. The value is the FQCN of the + * topmost non-transient class that contains association information for + * this relationship. + * + * @var class-string|null + */ + public string|null $declared = null; + + public array|null $cache = null; + + public bool|null $id = null; + + public bool|null $isOnDeleteCascade = null; + + /** @var class-string|null */ + public string|null $originalClass = null; + + public string|null $originalField = null; + + public bool $orphanRemoval = false; + + public bool|null $unique = null; + + /** + * @param string $fieldName The name of the field in the entity + * the association is mapped to. + * @param class-string $sourceEntity The class name of the source entity. + * In the case of to-many associations + * initially present in mapped + * superclasses, the nearest + * entity subclasses will be + * considered the respective source + * entities. + * @param class-string $targetEntity The class name of the target entity. + * If it is fully-qualified it is used as + * is. If it is a simple, unqualified + * class name the namespace is assumed to + * be the same as the namespace of the + * source entity. + */ + final public function __construct( + public readonly string $fieldName, + public string $sourceEntity, + public readonly string $targetEntity, + ) { + } + + /** + * @param mixed[] $mappingArray + * @psalm-param array{ + * fieldName: string, + * sourceEntity: class-string, + * targetEntity: class-string, + * cascade?: list<'persist'|'remove'|'detach'|'refresh'|'all'>, + * fetch?: ClassMetadata::FETCH_*|null, + * inherited?: class-string|null, + * declared?: class-string|null, + * cache?: array|null, + * id?: bool|null, + * isOnDeleteCascade?: bool|null, + * originalClass?: class-string|null, + * originalField?: string|null, + * orphanRemoval?: bool, + * unique?: bool|null, + * joinTable?: mixed[]|null, + * type?: int, + * isOwningSide: bool, + * } $mappingArray + */ + public static function fromMappingArray(array $mappingArray): static + { + unset($mappingArray['isOwningSide'], $mappingArray['type']); + $mapping = new static( + $mappingArray['fieldName'], + $mappingArray['sourceEntity'], + $mappingArray['targetEntity'], + ); + unset($mappingArray['fieldName'], $mappingArray['sourceEntity'], $mappingArray['targetEntity']); + + foreach ($mappingArray as $key => $value) { + if ($key === 'joinTable') { + assert($mapping instanceof ManyToManyAssociationMapping); + + if ($value === [] || $value === null) { + continue; + } + + assert($mapping instanceof ManyToManyOwningSideMapping); + + $mapping->joinTable = JoinTableMapping::fromMappingArray($value); + + continue; + } + + if (property_exists($mapping, $key)) { + $mapping->$key = $value; + } else { + throw new OutOfRangeException('Unknown property ' . $key . ' on class ' . static::class); + } + } + + return $mapping; + } + + /** + * @psalm-assert-if-true OwningSideMapping $this + * @psalm-assert-if-false InverseSideMapping $this + */ + final public function isOwningSide(): bool + { + return $this instanceof OwningSideMapping; + } + + /** @psalm-assert-if-true ToOneAssociationMapping $this */ + final public function isToOne(): bool + { + return $this instanceof ToOneAssociationMapping; + } + + /** @psalm-assert-if-true ToManyAssociationMapping $this */ + final public function isToMany(): bool + { + return $this instanceof ToManyAssociationMapping; + } + + /** @psalm-assert-if-true OneToOneOwningSideMapping $this */ + final public function isOneToOneOwningSide(): bool + { + return $this->isOneToOne() && $this->isOwningSide(); + } + + /** @psalm-assert-if-true OneToOneOwningSideMapping|ManyToOneAssociationMapping $this */ + final public function isToOneOwningSide(): bool + { + return $this->isToOne() && $this->isOwningSide(); + } + + /** @psalm-assert-if-true ManyToManyOwningSideMapping $this */ + final public function isManyToManyOwningSide(): bool + { + return $this instanceof ManyToManyOwningSideMapping; + } + + /** @psalm-assert-if-true OneToOneAssociationMapping $this */ + final public function isOneToOne(): bool + { + return $this instanceof OneToOneAssociationMapping; + } + + /** @psalm-assert-if-true OneToManyAssociationMapping $this */ + final public function isOneToMany(): bool + { + return $this instanceof OneToManyAssociationMapping; + } + + /** @psalm-assert-if-true ManyToOneAssociationMapping $this */ + final public function isManyToOne(): bool + { + return $this instanceof ManyToOneAssociationMapping; + } + + /** @psalm-assert-if-true ManyToManyAssociationMapping $this */ + final public function isManyToMany(): bool + { + return $this instanceof ManyToManyAssociationMapping; + } + + /** @psalm-assert-if-true ToManyAssociationMapping $this */ + final public function isOrdered(): bool + { + return $this->isToMany() && $this->orderBy() !== []; + } + + /** @psalm-assert-if-true ToManyAssociationMapping $this */ + public function isIndexed(): bool + { + return false; + } + + final public function type(): int + { + return match (true) { + $this instanceof OneToOneAssociationMapping => ClassMetadata::ONE_TO_ONE, + $this instanceof OneToManyAssociationMapping => ClassMetadata::ONE_TO_MANY, + $this instanceof ManyToOneAssociationMapping => ClassMetadata::MANY_TO_ONE, + $this instanceof ManyToManyAssociationMapping => ClassMetadata::MANY_TO_MANY, + default => throw new Exception('Cannot determine type for ' . static::class), + }; + } + + /** @param string $offset */ + public function offsetExists(mixed $offset): bool + { + return isset($this->$offset) || in_array($offset, ['isOwningSide', 'type'], true); + } + + final public function offsetGet(mixed $offset): mixed + { + return match ($offset) { + 'isOwningSide' => $this->isOwningSide(), + 'type' => $this->type(), + 'isCascadeRemove' => $this->isCascadeRemove(), + 'isCascadePersist' => $this->isCascadePersist(), + 'isCascadeRefresh' => $this->isCascadeRefresh(), + 'isCascadeDetach' => $this->isCascadeDetach(), + default => property_exists($this, $offset) ? $this->$offset : throw new OutOfRangeException(sprintf( + 'Unknown property "%s" on class %s', + $offset, + static::class, + )), + }; + } + + public function offsetSet(mixed $offset, mixed $value): void + { + assert($offset !== null); + if (! property_exists($this, $offset)) { + throw new OutOfRangeException(sprintf( + 'Unknown property "%s" on class %s', + $offset, + static::class, + )); + } + + if ($offset === 'joinTable') { + $value = JoinTableMapping::fromMappingArray($value); + } + + $this->$offset = $value; + } + + /** @param string $offset */ + public function offsetUnset(mixed $offset): void + { + if (! property_exists($this, $offset)) { + throw new OutOfRangeException(sprintf( + 'Unknown property "%s" on class %s', + $offset, + static::class, + )); + } + + $this->$offset = null; + } + + final public function isCascadeRemove(): bool + { + return in_array('remove', $this->cascade, true); + } + + final public function isCascadePersist(): bool + { + return in_array('persist', $this->cascade, true); + } + + final public function isCascadeRefresh(): bool + { + return in_array('refresh', $this->cascade, true); + } + + final public function isCascadeDetach(): bool + { + return in_array('detach', $this->cascade, true); + } + + /** @return array */ + public function toArray(): array + { + $array = (array) $this; + + $array['isOwningSide'] = $this->isOwningSide(); + $array['type'] = $this->type(); + + return $array; + } + + /** @return list */ + public function __sleep(): array + { + $serialized = ['fieldName', 'sourceEntity', 'targetEntity']; + + if (count($this->cascade) > 0) { + $serialized[] = 'cascade'; + } + + foreach ( + [ + 'fetch', + 'inherited', + 'declared', + 'cache', + 'originalClass', + 'originalField', + ] as $stringOrArrayProperty + ) { + if ($this->$stringOrArrayProperty !== null) { + $serialized[] = $stringOrArrayProperty; + } + } + + foreach (['id', 'orphanRemoval', 'isOnDeleteCascade', 'unique'] as $boolProperty) { + if ($this->$boolProperty) { + $serialized[] = $boolProperty; + } + } + + return $serialized; + } +} 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 @@ +|null + */ + public readonly array|null $joinColumns; + + /** + * The join column that is being mapped to the persistent attribute. + * + * @var array|null + */ + public readonly array|null $inverseJoinColumns; + + /** + * @param string $name The name of the relationship property whose mapping is being overridden. + * @param JoinColumn|array $joinColumns + * @param JoinColumn|array $inverseJoinColumns + * @param JoinTable|null $joinTable The join table that maps the relationship. + * @param string|null $inversedBy The name of the association-field on the inverse-side. + * @psalm-param 'LAZY'|'EAGER'|'EXTRA_LAZY'|null $fetch + */ + public function __construct( + public readonly string $name, + array|JoinColumn|null $joinColumns = null, + array|JoinColumn|null $inverseJoinColumns = null, + public readonly JoinTable|null $joinTable = null, + public readonly string|null $inversedBy = null, + public readonly string|null $fetch = null, + ) { + if ($joinColumns instanceof JoinColumn) { + $joinColumns = [$joinColumns]; + } + + if ($inverseJoinColumns instanceof JoinColumn) { + $inverseJoinColumns = [$inverseJoinColumns]; + } + + $this->joinColumns = $joinColumns; + $this->inverseJoinColumns = $inverseJoinColumns; + } +} 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 @@ + + */ + public readonly array $overrides; + + /** @param array|AssociationOverride $overrides */ + public function __construct(array|AssociationOverride $overrides) + { + if (! is_array($overrides)) { + $overrides = [$overrides]; + } + + foreach ($overrides as $override) { + if (! ($override instanceof AssociationOverride)) { + throw MappingException::invalidOverrideType('AssociationOverride', $override); + } + } + + $this->overrides = array_values($overrides); + } +} 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 @@ + + */ + public readonly array $overrides; + + /** @param array|AttributeOverride $overrides */ + public function __construct(array|AttributeOverride $overrides) + { + if (! is_array($overrides)) { + $overrides = [$overrides]; + } + + foreach ($overrides as $override) { + if (! ($override instanceof AttributeOverride)) { + throw MappingException::invalidOverrideType('AttributeOverride', $override); + } + } + + $this->overrides = array_values($overrides); + } +} 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 @@ +mapping['mappedBy'] = $fieldName; + + return $this; + } + + /** @return $this */ + public function inversedBy(string $fieldName): static + { + $this->mapping['inversedBy'] = $fieldName; + + return $this; + } + + /** @return $this */ + public function cascadeAll(): static + { + $this->mapping['cascade'] = ['ALL']; + + return $this; + } + + /** @return $this */ + public function cascadePersist(): static + { + $this->mapping['cascade'][] = 'persist'; + + return $this; + } + + /** @return $this */ + public function cascadeRemove(): static + { + $this->mapping['cascade'][] = 'remove'; + + return $this; + } + + /** @return $this */ + public function cascadeDetach(): static + { + $this->mapping['cascade'][] = 'detach'; + + return $this; + } + + /** @return $this */ + public function cascadeRefresh(): static + { + $this->mapping['cascade'][] = 'refresh'; + + return $this; + } + + /** @return $this */ + public function fetchExtraLazy(): static + { + $this->mapping['fetch'] = ClassMetadata::FETCH_EXTRA_LAZY; + + return $this; + } + + /** @return $this */ + public function fetchEager(): static + { + $this->mapping['fetch'] = ClassMetadata::FETCH_EAGER; + + return $this; + } + + /** @return $this */ + public function fetchLazy(): static + { + $this->mapping['fetch'] = ClassMetadata::FETCH_LAZY; + + return $this; + } + + /** + * Add Join Columns. + * + * @return $this + */ + public function addJoinColumn( + string $columnName, + string $referencedColumnName, + bool $nullable = true, + bool $unique = false, + string|null $onDelete = null, + string|null $columnDef = null, + ): static { + $this->joinColumns[] = [ + 'name' => $columnName, + 'referencedColumnName' => $referencedColumnName, + 'nullable' => $nullable, + 'unique' => $unique, + 'onDelete' => $onDelete, + 'columnDefinition' => $columnDef, + ]; + + return $this; + } + + /** + * Sets field as primary key. + * + * @return $this + */ + public function makePrimaryKey(): static + { + $this->mapping['id'] = true; + + return $this; + } + + /** + * Removes orphan entities when detached from their parent. + * + * @return $this + */ + public function orphanRemoval(): static + { + $this->mapping['orphanRemoval'] = true; + + return $this; + } + + /** @throws InvalidArgumentException */ + public function build(): ClassMetadataBuilder + { + $mapping = $this->mapping; + if ($this->joinColumns) { + $mapping['joinColumns'] = $this->joinColumns; + } + + $cm = $this->builder->getClassMetadata(); + if ($this->type === ClassMetadata::MANY_TO_ONE) { + $cm->mapManyToOne($mapping); + } elseif ($this->type === ClassMetadata::ONE_TO_ONE) { + $cm->mapOneToOne($mapping); + } else { + throw new InvalidArgumentException('Type should be a ToOne Association here'); + } + + return $this->builder; + } +} 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 @@ +cm; + } + + /** + * Marks the class as mapped superclass. + * + * @return $this + */ + public function setMappedSuperClass(): static + { + $this->cm->isMappedSuperclass = true; + $this->cm->isEmbeddedClass = false; + + return $this; + } + + /** + * Marks the class as embeddable. + * + * @return $this + */ + public function setEmbeddable(): static + { + $this->cm->isEmbeddedClass = true; + $this->cm->isMappedSuperclass = false; + + return $this; + } + + /** + * Adds and embedded class + * + * @param class-string $class + * + * @return $this + */ + public function addEmbedded(string $fieldName, string $class, string|false|null $columnPrefix = null): static + { + $this->cm->mapEmbedded( + [ + 'fieldName' => $fieldName, + 'class' => $class, + 'columnPrefix' => $columnPrefix, + ], + ); + + return $this; + } + + /** + * Sets custom Repository class name. + * + * @return $this + */ + public function setCustomRepositoryClass(string $repositoryClassName): static + { + $this->cm->setCustomRepositoryClass($repositoryClassName); + + return $this; + } + + /** + * Marks class read only. + * + * @return $this + */ + public function setReadOnly(): static + { + $this->cm->markReadOnly(); + + return $this; + } + + /** + * Sets the table name. + * + * @return $this + */ + public function setTable(string $name): static + { + $this->cm->setPrimaryTable(['name' => $name]); + + return $this; + } + + /** + * Adds Index. + * + * @psalm-param list $columns + * + * @return $this + */ + public function addIndex(array $columns, string $name): static + { + if (! isset($this->cm->table['indexes'])) { + $this->cm->table['indexes'] = []; + } + + $this->cm->table['indexes'][$name] = ['columns' => $columns]; + + return $this; + } + + /** + * Adds Unique Constraint. + * + * @psalm-param list $columns + * + * @return $this + */ + public function addUniqueConstraint(array $columns, string $name): static + { + if (! isset($this->cm->table['uniqueConstraints'])) { + $this->cm->table['uniqueConstraints'] = []; + } + + $this->cm->table['uniqueConstraints'][$name] = ['columns' => $columns]; + + return $this; + } + + /** + * Sets class as root of a joined table inheritance hierarchy. + * + * @return $this + */ + public function setJoinedTableInheritance(): static + { + $this->cm->setInheritanceType(ClassMetadata::INHERITANCE_TYPE_JOINED); + + return $this; + } + + /** + * Sets class as root of a single table inheritance hierarchy. + * + * @return $this + */ + public function setSingleTableInheritance(): static + { + $this->cm->setInheritanceType(ClassMetadata::INHERITANCE_TYPE_SINGLE_TABLE); + + return $this; + } + + /** + * Sets the discriminator column details. + * + * @psalm-param class-string|null $enumType + * @psalm-param array $options + * + * @return $this + */ + public function setDiscriminatorColumn( + string $name, + string $type = 'string', + int $length = 255, + string|null $columnDefinition = null, + string|null $enumType = null, + array $options = [], + ): static { + $this->cm->setDiscriminatorColumn( + [ + 'name' => $name, + 'type' => $type, + 'length' => $length, + 'columnDefinition' => $columnDefinition, + 'enumType' => $enumType, + 'options' => $options, + ], + ); + + return $this; + } + + /** + * Adds a subclass to this inheritance hierarchy. + * + * @return $this + */ + public function addDiscriminatorMapClass(string $name, string $class): static + { + $this->cm->addDiscriminatorMapClass($name, $class); + + return $this; + } + + /** + * Sets deferred explicit change tracking policy. + * + * @return $this + */ + public function setChangeTrackingPolicyDeferredExplicit(): static + { + $this->cm->setChangeTrackingPolicy(ClassMetadata::CHANGETRACKING_DEFERRED_EXPLICIT); + + return $this; + } + + /** + * Adds lifecycle event. + * + * @return $this + */ + public function addLifecycleEvent(string $methodName, string $event): static + { + $this->cm->addLifecycleCallback($methodName, $event); + + return $this; + } + + /** + * Adds Field. + * + * @psalm-param array $mapping + * + * @return $this + */ + public function addField(string $name, string $type, array $mapping = []): static + { + $mapping['fieldName'] = $name; + $mapping['type'] = $type; + + $this->cm->mapField($mapping); + + return $this; + } + + /** + * Creates a field builder. + */ + public function createField(string $name, string $type): FieldBuilder + { + return new FieldBuilder( + $this, + [ + 'fieldName' => $name, + 'type' => $type, + ], + ); + } + + /** + * Creates an embedded builder. + */ + public function createEmbedded(string $fieldName, string $class): EmbeddedBuilder + { + return new EmbeddedBuilder( + $this, + [ + 'fieldName' => $fieldName, + 'class' => $class, + 'columnPrefix' => null, + ], + ); + } + + /** + * Adds a simple many to one association, optionally with the inversed by field. + */ + public function addManyToOne( + string $name, + string $targetEntity, + string|null $inversedBy = null, + ): ClassMetadataBuilder { + $builder = $this->createManyToOne($name, $targetEntity); + + if ($inversedBy !== null) { + $builder->inversedBy($inversedBy); + } + + return $builder->build(); + } + + /** + * Creates a ManyToOne Association Builder. + * + * Note: This method does not add the association, you have to call build() on the AssociationBuilder. + */ + public function createManyToOne(string $name, string $targetEntity): AssociationBuilder + { + return new AssociationBuilder( + $this, + [ + 'fieldName' => $name, + 'targetEntity' => $targetEntity, + ], + ClassMetadata::MANY_TO_ONE, + ); + } + + /** + * Creates a OneToOne Association Builder. + */ + public function createOneToOne(string $name, string $targetEntity): AssociationBuilder + { + return new AssociationBuilder( + $this, + [ + 'fieldName' => $name, + 'targetEntity' => $targetEntity, + ], + ClassMetadata::ONE_TO_ONE, + ); + } + + /** + * Adds simple inverse one-to-one association. + */ + public function addInverseOneToOne(string $name, string $targetEntity, string $mappedBy): ClassMetadataBuilder + { + $builder = $this->createOneToOne($name, $targetEntity); + $builder->mappedBy($mappedBy); + + return $builder->build(); + } + + /** + * Adds simple owning one-to-one association. + */ + public function addOwningOneToOne( + string $name, + string $targetEntity, + string|null $inversedBy = null, + ): ClassMetadataBuilder { + $builder = $this->createOneToOne($name, $targetEntity); + + if ($inversedBy !== null) { + $builder->inversedBy($inversedBy); + } + + return $builder->build(); + } + + /** + * Creates a ManyToMany Association Builder. + */ + public function createManyToMany(string $name, string $targetEntity): ManyToManyAssociationBuilder + { + return new ManyToManyAssociationBuilder( + $this, + [ + 'fieldName' => $name, + 'targetEntity' => $targetEntity, + ], + ClassMetadata::MANY_TO_MANY, + ); + } + + /** + * Adds a simple owning many to many association. + */ + public function addOwningManyToMany( + string $name, + string $targetEntity, + string|null $inversedBy = null, + ): ClassMetadataBuilder { + $builder = $this->createManyToMany($name, $targetEntity); + + if ($inversedBy !== null) { + $builder->inversedBy($inversedBy); + } + + return $builder->build(); + } + + /** + * Adds a simple inverse many to many association. + */ + public function addInverseManyToMany(string $name, string $targetEntity, string $mappedBy): ClassMetadataBuilder + { + $builder = $this->createManyToMany($name, $targetEntity); + $builder->mappedBy($mappedBy); + + return $builder->build(); + } + + /** + * Creates a one to many association builder. + */ + public function createOneToMany(string $name, string $targetEntity): OneToManyAssociationBuilder + { + return new OneToManyAssociationBuilder( + $this, + [ + 'fieldName' => $name, + 'targetEntity' => $targetEntity, + ], + ClassMetadata::ONE_TO_MANY, + ); + } + + /** + * Adds simple OneToMany association. + */ + public function addOneToMany(string $name, string $targetEntity, string $mappedBy): ClassMetadataBuilder + { + $builder = $this->createOneToMany($name, $targetEntity); + $builder->mappedBy($mappedBy); + + return $builder->build(); + } +} 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 @@ +mapping['columnPrefix'] = $columnPrefix; + + return $this; + } + + /** + * Finalizes this embeddable and attach it to the ClassMetadata. + * + * Without this call an EmbeddedBuilder has no effect on the ClassMetadata. + */ + public function build(): ClassMetadataBuilder + { + $cm = $this->builder->getClassMetadata(); + + $cm->mapEmbedded($this->mapping); + + return $this->builder; + } +} 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 @@ + true, + Events::postRemove => true, + Events::prePersist => true, + Events::postPersist => true, + Events::preUpdate => true, + Events::postUpdate => true, + Events::postLoad => true, + Events::preFlush => true, + ]; + + /** + * Lookup the entity class to find methods that match to event lifecycle names + * + * @param ClassMetadata $metadata The entity metadata. + * @param string $className The listener class name. + * + * @throws MappingException When the listener class not found. + */ + public static function bindEntityListener(ClassMetadata $metadata, string $className): void + { + $class = $metadata->fullyQualifiedClassName($className); + + if (! class_exists($class)) { + throw MappingException::entityListenerClassNotFound($class, $className); + } + + foreach (get_class_methods($class) as $method) { + if (! isset(self::EVENTS[$method])) { + continue; + } + + $metadata->addEntityListener($method, $class, $method); + } + } +} 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 @@ +mapping['length'] = $length; + + return $this; + } + + /** + * Sets nullable. + * + * @return $this + */ + public function nullable(bool $flag = true): static + { + $this->mapping['nullable'] = $flag; + + return $this; + } + + /** + * Sets Unique. + * + * @return $this + */ + public function unique(bool $flag = true): static + { + $this->mapping['unique'] = $flag; + + return $this; + } + + /** + * Sets column name. + * + * @return $this + */ + public function columnName(string $name): static + { + $this->mapping['columnName'] = $name; + + return $this; + } + + /** + * Sets Precision. + * + * @return $this + */ + public function precision(int $p): static + { + $this->mapping['precision'] = $p; + + return $this; + } + + /** + * Sets insertable. + * + * @return $this + */ + public function insertable(bool $flag = true): self + { + if (! $flag) { + $this->mapping['notInsertable'] = true; + } + + return $this; + } + + /** + * Sets updatable. + * + * @return $this + */ + public function updatable(bool $flag = true): self + { + if (! $flag) { + $this->mapping['notUpdatable'] = true; + } + + return $this; + } + + /** + * Sets scale. + * + * @return $this + */ + public function scale(int $s): static + { + $this->mapping['scale'] = $s; + + return $this; + } + + /** + * Sets field as primary key. + * + * @return $this + */ + public function makePrimaryKey(): static + { + $this->mapping['id'] = true; + + return $this; + } + + /** + * Sets an option. + * + * @return $this + */ + public function option(string $name, mixed $value): static + { + $this->mapping['options'][$name] = $value; + + return $this; + } + + /** @return $this */ + public function generatedValue(string $strategy = 'AUTO'): static + { + $this->generatedValue = $strategy; + + return $this; + } + + /** + * Sets field versioned. + * + * @return $this + */ + public function isVersionField(): static + { + $this->version = true; + + return $this; + } + + /** + * Sets Sequence Generator. + * + * @return $this + */ + public function setSequenceGenerator(string $sequenceName, int $allocationSize = 1, int $initialValue = 1): static + { + $this->sequenceDef = [ + 'sequenceName' => $sequenceName, + 'allocationSize' => $allocationSize, + 'initialValue' => $initialValue, + ]; + + return $this; + } + + /** + * Sets column definition. + * + * @return $this + */ + public function columnDefinition(string $def): static + { + $this->mapping['columnDefinition'] = $def; + + return $this; + } + + /** + * Set the FQCN of the custom ID generator. + * This class must extend \Doctrine\ORM\Id\AbstractIdGenerator. + * + * @return $this + */ + public function setCustomIdGenerator(string $customIdGenerator): static + { + $this->customIdGenerator = $customIdGenerator; + + return $this; + } + + /** + * Finalizes this field and attach it to the ClassMetadata. + * + * Without this call a FieldBuilder has no effect on the ClassMetadata. + */ + public function build(): ClassMetadataBuilder + { + $cm = $this->builder->getClassMetadata(); + if ($this->generatedValue) { + $cm->setIdGeneratorType(constant('Doctrine\ORM\Mapping\ClassMetadata::GENERATOR_TYPE_' . $this->generatedValue)); + } + + if ($this->version) { + $cm->setVersionMapping($this->mapping); + } + + $cm->mapField($this->mapping); + if ($this->sequenceDef) { + $cm->setSequenceGeneratorDefinition($this->sequenceDef); + } + + if ($this->customIdGenerator) { + $cm->setCustomGeneratorDefinition(['class' => $this->customIdGenerator]); + } + + return $this->builder; + } +} 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 @@ +joinTableName = $name; + + return $this; + } + + /** + * Adds Inverse Join Columns. + * + * @return $this + */ + public function addInverseJoinColumn( + string $columnName, + string $referencedColumnName, + bool $nullable = true, + bool $unique = false, + string|null $onDelete = null, + string|null $columnDef = null, + ): static { + $this->inverseJoinColumns[] = [ + 'name' => $columnName, + 'referencedColumnName' => $referencedColumnName, + 'nullable' => $nullable, + 'unique' => $unique, + 'onDelete' => $onDelete, + 'columnDefinition' => $columnDef, + ]; + + return $this; + } + + public function build(): ClassMetadataBuilder + { + $mapping = $this->mapping; + $mapping['joinTable'] = []; + if ($this->joinColumns) { + $mapping['joinTable']['joinColumns'] = $this->joinColumns; + } + + if ($this->inverseJoinColumns) { + $mapping['joinTable']['inverseJoinColumns'] = $this->inverseJoinColumns; + } + + if ($this->joinTableName) { + $mapping['joinTable']['name'] = $this->joinTableName; + } + + $cm = $this->builder->getClassMetadata(); + $cm->mapManyToMany($mapping); + + return $this->builder; + } +} 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 @@ + $fieldNames + * + * @return $this + */ + public function setOrderBy(array $fieldNames): static + { + $this->mapping['orderBy'] = $fieldNames; + + return $this; + } + + /** @return $this */ + public function setIndexBy(string $fieldName): static + { + $this->mapping['indexBy'] = $fieldName; + + return $this; + } + + public function build(): ClassMetadataBuilder + { + $mapping = $this->mapping; + if ($this->joinColumns) { + $mapping['joinColumns'] = $this->joinColumns; + } + + $cm = $this->builder->getClassMetadata(); + $cm->mapOneToMany($mapping); + + return $this->builder; + } +} 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 @@ + $typedFieldMappers */ + private readonly array $typedFieldMappers; + + public function __construct(TypedFieldMapper ...$typedFieldMappers) + { + self::validateVariadicParameter($typedFieldMappers); + + $this->typedFieldMappers = $typedFieldMappers; + } + + /** + * {@inheritDoc} + */ + public function validateAndComplete(array $mapping, ReflectionProperty $field): array + { + foreach ($this->typedFieldMappers as $typedFieldMapper) { + $mapping = $typedFieldMapper->validateAndComplete($mapping, $field); + } + + return $mapping; + } +} 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 @@ +ClassMetadata instance holds all the object-relational mapping metadata + * of an entity and its associations. + * + * Once populated, ClassMetadata instances are usually cached in a serialized form. + * + * IMPORTANT NOTE: + * + * The fields of this class are only public for 2 reasons: + * 1) To allow fast READ access. + * 2) To drastically reduce the size of a serialized instance (private/protected members + * get the whole class name, namespace inclusive, prepended to every property in + * the serialized representation). + * + * @psalm-type ConcreteAssociationMapping = OneToOneOwningSideMapping|OneToOneInverseSideMapping|ManyToOneAssociationMapping|OneToManyAssociationMapping|ManyToManyOwningSideMapping|ManyToManyInverseSideMapping + * @template-covariant T of object + * @template-implements PersistenceClassMetadata + */ +class ClassMetadata implements PersistenceClassMetadata, Stringable +{ + /* The inheritance mapping types */ + /** + * NONE means the class does not participate in an inheritance hierarchy + * and therefore does not need an inheritance mapping type. + */ + public const INHERITANCE_TYPE_NONE = 1; + + /** + * JOINED means the class will be persisted according to the rules of + * Class Table Inheritance. + */ + public const INHERITANCE_TYPE_JOINED = 2; + + /** + * SINGLE_TABLE means the class will be persisted according to the rules of + * Single Table Inheritance. + */ + public const INHERITANCE_TYPE_SINGLE_TABLE = 3; + + /* The Id generator types. */ + /** + * AUTO means the generator type will depend on what the used platform prefers. + * Offers full portability. + */ + public const GENERATOR_TYPE_AUTO = 1; + + /** + * SEQUENCE means a separate sequence object will be used. Platforms that do + * not have native sequence support may emulate it. Full portability is currently + * not guaranteed. + */ + public const GENERATOR_TYPE_SEQUENCE = 2; + + /** + * IDENTITY means an identity column is used for id generation. The database + * will fill in the id column on insertion. Platforms that do not support + * native identity columns may emulate them. Full portability is currently + * not guaranteed. + */ + public const GENERATOR_TYPE_IDENTITY = 4; + + /** + * NONE means the class does not have a generated id. That means the class + * must have a natural, manually assigned id. + */ + public const GENERATOR_TYPE_NONE = 5; + + /** + * CUSTOM means that customer will use own ID generator that supposedly work + */ + public const GENERATOR_TYPE_CUSTOM = 7; + + /** + * DEFERRED_IMPLICIT means that changes of entities are calculated at commit-time + * by doing a property-by-property comparison with the original data. This will + * be done for all entities that are in MANAGED state at commit-time. + * + * This is the default change tracking policy. + */ + public const CHANGETRACKING_DEFERRED_IMPLICIT = 1; + + /** + * DEFERRED_EXPLICIT means that changes of entities are calculated at commit-time + * by doing a property-by-property comparison with the original data. This will + * be done only for entities that were explicitly saved (through persist() or a cascade). + */ + public const CHANGETRACKING_DEFERRED_EXPLICIT = 2; + + /** + * Specifies that an association is to be fetched when it is first accessed. + */ + public const FETCH_LAZY = 2; + + /** + * Specifies that an association is to be fetched when the owner of the + * association is fetched. + */ + public const FETCH_EAGER = 3; + + /** + * Specifies that an association is to be fetched lazy (on first access) and that + * commands such as Collection#count, Collection#slice are issued directly against + * the database if the collection is not yet initialized. + */ + public const FETCH_EXTRA_LAZY = 4; + + /** + * Identifies a one-to-one association. + */ + public const ONE_TO_ONE = 1; + + /** + * Identifies a many-to-one association. + */ + public const MANY_TO_ONE = 2; + + /** + * Identifies a one-to-many association. + */ + public const ONE_TO_MANY = 4; + + /** + * Identifies a many-to-many association. + */ + public const MANY_TO_MANY = 8; + + /** + * Combined bitmask for to-one (single-valued) associations. + */ + public const TO_ONE = 3; + + /** + * Combined bitmask for to-many (collection-valued) associations. + */ + public const TO_MANY = 12; + + /** + * ReadOnly cache can do reads, inserts and deletes, cannot perform updates or employ any locks, + */ + public const CACHE_USAGE_READ_ONLY = 1; + + /** + * Nonstrict Read Write Cache doesn’t employ any locks but can do inserts, update and deletes. + */ + public const CACHE_USAGE_NONSTRICT_READ_WRITE = 2; + + /** + * Read Write Attempts to lock the entity before update/delete. + */ + public const CACHE_USAGE_READ_WRITE = 3; + + /** + * The value of this column is never generated by the database. + */ + public const GENERATED_NEVER = 0; + + /** + * The value of this column is generated by the database on INSERT, but not on UPDATE. + */ + public const GENERATED_INSERT = 1; + + /** + * The value of this column is generated by the database on both INSERT and UDPATE statements. + */ + public const GENERATED_ALWAYS = 2; + + /** + * READ-ONLY: The namespace the entity class is contained in. + * + * @todo Not really needed. Usage could be localized. + */ + public string|null $namespace = null; + + /** + * READ-ONLY: The name of the entity class that is at the root of the mapped entity inheritance + * hierarchy. If the entity is not part of a mapped inheritance hierarchy this is the same + * as {@link $name}. + * + * @psalm-var class-string + */ + public string $rootEntityName; + + /** + * READ-ONLY: The definition of custom generator. Only used for CUSTOM + * generator type + * + * The definition has the following structure: + * + * array( + * 'class' => 'ClassName', + * ) + * + * + * @todo Merge with tableGeneratorDefinition into generic generatorDefinition + * @var array|null + */ + public array|null $customGeneratorDefinition = null; + + /** + * The name of the custom repository class used for the entity class. + * (Optional). + * + * @psalm-var ?class-string + */ + public string|null $customRepositoryClassName = null; + + /** + * READ-ONLY: Whether this class describes the mapping of a mapped superclass. + */ + public bool $isMappedSuperclass = false; + + /** + * READ-ONLY: Whether this class describes the mapping of an embeddable class. + */ + public bool $isEmbeddedClass = false; + + /** + * READ-ONLY: The names of the parent entity classes (ancestors), starting with the + * nearest one and ending with the root entity class. + * + * @psalm-var list + */ + public array $parentClasses = []; + + /** + * READ-ONLY: For classes in inheritance mapping hierarchies, this field contains the names of all + * entity subclasses of this class. These may also be abstract classes. + * + * This list is used, for example, to enumerate all necessary tables in JTI when querying for root + * or subclass entities, or to gather all fields comprised in an entity inheritance tree. + * + * For classes that do not use STI/JTI, this list is empty. + * + * Implementation note: + * + * In PHP, there is no general way to discover all subclasses of a given class at runtime. For that + * reason, the list of classes given in the discriminator map at the root entity is considered + * authoritative. The discriminator map must contain all concrete classes that can + * appear in the particular inheritance hierarchy tree. Since there can be no instances of abstract + * entity classes, users are not required to list such classes with a discriminator value. + * + * The possibly remaining "gaps" for abstract entity classes are filled after the class metadata for the + * root entity has been loaded. + * + * For subclasses of such root entities, the list can be reused/passed downwards, it only needs to + * be filtered accordingly (only keep remaining subclasses) + * + * @psalm-var list + */ + public array $subClasses = []; + + /** + * READ-ONLY: The names of all embedded classes based on properties. + * + * @psalm-var array + */ + public array $embeddedClasses = []; + + /** + * READ-ONLY: The field names of all fields that are part of the identifier/primary key + * of the mapped entity class. + * + * @psalm-var list + */ + public array $identifier = []; + + /** + * READ-ONLY: The inheritance mapping type used by the class. + * + * @psalm-var self::INHERITANCE_TYPE_* + */ + public int $inheritanceType = self::INHERITANCE_TYPE_NONE; + + /** + * READ-ONLY: The Id generator type used by the class. + * + * @psalm-var self::GENERATOR_TYPE_* + */ + public int $generatorType = self::GENERATOR_TYPE_NONE; + + /** + * READ-ONLY: The field mappings of the class. + * Keys are field names and values are FieldMapping instances + * + * @var array + */ + public array $fieldMappings = []; + + /** + * READ-ONLY: An array of field names. Used to look up field names from column names. + * Keys are column names and values are field names. + * + * @psalm-var array + */ + public array $fieldNames = []; + + /** + * READ-ONLY: A map of field names to column names. Keys are field names and values column names. + * Used to look up column names from field names. + * This is the reverse lookup map of $_fieldNames. + * + * @deprecated 3.0 Remove this. + * + * @var mixed[] + */ + public array $columnNames = []; + + /** + * READ-ONLY: The discriminator value of this class. + * + * This does only apply to the JOINED and SINGLE_TABLE inheritance mapping strategies + * where a discriminator column is used. + * + * @see discriminatorColumn + */ + public mixed $discriminatorValue = null; + + /** + * READ-ONLY: The discriminator map of all mapped classes in the hierarchy. + * + * This does only apply to the JOINED and SINGLE_TABLE inheritance mapping strategies + * where a discriminator column is used. + * + * @see discriminatorColumn + * + * @var array + * + * @psalm-var array + */ + public array $discriminatorMap = []; + + /** + * READ-ONLY: The definition of the discriminator column used in JOINED and SINGLE_TABLE + * inheritance mappings. + */ + public DiscriminatorColumnMapping|null $discriminatorColumn = null; + + /** + * READ-ONLY: The primary table definition. The definition is an array with the + * following entries: + * + * name => + * schema => + * indexes => array + * uniqueConstraints => array + * + * @var mixed[] + * @psalm-var array{ + * name: string, + * schema?: string, + * indexes?: array, + * uniqueConstraints?: array, + * options?: array, + * quoted?: bool + * } + */ + public array $table; + + /** + * READ-ONLY: The registered lifecycle callbacks for entities of this class. + * + * @psalm-var array> + */ + public array $lifecycleCallbacks = []; + + /** + * READ-ONLY: The registered entity listeners. + * + * @psalm-var array> + */ + public array $entityListeners = []; + + /** + * READ-ONLY: The association mappings of this class. + * + * A join table definition has the following structure: + *
+     * array(
+     *     'name' => ,
+     *      'joinColumns' => array(),
+     *      'inverseJoinColumns' => array()
+     * )
+     * 
+ * + * @psalm-var array + */ + public array $associationMappings = []; + + /** + * READ-ONLY: Flag indicating whether the identifier/primary key of the class is composite. + */ + public bool $isIdentifierComposite = false; + + /** + * READ-ONLY: Flag indicating whether the identifier/primary key contains at least one foreign key association. + * + * This flag is necessary because some code blocks require special treatment of this cases. + */ + public bool $containsForeignIdentifier = false; + + /** + * READ-ONLY: Flag indicating whether the identifier/primary key contains at least one ENUM type. + * + * This flag is necessary because some code blocks require special treatment of this cases. + */ + public bool $containsEnumIdentifier = false; + + /** + * READ-ONLY: The ID generator used for generating IDs for this class. + * + * @todo Remove! + */ + public AbstractIdGenerator $idGenerator; + + /** + * READ-ONLY: The definition of the sequence generator of this class. Only used for the + * SEQUENCE generation strategy. + * + * The definition has the following structure: + * + * array( + * 'sequenceName' => 'name', + * 'allocationSize' => '20', + * 'initialValue' => '1' + * ) + * + * + * @var array|null + * @psalm-var array{sequenceName: string, allocationSize: string, initialValue: string, quoted?: mixed}|null + * @todo Merge with tableGeneratorDefinition into generic generatorDefinition + */ + public array|null $sequenceGeneratorDefinition = null; + + /** + * READ-ONLY: The policy used for change-tracking on entities of this class. + */ + public int $changeTrackingPolicy = self::CHANGETRACKING_DEFERRED_IMPLICIT; + + /** + * READ-ONLY: A Flag indicating whether one or more columns of this class + * have to be reloaded after insert / update operations. + */ + public bool $requiresFetchAfterChange = false; + + /** + * READ-ONLY: A flag for whether or not instances of this class are to be versioned + * with optimistic locking. + */ + public bool $isVersioned = false; + + /** + * READ-ONLY: The name of the field which is used for versioning in optimistic locking (if any). + */ + public string|null $versionField = null; + + /** @var mixed[]|null */ + public array|null $cache = null; + + /** + * The ReflectionClass instance of the mapped class. + * + * @var ReflectionClass|null + */ + public ReflectionClass|null $reflClass = null; + + /** + * Is this entity marked as "read-only"? + * + * That means it is never considered for change-tracking in the UnitOfWork. It is a very helpful performance + * optimization for entities that are immutable, either in your domain or through the relation database + * (coming from a view, or a history table for example). + */ + public bool $isReadOnly = false; + + /** + * NamingStrategy determining the default column and table names. + */ + protected NamingStrategy $namingStrategy; + + /** + * The ReflectionProperty instances of the mapped class. + * + * @var array + */ + public array $reflFields = []; + + private InstantiatorInterface|null $instantiator = null; + + private readonly TypedFieldMapper $typedFieldMapper; + + /** + * Initializes a new ClassMetadata instance that will hold the object-relational mapping + * metadata of the class with the given name. + * + * @param string $name The name of the entity class the new instance is used for. + * @psalm-param class-string $name + */ + public function __construct(public string $name, NamingStrategy|null $namingStrategy = null, TypedFieldMapper|null $typedFieldMapper = null) + { + $this->rootEntityName = $name; + $this->namingStrategy = $namingStrategy ?? new DefaultNamingStrategy(); + $this->instantiator = new Instantiator(); + $this->typedFieldMapper = $typedFieldMapper ?? new DefaultTypedFieldMapper(); + } + + /** + * Gets the ReflectionProperties of the mapped class. + * + * @return ReflectionProperty[]|null[] An array of ReflectionProperty instances. + * @psalm-return array + */ + public function getReflectionProperties(): array + { + return $this->reflFields; + } + + /** + * Gets a ReflectionProperty for a specific field of the mapped class. + */ + public function getReflectionProperty(string $name): ReflectionProperty|null + { + return $this->reflFields[$name]; + } + + /** + * Gets the ReflectionProperty for the single identifier field. + * + * @throws BadMethodCallException If the class has a composite identifier. + */ + public function getSingleIdReflectionProperty(): ReflectionProperty|null + { + if ($this->isIdentifierComposite) { + throw new BadMethodCallException('Class ' . $this->name . ' has a composite identifier.'); + } + + return $this->reflFields[$this->identifier[0]]; + } + + /** + * Extracts the identifier values of an entity of this class. + * + * For composite identifiers, the identifier values are returned as an array + * with the same order as the field order in {@link identifier}. + * + * @return array + */ + public function getIdentifierValues(object $entity): array + { + if ($this->isIdentifierComposite) { + $id = []; + + foreach ($this->identifier as $idField) { + $value = $this->reflFields[$idField]->getValue($entity); + + if ($value !== null) { + $id[$idField] = $value; + } + } + + return $id; + } + + $id = $this->identifier[0]; + $value = $this->reflFields[$id]->getValue($entity); + + if ($value === null) { + return []; + } + + return [$id => $value]; + } + + /** + * Populates the entity identifier of an entity. + * + * @psalm-param array $id + * + * @todo Rename to assignIdentifier() + */ + public function setIdentifierValues(object $entity, array $id): void + { + foreach ($id as $idField => $idValue) { + $this->reflFields[$idField]->setValue($entity, $idValue); + } + } + + /** + * Sets the specified field to the specified value on the given entity. + */ + public function setFieldValue(object $entity, string $field, mixed $value): void + { + $this->reflFields[$field]->setValue($entity, $value); + } + + /** + * Gets the specified field's value off the given entity. + */ + public function getFieldValue(object $entity, string $field): mixed + { + return $this->reflFields[$field]->getValue($entity); + } + + /** + * Creates a string representation of this instance. + * + * @return string The string representation of this instance. + * + * @todo Construct meaningful string representation. + */ + public function __toString(): string + { + return self::class . '@' . spl_object_id($this); + } + + /** + * Determines which fields get serialized. + * + * It is only serialized what is necessary for best unserialization performance. + * That means any metadata properties that are not set or empty or simply have + * their default value are NOT serialized. + * + * Parts that are also NOT serialized because they can not be properly unserialized: + * - reflClass (ReflectionClass) + * - reflFields (ReflectionProperty array) + * + * @return string[] The names of all the fields that should be serialized. + */ + public function __sleep(): array + { + // This metadata is always serialized/cached. + $serialized = [ + 'associationMappings', + 'columnNames', //TODO: 3.0 Remove this. Can use fieldMappings[$fieldName]['columnName'] + 'fieldMappings', + 'fieldNames', + 'embeddedClasses', + 'identifier', + 'isIdentifierComposite', // TODO: REMOVE + 'name', + 'namespace', // TODO: REMOVE + 'table', + 'rootEntityName', + 'idGenerator', //TODO: Does not really need to be serialized. Could be moved to runtime. + ]; + + // The rest of the metadata is only serialized if necessary. + if ($this->changeTrackingPolicy !== self::CHANGETRACKING_DEFERRED_IMPLICIT) { + $serialized[] = 'changeTrackingPolicy'; + } + + if ($this->customRepositoryClassName) { + $serialized[] = 'customRepositoryClassName'; + } + + if ($this->inheritanceType !== self::INHERITANCE_TYPE_NONE) { + $serialized[] = 'inheritanceType'; + $serialized[] = 'discriminatorColumn'; + $serialized[] = 'discriminatorValue'; + $serialized[] = 'discriminatorMap'; + $serialized[] = 'parentClasses'; + $serialized[] = 'subClasses'; + } + + if ($this->generatorType !== self::GENERATOR_TYPE_NONE) { + $serialized[] = 'generatorType'; + if ($this->generatorType === self::GENERATOR_TYPE_SEQUENCE) { + $serialized[] = 'sequenceGeneratorDefinition'; + } + } + + if ($this->isMappedSuperclass) { + $serialized[] = 'isMappedSuperclass'; + } + + if ($this->isEmbeddedClass) { + $serialized[] = 'isEmbeddedClass'; + } + + if ($this->containsForeignIdentifier) { + $serialized[] = 'containsForeignIdentifier'; + } + + if ($this->containsEnumIdentifier) { + $serialized[] = 'containsEnumIdentifier'; + } + + if ($this->isVersioned) { + $serialized[] = 'isVersioned'; + $serialized[] = 'versionField'; + } + + if ($this->lifecycleCallbacks) { + $serialized[] = 'lifecycleCallbacks'; + } + + if ($this->entityListeners) { + $serialized[] = 'entityListeners'; + } + + if ($this->isReadOnly) { + $serialized[] = 'isReadOnly'; + } + + if ($this->customGeneratorDefinition) { + $serialized[] = 'customGeneratorDefinition'; + } + + if ($this->cache) { + $serialized[] = 'cache'; + } + + if ($this->requiresFetchAfterChange) { + $serialized[] = 'requiresFetchAfterChange'; + } + + return $serialized; + } + + /** + * Creates a new instance of the mapped class, without invoking the constructor. + */ + public function newInstance(): object + { + return $this->instantiator->instantiate($this->name); + } + + /** + * Restores some state that can not be serialized/unserialized. + */ + public function wakeupReflection(ReflectionService $reflService): void + { + // Restore ReflectionClass and properties + $this->reflClass = $reflService->getClass($this->name); + $this->instantiator = $this->instantiator ?: new Instantiator(); + + $parentReflFields = []; + + foreach ($this->embeddedClasses as $property => $embeddedClass) { + if (isset($embeddedClass->declaredField)) { + assert($embeddedClass->originalField !== null); + $childProperty = $this->getAccessibleProperty( + $reflService, + $this->embeddedClasses[$embeddedClass->declaredField]->class, + $embeddedClass->originalField, + ); + assert($childProperty !== null); + $parentReflFields[$property] = new ReflectionEmbeddedProperty( + $parentReflFields[$embeddedClass->declaredField], + $childProperty, + $this->embeddedClasses[$embeddedClass->declaredField]->class, + ); + + continue; + } + + $fieldRefl = $this->getAccessibleProperty( + $reflService, + $embeddedClass->declared ?? $this->name, + $property, + ); + + $parentReflFields[$property] = $fieldRefl; + $this->reflFields[$property] = $fieldRefl; + } + + foreach ($this->fieldMappings as $field => $mapping) { + if (isset($mapping->declaredField) && isset($parentReflFields[$mapping->declaredField])) { + assert($mapping->originalField !== null); + assert($mapping->originalClass !== null); + $childProperty = $this->getAccessibleProperty($reflService, $mapping->originalClass, $mapping->originalField); + assert($childProperty !== null); + + if (isset($mapping->enumType)) { + $childProperty = new EnumReflectionProperty( + $childProperty, + $mapping->enumType, + ); + } + + $this->reflFields[$field] = new ReflectionEmbeddedProperty( + $parentReflFields[$mapping->declaredField], + $childProperty, + $mapping->originalClass, + ); + continue; + } + + $this->reflFields[$field] = isset($mapping->declared) + ? $this->getAccessibleProperty($reflService, $mapping->declared, $field) + : $this->getAccessibleProperty($reflService, $this->name, $field); + + if (isset($mapping->enumType) && $this->reflFields[$field] !== null) { + $this->reflFields[$field] = new EnumReflectionProperty( + $this->reflFields[$field], + $mapping->enumType, + ); + } + } + + foreach ($this->associationMappings as $field => $mapping) { + $this->reflFields[$field] = isset($mapping->declared) + ? $this->getAccessibleProperty($reflService, $mapping->declared, $field) + : $this->getAccessibleProperty($reflService, $this->name, $field); + } + } + + /** + * Initializes a new ClassMetadata instance that will hold the object-relational mapping + * metadata of the class with the given name. + * + * @param ReflectionService $reflService The reflection service. + */ + public function initializeReflection(ReflectionService $reflService): void + { + $this->reflClass = $reflService->getClass($this->name); + $this->namespace = $reflService->getClassNamespace($this->name); + + if ($this->reflClass) { + $this->name = $this->rootEntityName = $this->reflClass->name; + } + + $this->table['name'] = $this->namingStrategy->classToTableName($this->name); + } + + /** + * Validates Identifier. + * + * @throws MappingException + */ + public function validateIdentifier(): void + { + if ($this->isMappedSuperclass || $this->isEmbeddedClass) { + return; + } + + // Verify & complete identifier mapping + if (! $this->identifier) { + throw MappingException::identifierRequired($this->name); + } + + if ($this->usesIdGenerator() && $this->isIdentifierComposite) { + throw MappingException::compositeKeyAssignedIdGeneratorRequired($this->name); + } + } + + /** + * Validates association targets actually exist. + * + * @throws MappingException + */ + public function validateAssociations(): void + { + foreach ($this->associationMappings as $mapping) { + if ( + ! class_exists($mapping->targetEntity) + && ! interface_exists($mapping->targetEntity) + && ! trait_exists($mapping->targetEntity) + ) { + throw MappingException::invalidTargetEntityClass($mapping->targetEntity, $this->name, $mapping->fieldName); + } + } + } + + /** + * Validates lifecycle callbacks. + * + * @throws MappingException + */ + public function validateLifecycleCallbacks(ReflectionService $reflService): void + { + foreach ($this->lifecycleCallbacks as $callbacks) { + foreach ($callbacks as $callbackFuncName) { + if (! $reflService->hasPublicMethod($this->name, $callbackFuncName)) { + throw MappingException::lifecycleCallbackMethodNotFound($this->name, $callbackFuncName); + } + } + } + } + + /** + * {@inheritDoc} + * + * Can return null when using static reflection, in violation of the LSP + */ + public function getReflectionClass(): ReflectionClass|null + { + return $this->reflClass; + } + + /** @psalm-param array{usage?: mixed, region?: mixed} $cache */ + public function enableCache(array $cache): void + { + if (! isset($cache['usage'])) { + $cache['usage'] = self::CACHE_USAGE_READ_ONLY; + } + + if (! isset($cache['region'])) { + $cache['region'] = strtolower(str_replace('\\', '_', $this->rootEntityName)); + } + + $this->cache = $cache; + } + + /** @psalm-param array{usage?: int, region?: string} $cache */ + public function enableAssociationCache(string $fieldName, array $cache): void + { + $this->associationMappings[$fieldName]->cache = $this->getAssociationCacheDefaults($fieldName, $cache); + } + + /** + * @psalm-param array{usage?: int, region?: string|null} $cache + * + * @return int[]|string[] + * @psalm-return array{usage: int, region: string|null} + */ + public function getAssociationCacheDefaults(string $fieldName, array $cache): array + { + if (! isset($cache['usage'])) { + $cache['usage'] = $this->cache['usage'] ?? self::CACHE_USAGE_READ_ONLY; + } + + if (! isset($cache['region'])) { + $cache['region'] = strtolower(str_replace('\\', '_', $this->rootEntityName)) . '__' . $fieldName; + } + + return $cache; + } + + /** + * Sets the change tracking policy used by this class. + */ + public function setChangeTrackingPolicy(int $policy): void + { + $this->changeTrackingPolicy = $policy; + } + + /** + * Whether the change tracking policy of this class is "deferred explicit". + */ + public function isChangeTrackingDeferredExplicit(): bool + { + return $this->changeTrackingPolicy === self::CHANGETRACKING_DEFERRED_EXPLICIT; + } + + /** + * Whether the change tracking policy of this class is "deferred implicit". + */ + public function isChangeTrackingDeferredImplicit(): bool + { + return $this->changeTrackingPolicy === self::CHANGETRACKING_DEFERRED_IMPLICIT; + } + + /** + * Checks whether a field is part of the identifier/primary key field(s). + */ + public function isIdentifier(string $fieldName): bool + { + if (! $this->identifier) { + return false; + } + + if (! $this->isIdentifierComposite) { + return $fieldName === $this->identifier[0]; + } + + return in_array($fieldName, $this->identifier, true); + } + + public function isUniqueField(string $fieldName): bool + { + $mapping = $this->getFieldMapping($fieldName); + + return $mapping !== false && isset($mapping->unique) && $mapping->unique; + } + + public function isNullable(string $fieldName): bool + { + $mapping = $this->getFieldMapping($fieldName); + + return $mapping !== false && isset($mapping->nullable) && $mapping->nullable; + } + + /** + * Gets a column name for a field name. + * If the column name for the field cannot be found, the given field name + * is returned. + */ + public function getColumnName(string $fieldName): string + { + return $this->columnNames[$fieldName] ?? $fieldName; + } + + /** + * Gets the mapping of a (regular) field that holds some data but not a + * reference to another object. + * + * @throws MappingException + */ + public function getFieldMapping(string $fieldName): FieldMapping + { + if (! isset($this->fieldMappings[$fieldName])) { + throw MappingException::mappingNotFound($this->name, $fieldName); + } + + return $this->fieldMappings[$fieldName]; + } + + /** + * Gets the mapping of an association. + * + * @see ClassMetadata::$associationMappings + * + * @param string $fieldName The field name that represents the association in + * the object model. + * + * @throws MappingException + */ + public function getAssociationMapping(string $fieldName): AssociationMapping + { + if (! isset($this->associationMappings[$fieldName])) { + throw MappingException::mappingNotFound($this->name, $fieldName); + } + + return $this->associationMappings[$fieldName]; + } + + /** + * Gets all association mappings of the class. + * + * @psalm-return array + */ + public function getAssociationMappings(): array + { + return $this->associationMappings; + } + + /** + * Gets the field name for a column name. + * If no field name can be found the column name is returned. + * + * @return string The column alias. + */ + public function getFieldName(string $columnName): string + { + return $this->fieldNames[$columnName] ?? $columnName; + } + + /** + * Checks whether given property has type + */ + private function isTypedProperty(string $name): bool + { + return isset($this->reflClass) + && $this->reflClass->hasProperty($name) + && $this->reflClass->getProperty($name)->hasType(); + } + + /** + * Validates & completes the given field mapping based on typed property. + * + * @param array{fieldName: string, type?: string} $mapping The field mapping to validate & complete. + * + * @return array{fieldName: string, enumType?: class-string, type?: string} The updated mapping. + */ + private function validateAndCompleteTypedFieldMapping(array $mapping): array + { + $field = $this->reflClass->getProperty($mapping['fieldName']); + + $mapping = $this->typedFieldMapper->validateAndComplete($mapping, $field); + + return $mapping; + } + + /** + * Validates & completes the basic mapping information based on typed property. + * + * @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. + * + * @return mixed[] The updated mapping. + */ + private function validateAndCompleteTypedAssociationMapping(array $mapping): array + { + $type = $this->reflClass->getProperty($mapping['fieldName'])->getType(); + + if ($type === null || ($mapping['type'] & self::TO_ONE) === 0) { + return $mapping; + } + + if (! isset($mapping['targetEntity']) && $type instanceof ReflectionNamedType) { + $mapping['targetEntity'] = $type->getName(); + } + + return $mapping; + } + + /** + * Validates & completes the given field mapping. + * + * @psalm-param array{ + * fieldName?: string, + * columnName?: string, + * id?: bool, + * generated?: self::GENERATED_*, + * enumType?: class-string, + * } $mapping The field mapping to validate & complete. + * + * @return FieldMapping The updated mapping. + * + * @throws MappingException + */ + protected function validateAndCompleteFieldMapping(array $mapping): FieldMapping + { + // Check mandatory fields + if (! isset($mapping['fieldName']) || ! $mapping['fieldName']) { + throw MappingException::missingFieldName($this->name); + } + + if ($this->isTypedProperty($mapping['fieldName'])) { + $mapping = $this->validateAndCompleteTypedFieldMapping($mapping); + } + + if (! isset($mapping['type'])) { + // Default to string + $mapping['type'] = 'string'; + } + + // Complete fieldName and columnName mapping + if (! isset($mapping['columnName'])) { + $mapping['columnName'] = $this->namingStrategy->propertyToColumnName($mapping['fieldName'], $this->name); + } + + $mapping = FieldMapping::fromMappingArray($mapping); + + if ($mapping->columnName[0] === '`') { + $mapping->columnName = trim($mapping->columnName, '`'); + $mapping->quoted = true; + } + + $this->columnNames[$mapping->fieldName] = $mapping->columnName; + + if (isset($this->fieldNames[$mapping->columnName]) || ($this->discriminatorColumn && $this->discriminatorColumn->name === $mapping->columnName)) { + throw MappingException::duplicateColumnName($this->name, $mapping->columnName); + } + + $this->fieldNames[$mapping->columnName] = $mapping->fieldName; + + // Complete id mapping + if (isset($mapping->id) && $mapping->id === true) { + if ($this->versionField === $mapping->fieldName) { + throw MappingException::cannotVersionIdField($this->name, $mapping->fieldName); + } + + if (! in_array($mapping->fieldName, $this->identifier, true)) { + $this->identifier[] = $mapping->fieldName; + } + + // Check for composite key + if (! $this->isIdentifierComposite && count($this->identifier) > 1) { + $this->isIdentifierComposite = true; + } + } + + if (isset($mapping->generated)) { + if (! in_array($mapping->generated, [self::GENERATED_NEVER, self::GENERATED_INSERT, self::GENERATED_ALWAYS])) { + throw MappingException::invalidGeneratedMode($mapping->generated); + } + + if ($mapping->generated === self::GENERATED_NEVER) { + unset($mapping->generated); + } + } + + if (isset($mapping->enumType)) { + if (! enum_exists($mapping->enumType)) { + throw MappingException::nonEnumTypeMapped($this->name, $mapping->fieldName, $mapping->enumType); + } + + if (! empty($mapping->id)) { + $this->containsEnumIdentifier = true; + } + } + + return $mapping; + } + + /** + * Validates & completes the basic mapping information that is common to all + * association mappings (one-to-one, many-ot-one, one-to-many, many-to-many). + * + * @psalm-param array $mapping The mapping. + * + * @return ConcreteAssociationMapping + * + * @throws MappingException If something is wrong with the mapping. + */ + protected function _validateAndCompleteAssociationMapping(array $mapping): AssociationMapping + { + if (array_key_exists('mappedBy', $mapping) && $mapping['mappedBy'] === null) { + unset($mapping['mappedBy']); + } + + if (array_key_exists('inversedBy', $mapping) && $mapping['inversedBy'] === null) { + unset($mapping['inversedBy']); + } + + if (array_key_exists('joinColumns', $mapping) && in_array($mapping['joinColumns'], [null, []], true)) { + unset($mapping['joinColumns']); + } + + $mapping['isOwningSide'] = true; // assume owning side until we hit mappedBy + + if (empty($mapping['indexBy'])) { + unset($mapping['indexBy']); + } + + // If targetEntity is unqualified, assume it is in the same namespace as + // the sourceEntity. + $mapping['sourceEntity'] = $this->name; + + if ($this->isTypedProperty($mapping['fieldName'])) { + $mapping = $this->validateAndCompleteTypedAssociationMapping($mapping); + } + + if (isset($mapping['targetEntity'])) { + $mapping['targetEntity'] = $this->fullyQualifiedClassName($mapping['targetEntity']); + $mapping['targetEntity'] = ltrim($mapping['targetEntity'], '\\'); + } + + if (($mapping['type'] & self::MANY_TO_ONE) > 0 && isset($mapping['orphanRemoval']) && $mapping['orphanRemoval']) { + throw MappingException::illegalOrphanRemoval($this->name, $mapping['fieldName']); + } + + // Complete id mapping + if (isset($mapping['id']) && $mapping['id'] === true) { + if (isset($mapping['orphanRemoval']) && $mapping['orphanRemoval']) { + throw MappingException::illegalOrphanRemovalOnIdentifierAssociation($this->name, $mapping['fieldName']); + } + + if (! in_array($mapping['fieldName'], $this->identifier, true)) { + if (isset($mapping['joinColumns']) && count($mapping['joinColumns']) >= 2) { + throw MappingException::cannotMapCompositePrimaryKeyEntitiesAsForeignId( + $mapping['targetEntity'], + $this->name, + $mapping['fieldName'], + ); + } + + assert(is_string($mapping['fieldName'])); + $this->identifier[] = $mapping['fieldName']; + $this->containsForeignIdentifier = true; + } + + // Check for composite key + if (! $this->isIdentifierComposite && count($this->identifier) > 1) { + $this->isIdentifierComposite = true; + } + + if ($this->cache && ! isset($mapping['cache'])) { + throw NonCacheableEntityAssociation::fromEntityAndField( + $this->name, + $mapping['fieldName'], + ); + } + } + + // Mandatory attributes for both sides + // Mandatory: fieldName, targetEntity + if (! isset($mapping['fieldName']) || ! $mapping['fieldName']) { + throw MappingException::missingFieldName($this->name); + } + + if (! isset($mapping['targetEntity'])) { + throw MappingException::missingTargetEntity($mapping['fieldName']); + } + + // Mandatory and optional attributes for either side + if (! isset($mapping['mappedBy'])) { + if (isset($mapping['joinTable'])) { + if (isset($mapping['joinTable']['name']) && $mapping['joinTable']['name'][0] === '`') { + $mapping['joinTable']['name'] = trim($mapping['joinTable']['name'], '`'); + $mapping['joinTable']['quoted'] = true; + } + } + } else { + $mapping['isOwningSide'] = false; + } + + if (isset($mapping['id']) && $mapping['id'] === true && $mapping['type'] & self::TO_MANY) { + throw MappingException::illegalToManyIdentifierAssociation($this->name, $mapping['fieldName']); + } + + // Fetch mode. Default fetch mode to LAZY, if not set. + if (! isset($mapping['fetch'])) { + $mapping['fetch'] = self::FETCH_LAZY; + } + + // Cascades + $cascades = isset($mapping['cascade']) ? array_map('strtolower', $mapping['cascade']) : []; + + $allCascades = ['remove', 'persist', 'refresh', 'detach']; + if (in_array('all', $cascades, true)) { + $cascades = $allCascades; + } elseif (count($cascades) !== count(array_intersect($cascades, $allCascades))) { + throw MappingException::invalidCascadeOption( + array_diff($cascades, $allCascades), + $this->name, + $mapping['fieldName'], + ); + } + + $mapping['cascade'] = $cascades; + + switch ($mapping['type']) { + case self::ONE_TO_ONE: + if (isset($mapping['joinColumns']) && $mapping['joinColumns'] && ! $mapping['isOwningSide']) { + throw MappingException::joinColumnNotAllowedOnOneToOneInverseSide( + $this->name, + $mapping['fieldName'], + ); + } + + return $mapping['isOwningSide'] ? + OneToOneOwningSideMapping::fromMappingArrayAndName( + $mapping, + $this->namingStrategy, + $this->name, + $this->table ?? null, + $this->isInheritanceTypeSingleTable(), + ) : + OneToOneInverseSideMapping::fromMappingArrayAndName($mapping, $this->name); + + case self::MANY_TO_ONE: + return ManyToOneAssociationMapping::fromMappingArrayAndName( + $mapping, + $this->namingStrategy, + $this->name, + $this->table ?? null, + $this->isInheritanceTypeSingleTable(), + ); + + case self::ONE_TO_MANY: + return OneToManyAssociationMapping::fromMappingArrayAndName($mapping, $this->name); + + case self::MANY_TO_MANY: + if (isset($mapping['joinColumns'])) { + unset($mapping['joinColumns']); + } + + return $mapping['isOwningSide'] ? + ManyToManyOwningSideMapping::fromMappingArrayAndNamingStrategy($mapping, $this->namingStrategy) : + ManyToManyInverseSideMapping::fromMappingArray($mapping); + + default: + throw MappingException::invalidAssociationType( + $this->name, + $mapping['fieldName'], + $mapping['type'], + ); + } + } + + /** + * {@inheritDoc} + */ + public function getIdentifierFieldNames(): array + { + return $this->identifier; + } + + /** + * Gets the name of the single id field. Note that this only works on + * entity classes that have a single-field pk. + * + * @throws MappingException If the class doesn't have an identifier or it has a composite primary key. + */ + public function getSingleIdentifierFieldName(): string + { + if ($this->isIdentifierComposite) { + throw MappingException::singleIdNotAllowedOnCompositePrimaryKey($this->name); + } + + if (! isset($this->identifier[0])) { + throw MappingException::noIdDefined($this->name); + } + + return $this->identifier[0]; + } + + /** + * Gets the column name of the single id column. Note that this only works on + * entity classes that have a single-field pk. + * + * @throws MappingException If the class doesn't have an identifier or it has a composite primary key. + */ + public function getSingleIdentifierColumnName(): string + { + return $this->getColumnName($this->getSingleIdentifierFieldName()); + } + + /** + * INTERNAL: + * Sets the mapped identifier/primary key fields of this class. + * Mainly used by the ClassMetadataFactory to assign inherited identifiers. + * + * @psalm-param list $identifier + */ + public function setIdentifier(array $identifier): void + { + $this->identifier = $identifier; + $this->isIdentifierComposite = (count($this->identifier) > 1); + } + + /** + * {@inheritDoc} + */ + public function getIdentifier(): array + { + return $this->identifier; + } + + public function hasField(string $fieldName): bool + { + return isset($this->fieldMappings[$fieldName]) || isset($this->embeddedClasses[$fieldName]); + } + + /** + * Gets an array containing all the column names. + * + * @psalm-param list|null $fieldNames + * + * @return mixed[] + * @psalm-return list + */ + public function getColumnNames(array|null $fieldNames = null): array + { + if ($fieldNames === null) { + return array_keys($this->fieldNames); + } + + return array_values(array_map($this->getColumnName(...), $fieldNames)); + } + + /** + * Returns an array with all the identifier column names. + * + * @psalm-return list + */ + public function getIdentifierColumnNames(): array + { + $columnNames = []; + + foreach ($this->identifier as $idProperty) { + if (isset($this->fieldMappings[$idProperty])) { + $columnNames[] = $this->fieldMappings[$idProperty]->columnName; + + continue; + } + + // Association defined as Id field + assert($this->associationMappings[$idProperty]->isToOneOwningSide()); + $joinColumns = $this->associationMappings[$idProperty]->joinColumns; + $assocColumnNames = array_map(static fn (JoinColumnMapping $joinColumn): string => $joinColumn->name, $joinColumns); + + $columnNames = array_merge($columnNames, $assocColumnNames); + } + + return $columnNames; + } + + /** + * Sets the type of Id generator to use for the mapped class. + * + * @psalm-param self::GENERATOR_TYPE_* $generatorType + */ + public function setIdGeneratorType(int $generatorType): void + { + $this->generatorType = $generatorType; + } + + /** + * Checks whether the mapped class uses an Id generator. + */ + public function usesIdGenerator(): bool + { + return $this->generatorType !== self::GENERATOR_TYPE_NONE; + } + + public function isInheritanceTypeNone(): bool + { + return $this->inheritanceType === self::INHERITANCE_TYPE_NONE; + } + + /** + * Checks whether the mapped class uses the JOINED inheritance mapping strategy. + * + * @return bool TRUE if the class participates in a JOINED inheritance mapping, + * FALSE otherwise. + */ + public function isInheritanceTypeJoined(): bool + { + return $this->inheritanceType === self::INHERITANCE_TYPE_JOINED; + } + + /** + * Checks whether the mapped class uses the SINGLE_TABLE inheritance mapping strategy. + * + * @return bool TRUE if the class participates in a SINGLE_TABLE inheritance mapping, + * FALSE otherwise. + */ + public function isInheritanceTypeSingleTable(): bool + { + return $this->inheritanceType === self::INHERITANCE_TYPE_SINGLE_TABLE; + } + + /** + * Checks whether the class uses an identity column for the Id generation. + */ + public function isIdGeneratorIdentity(): bool + { + return $this->generatorType === self::GENERATOR_TYPE_IDENTITY; + } + + /** + * Checks whether the class uses a sequence for id generation. + * + * @psalm-assert-if-true !null $this->sequenceGeneratorDefinition + */ + public function isIdGeneratorSequence(): bool + { + return $this->generatorType === self::GENERATOR_TYPE_SEQUENCE; + } + + /** + * Checks whether the class has a natural identifier/pk (which means it does + * not use any Id generator. + */ + public function isIdentifierNatural(): bool + { + return $this->generatorType === self::GENERATOR_TYPE_NONE; + } + + /** + * Gets the type of a field. + * + * @todo 3.0 Remove this. PersisterHelper should fix it somehow + */ + public function getTypeOfField(string $fieldName): string|null + { + return isset($this->fieldMappings[$fieldName]) + ? $this->fieldMappings[$fieldName]->type + : null; + } + + /** + * Gets the name of the primary table. + */ + public function getTableName(): string + { + return $this->table['name']; + } + + /** + * Gets primary table's schema name. + */ + public function getSchemaName(): string|null + { + return $this->table['schema'] ?? null; + } + + /** + * Gets the table name to use for temporary identifier tables of this class. + */ + public function getTemporaryIdTableName(): string + { + // replace dots with underscores because PostgreSQL creates temporary tables in a special schema + return str_replace('.', '_', $this->getTableName() . '_id_tmp'); + } + + /** + * Sets the mapped subclasses of this class. + * + * @psalm-param list $subclasses The names of all mapped subclasses. + */ + public function setSubclasses(array $subclasses): void + { + foreach ($subclasses as $subclass) { + $this->subClasses[] = $this->fullyQualifiedClassName($subclass); + } + } + + /** + * Sets the parent class names. Only entity classes may be given. + * + * Assumes that the class names in the passed array are in the order: + * directParent -> directParentParent -> directParentParentParent ... -> root. + * + * @psalm-param list $classNames + */ + public function setParentClasses(array $classNames): void + { + $this->parentClasses = $classNames; + + if (count($classNames) > 0) { + $this->rootEntityName = array_pop($classNames); + } + } + + /** + * Sets the inheritance type used by the class and its subclasses. + * + * @psalm-param self::INHERITANCE_TYPE_* $type + * + * @throws MappingException + */ + public function setInheritanceType(int $type): void + { + if (! $this->isInheritanceType($type)) { + throw MappingException::invalidInheritanceType($this->name, $type); + } + + $this->inheritanceType = $type; + } + + /** + * Sets the association to override association mapping of property for an entity relationship. + * + * @psalm-param array $overrideMapping + * + * @throws MappingException + */ + public function setAssociationOverride(string $fieldName, array $overrideMapping): void + { + if (! isset($this->associationMappings[$fieldName])) { + throw MappingException::invalidOverrideFieldName($this->name, $fieldName); + } + + $mapping = $this->associationMappings[$fieldName]->toArray(); + + if (isset($mapping['inherited'])) { + throw MappingException::illegalOverrideOfInheritedProperty( + $this->name, + $fieldName, + $mapping['inherited'], + ); + } + + if (isset($overrideMapping['joinColumns'])) { + $mapping['joinColumns'] = $overrideMapping['joinColumns']; + } + + if (isset($overrideMapping['inversedBy'])) { + $mapping['inversedBy'] = $overrideMapping['inversedBy']; + } + + if (isset($overrideMapping['joinTable'])) { + $mapping['joinTable'] = $overrideMapping['joinTable']; + } + + if (isset($overrideMapping['fetch'])) { + $mapping['fetch'] = $overrideMapping['fetch']; + } + + switch ($mapping['type']) { + case self::ONE_TO_ONE: + case self::MANY_TO_ONE: + $mapping['joinColumnFieldNames'] = []; + $mapping['sourceToTargetKeyColumns'] = []; + break; + case self::MANY_TO_MANY: + $mapping['relationToSourceKeyColumns'] = []; + $mapping['relationToTargetKeyColumns'] = []; + break; + } + + $this->associationMappings[$fieldName] = $this->_validateAndCompleteAssociationMapping($mapping); + } + + /** + * Sets the override for a mapped field. + * + * @psalm-param array $overrideMapping + * + * @throws MappingException + */ + public function setAttributeOverride(string $fieldName, array $overrideMapping): void + { + if (! isset($this->fieldMappings[$fieldName])) { + throw MappingException::invalidOverrideFieldName($this->name, $fieldName); + } + + $mapping = $this->fieldMappings[$fieldName]; + + if (isset($mapping->inherited)) { + throw MappingException::illegalOverrideOfInheritedProperty($this->name, $fieldName, $mapping->inherited); + } + + if (isset($mapping->id)) { + $overrideMapping['id'] = $mapping->id; + } + + if (isset($mapping->declared)) { + $overrideMapping['declared'] = $mapping->declared; + } + + if (! isset($overrideMapping['type'])) { + $overrideMapping['type'] = $mapping->type; + } + + if (! isset($overrideMapping['fieldName'])) { + $overrideMapping['fieldName'] = $mapping->fieldName; + } + + if ($overrideMapping['type'] !== $mapping->type) { + throw MappingException::invalidOverrideFieldType($this->name, $fieldName); + } + + unset($this->fieldMappings[$fieldName]); + unset($this->fieldNames[$mapping->columnName]); + unset($this->columnNames[$mapping->fieldName]); + + $overrideMapping = $this->validateAndCompleteFieldMapping($overrideMapping); + + $this->fieldMappings[$fieldName] = $overrideMapping; + } + + /** + * Checks whether a mapped field is inherited from an entity superclass. + */ + public function isInheritedField(string $fieldName): bool + { + return isset($this->fieldMappings[$fieldName]->inherited); + } + + /** + * Checks if this entity is the root in any entity-inheritance-hierarchy. + */ + public function isRootEntity(): bool + { + return $this->name === $this->rootEntityName; + } + + /** + * Checks whether a mapped association field is inherited from a superclass. + */ + public function isInheritedAssociation(string $fieldName): bool + { + return isset($this->associationMappings[$fieldName]->inherited); + } + + public function isInheritedEmbeddedClass(string $fieldName): bool + { + return isset($this->embeddedClasses[$fieldName]->inherited); + } + + /** + * Sets the name of the primary table the class is mapped to. + * + * @deprecated Use {@link setPrimaryTable}. + */ + public function setTableName(string $tableName): void + { + $this->table['name'] = $tableName; + } + + /** + * Sets the primary table definition. The provided array supports the + * following structure: + * + * name => (optional, defaults to class name) + * indexes => array of indexes (optional) + * uniqueConstraints => array of constraints (optional) + * + * If a key is omitted, the current value is kept. + * + * @psalm-param array $table The table description. + */ + public function setPrimaryTable(array $table): void + { + if (isset($table['name'])) { + // Split schema and table name from a table name like "myschema.mytable" + if (str_contains($table['name'], '.')) { + [$this->table['schema'], $table['name']] = explode('.', $table['name'], 2); + } + + if ($table['name'][0] === '`') { + $table['name'] = trim($table['name'], '`'); + $this->table['quoted'] = true; + } + + $this->table['name'] = $table['name']; + } + + if (isset($table['quoted'])) { + $this->table['quoted'] = $table['quoted']; + } + + if (isset($table['schema'])) { + $this->table['schema'] = $table['schema']; + } + + if (isset($table['indexes'])) { + $this->table['indexes'] = $table['indexes']; + } + + if (isset($table['uniqueConstraints'])) { + $this->table['uniqueConstraints'] = $table['uniqueConstraints']; + } + + if (isset($table['options'])) { + $this->table['options'] = $table['options']; + } + } + + /** + * Checks whether the given type identifies an inheritance type. + */ + private function isInheritanceType(int $type): bool + { + return $type === self::INHERITANCE_TYPE_NONE || + $type === self::INHERITANCE_TYPE_SINGLE_TABLE || + $type === self::INHERITANCE_TYPE_JOINED; + } + + /** + * Adds a mapped field to the class. + * + * @psalm-param array $mapping The field mapping. + * + * @throws MappingException + */ + public function mapField(array $mapping): void + { + $mapping = $this->validateAndCompleteFieldMapping($mapping); + $this->assertFieldNotMapped($mapping->fieldName); + + if (isset($mapping->generated)) { + $this->requiresFetchAfterChange = true; + } + + $this->fieldMappings[$mapping->fieldName] = $mapping; + } + + /** + * INTERNAL: + * Adds an association mapping without completing/validating it. + * This is mainly used to add inherited association mappings to derived classes. + * + * @param ConcreteAssociationMapping $mapping + * + * @throws MappingException + */ + public function addInheritedAssociationMapping(AssociationMapping $mapping/*, $owningClassName = null*/): void + { + if (isset($this->associationMappings[$mapping->fieldName])) { + throw MappingException::duplicateAssociationMapping($this->name, $mapping->fieldName); + } + + $this->associationMappings[$mapping->fieldName] = $mapping; + } + + /** + * INTERNAL: + * Adds a field mapping without completing/validating it. + * This is mainly used to add inherited field mappings to derived classes. + */ + public function addInheritedFieldMapping(FieldMapping $fieldMapping): void + { + $this->fieldMappings[$fieldMapping->fieldName] = $fieldMapping; + $this->columnNames[$fieldMapping->fieldName] = $fieldMapping->columnName; + $this->fieldNames[$fieldMapping->columnName] = $fieldMapping->fieldName; + + if (isset($fieldMapping->generated)) { + $this->requiresFetchAfterChange = true; + } + } + + /** + * Adds a one-to-one mapping. + * + * @param array $mapping The mapping. + */ + public function mapOneToOne(array $mapping): void + { + $mapping['type'] = self::ONE_TO_ONE; + + $mapping = $this->_validateAndCompleteAssociationMapping($mapping); + + $this->_storeAssociationMapping($mapping); + } + + /** + * Adds a one-to-many mapping. + * + * @psalm-param array $mapping The mapping. + */ + public function mapOneToMany(array $mapping): void + { + $mapping['type'] = self::ONE_TO_MANY; + + $mapping = $this->_validateAndCompleteAssociationMapping($mapping); + + $this->_storeAssociationMapping($mapping); + } + + /** + * Adds a many-to-one mapping. + * + * @psalm-param array $mapping The mapping. + */ + public function mapManyToOne(array $mapping): void + { + $mapping['type'] = self::MANY_TO_ONE; + + $mapping = $this->_validateAndCompleteAssociationMapping($mapping); + + $this->_storeAssociationMapping($mapping); + } + + /** + * Adds a many-to-many mapping. + * + * @psalm-param array $mapping The mapping. + */ + public function mapManyToMany(array $mapping): void + { + $mapping['type'] = self::MANY_TO_MANY; + + $mapping = $this->_validateAndCompleteAssociationMapping($mapping); + + $this->_storeAssociationMapping($mapping); + } + + /** + * Stores the association mapping. + * + * @param ConcreteAssociationMapping $assocMapping + * + * @throws MappingException + */ + protected function _storeAssociationMapping(AssociationMapping $assocMapping): void + { + $sourceFieldName = $assocMapping->fieldName; + + $this->assertFieldNotMapped($sourceFieldName); + + $this->associationMappings[$sourceFieldName] = $assocMapping; + } + + /** + * Registers a custom repository class for the entity class. + * + * @param string|null $repositoryClassName The class name of the custom mapper. + * @psalm-param class-string|null $repositoryClassName + */ + public function setCustomRepositoryClass(string|null $repositoryClassName): void + { + if ($repositoryClassName === null) { + $this->customRepositoryClassName = null; + + return; + } + + $this->customRepositoryClassName = $this->fullyQualifiedClassName($repositoryClassName); + } + + /** + * Dispatches the lifecycle event of the given entity to the registered + * lifecycle callbacks and lifecycle listeners. + * + * @deprecated Deprecated since version 2.4 in favor of \Doctrine\ORM\Event\ListenersInvoker + * + * @param string $lifecycleEvent The lifecycle event. + */ + public function invokeLifecycleCallbacks(string $lifecycleEvent, object $entity): void + { + foreach ($this->lifecycleCallbacks[$lifecycleEvent] as $callback) { + $entity->$callback(); + } + } + + /** + * Whether the class has any attached lifecycle listeners or callbacks for a lifecycle event. + */ + public function hasLifecycleCallbacks(string $lifecycleEvent): bool + { + return isset($this->lifecycleCallbacks[$lifecycleEvent]); + } + + /** + * Gets the registered lifecycle callbacks for an event. + * + * @return string[] + * @psalm-return list + */ + public function getLifecycleCallbacks(string $event): array + { + return $this->lifecycleCallbacks[$event] ?? []; + } + + /** + * Adds a lifecycle callback for entities of this class. + */ + public function addLifecycleCallback(string $callback, string $event): void + { + if ($this->isEmbeddedClass) { + throw MappingException::illegalLifecycleCallbackOnEmbeddedClass($callback, $this->name); + } + + if (isset($this->lifecycleCallbacks[$event]) && in_array($callback, $this->lifecycleCallbacks[$event], true)) { + return; + } + + $this->lifecycleCallbacks[$event][] = $callback; + } + + /** + * Sets the lifecycle callbacks for entities of this class. + * Any previously registered callbacks are overwritten. + * + * @psalm-param array> $callbacks + */ + public function setLifecycleCallbacks(array $callbacks): void + { + $this->lifecycleCallbacks = $callbacks; + } + + /** + * Adds a entity listener for entities of this class. + * + * @param string $eventName The entity lifecycle event. + * @param string $class The listener class. + * @param string $method The listener callback method. + * + * @throws MappingException + */ + public function addEntityListener(string $eventName, string $class, string $method): void + { + $class = $this->fullyQualifiedClassName($class); + + $listener = [ + 'class' => $class, + 'method' => $method, + ]; + + if (! class_exists($class)) { + throw MappingException::entityListenerClassNotFound($class, $this->name); + } + + if (! method_exists($class, $method)) { + throw MappingException::entityListenerMethodNotFound($class, $method, $this->name); + } + + if (isset($this->entityListeners[$eventName]) && in_array($listener, $this->entityListeners[$eventName], true)) { + throw MappingException::duplicateEntityListener($class, $method, $this->name); + } + + $this->entityListeners[$eventName][] = $listener; + } + + /** + * Sets the discriminator column definition. + * + * @see getDiscriminatorColumn() + * + * @param DiscriminatorColumnMapping|mixed[]|null $columnDef + * @psalm-param DiscriminatorColumnMapping|array{ + * name: string|null, + * fieldName?: string|null, + * type?: string|null, + * length?: int|null, + * columnDefinition?: string|null, + * enumType?: class-string|null, + * options?: array|null + * }|null $columnDef + * + * @throws MappingException + */ + public function setDiscriminatorColumn(DiscriminatorColumnMapping|array|null $columnDef): void + { + if ($columnDef instanceof DiscriminatorColumnMapping) { + $this->discriminatorColumn = $columnDef; + + return; + } + + if ($columnDef !== null) { + if (! isset($columnDef['name'])) { + throw MappingException::nameIsMandatoryForDiscriminatorColumns($this->name); + } + + if (isset($this->fieldNames[$columnDef['name']])) { + throw MappingException::duplicateColumnName($this->name, $columnDef['name']); + } + + $columnDef['fieldName'] ??= $columnDef['name']; + $columnDef['type'] ??= 'string'; + $columnDef['options'] ??= []; + + if (in_array($columnDef['type'], ['boolean', 'array', 'object', 'datetime', 'time', 'date'], true)) { + throw MappingException::invalidDiscriminatorColumnType($this->name, $columnDef['type']); + } + + $this->discriminatorColumn = DiscriminatorColumnMapping::fromMappingArray($columnDef); + } + } + + final public function getDiscriminatorColumn(): DiscriminatorColumnMapping + { + if ($this->discriminatorColumn === null) { + throw new LogicException('The discriminator column was not set.'); + } + + return $this->discriminatorColumn; + } + + /** + * Sets the discriminator values used by this class. + * Used for JOINED and SINGLE_TABLE inheritance mapping strategies. + * + * @param array $map + */ + public function setDiscriminatorMap(array $map): void + { + foreach ($map as $value => $className) { + $this->addDiscriminatorMapClass($value, $className); + } + } + + /** + * Adds one entry of the discriminator map with a new class and corresponding name. + * + * @throws MappingException + */ + public function addDiscriminatorMapClass(int|string $name, string $className): void + { + $className = $this->fullyQualifiedClassName($className); + $className = ltrim($className, '\\'); + + $this->discriminatorMap[$name] = $className; + + if ($this->name === $className) { + $this->discriminatorValue = $name; + + return; + } + + if (! (class_exists($className) || interface_exists($className))) { + throw MappingException::invalidClassInDiscriminatorMap($className, $this->name); + } + + $this->addSubClass($className); + } + + /** @param array $classes */ + public function addSubClasses(array $classes): void + { + foreach ($classes as $className) { + $this->addSubClass($className); + } + } + + public function addSubClass(string $className): void + { + // By ignoring classes that are not subclasses of the current class, we simplify inheriting + // the subclass list from a parent class at the beginning of \Doctrine\ORM\Mapping\ClassMetadataFactory::doLoadMetadata. + + if (is_subclass_of($className, $this->name) && ! in_array($className, $this->subClasses, true)) { + $this->subClasses[] = $className; + } + } + + public function hasAssociation(string $fieldName): bool + { + return isset($this->associationMappings[$fieldName]); + } + + public function isSingleValuedAssociation(string $fieldName): bool + { + return isset($this->associationMappings[$fieldName]) + && ($this->associationMappings[$fieldName]->isToOne()); + } + + public function isCollectionValuedAssociation(string $fieldName): bool + { + return isset($this->associationMappings[$fieldName]) + && ! $this->associationMappings[$fieldName]->isToOne(); + } + + /** + * Is this an association that only has a single join column? + */ + public function isAssociationWithSingleJoinColumn(string $fieldName): bool + { + return isset($this->associationMappings[$fieldName]) + && isset($this->associationMappings[$fieldName]->joinColumns[0]) + && ! isset($this->associationMappings[$fieldName]->joinColumns[1]); + } + + /** + * Returns the single association join column (if any). + * + * @throws MappingException + */ + public function getSingleAssociationJoinColumnName(string $fieldName): string + { + if (! $this->isAssociationWithSingleJoinColumn($fieldName)) { + throw MappingException::noSingleAssociationJoinColumnFound($this->name, $fieldName); + } + + $assoc = $this->associationMappings[$fieldName]; + + assert($assoc->isToOneOwningSide()); + + return $assoc->joinColumns[0]->name; + } + + /** + * Returns the single association referenced join column name (if any). + * + * @throws MappingException + */ + public function getSingleAssociationReferencedJoinColumnName(string $fieldName): string + { + if (! $this->isAssociationWithSingleJoinColumn($fieldName)) { + throw MappingException::noSingleAssociationJoinColumnFound($this->name, $fieldName); + } + + $assoc = $this->associationMappings[$fieldName]; + + assert($assoc->isToOneOwningSide()); + + return $assoc->joinColumns[0]->referencedColumnName; + } + + /** + * Used to retrieve a fieldname for either field or association from a given column. + * + * This method is used in foreign-key as primary-key contexts. + * + * @throws MappingException + */ + public function getFieldForColumn(string $columnName): string + { + if (isset($this->fieldNames[$columnName])) { + return $this->fieldNames[$columnName]; + } + + foreach ($this->associationMappings as $assocName => $mapping) { + if ( + $this->isAssociationWithSingleJoinColumn($assocName) && + assert($this->associationMappings[$assocName]->isToOneOwningSide()) && + $this->associationMappings[$assocName]->joinColumns[0]->name === $columnName + ) { + return $assocName; + } + } + + throw MappingException::noFieldNameFoundForColumn($this->name, $columnName); + } + + /** + * Sets the ID generator used to generate IDs for instances of this class. + */ + public function setIdGenerator(AbstractIdGenerator $generator): void + { + $this->idGenerator = $generator; + } + + /** + * Sets definition. + * + * @psalm-param array $definition + */ + public function setCustomGeneratorDefinition(array $definition): void + { + $this->customGeneratorDefinition = $definition; + } + + /** + * Sets the definition of the sequence ID generator for this class. + * + * The definition must have the following structure: + * + * array( + * 'sequenceName' => 'name', + * 'allocationSize' => 20, + * 'initialValue' => 1 + * 'quoted' => 1 + * ) + * + * + * @psalm-param array{sequenceName?: string, allocationSize?: int|string, initialValue?: int|string, quoted?: mixed} $definition + * + * @throws MappingException + */ + public function setSequenceGeneratorDefinition(array $definition): void + { + if (! isset($definition['sequenceName']) || trim($definition['sequenceName']) === '') { + throw MappingException::missingSequenceName($this->name); + } + + if ($definition['sequenceName'][0] === '`') { + $definition['sequenceName'] = trim($definition['sequenceName'], '`'); + $definition['quoted'] = true; + } + + if (! isset($definition['allocationSize']) || trim((string) $definition['allocationSize']) === '') { + $definition['allocationSize'] = '1'; + } + + if (! isset($definition['initialValue']) || trim((string) $definition['initialValue']) === '') { + $definition['initialValue'] = '1'; + } + + $definition['allocationSize'] = (string) $definition['allocationSize']; + $definition['initialValue'] = (string) $definition['initialValue']; + + $this->sequenceGeneratorDefinition = $definition; + } + + /** + * Sets the version field mapping used for versioning. Sets the default + * value to use depending on the column type. + * + * @psalm-param array $mapping The version field mapping array. + * + * @throws MappingException + */ + public function setVersionMapping(array &$mapping): void + { + $this->isVersioned = true; + $this->versionField = $mapping['fieldName']; + $this->requiresFetchAfterChange = true; + + if (! isset($mapping['default'])) { + if (in_array($mapping['type'], ['integer', 'bigint', 'smallint'], true)) { + $mapping['default'] = 1; + } elseif ($mapping['type'] === 'datetime') { + $mapping['default'] = 'CURRENT_TIMESTAMP'; + } else { + throw MappingException::unsupportedOptimisticLockingType($this->name, $mapping['fieldName'], $mapping['type']); + } + } + } + + /** + * Sets whether this class is to be versioned for optimistic locking. + */ + public function setVersioned(bool $bool): void + { + $this->isVersioned = $bool; + + if ($bool) { + $this->requiresFetchAfterChange = true; + } + } + + /** + * Sets the name of the field that is to be used for versioning if this class is + * versioned for optimistic locking. + */ + public function setVersionField(string|null $versionField): void + { + $this->versionField = $versionField; + } + + /** + * Marks this class as read only, no change tracking is applied to it. + */ + public function markReadOnly(): void + { + $this->isReadOnly = true; + } + + /** + * {@inheritDoc} + */ + public function getFieldNames(): array + { + return array_keys($this->fieldMappings); + } + + /** + * {@inheritDoc} + */ + public function getAssociationNames(): array + { + return array_keys($this->associationMappings); + } + + /** + * {@inheritDoc} + * + * @psalm-return class-string + * + * @throws InvalidArgumentException + */ + public function getAssociationTargetClass(string $assocName): string + { + return $this->associationMappings[$assocName]->targetEntity + ?? throw new InvalidArgumentException("Association name expected, '" . $assocName . "' is not an association."); + } + + public function getName(): string + { + return $this->name; + } + + public function isAssociationInverseSide(string $assocName): bool + { + return isset($this->associationMappings[$assocName]) + && ! $this->associationMappings[$assocName]->isOwningSide(); + } + + public function getAssociationMappedByTargetField(string $assocName): string + { + $assoc = $this->getAssociationMapping($assocName); + + if (! $assoc instanceof InverseSideMapping) { + throw new LogicException(sprintf( + <<<'EXCEPTION' + Context: Calling %s() with "%s", which is the owning side of an association. + Problem: The owning side of an association has no "mappedBy" field. + Solution: Call %s::isAssociationInverseSide() to check first. + EXCEPTION, + __METHOD__, + $assocName, + self::class, + )); + } + + return $assoc->mappedBy; + } + + /** + * @param C $className + * + * @return string|null null if and only if the input value is null + * @psalm-return (C is class-string ? class-string : (C is string ? string : null)) + * + * @template C of string|null + */ + public function fullyQualifiedClassName(string|null $className): string|null + { + if ($className === null) { + Deprecation::trigger( + 'doctrine/orm', + 'https://github.com/doctrine/orm/pull/11294', + 'Passing null to %s is deprecated and will not be supported in Doctrine ORM 4.0', + __METHOD__, + ); + + return null; + } + + if (! str_contains($className, '\\') && $this->namespace) { + return $this->namespace . '\\' . $className; + } + + return $className; + } + + public function getMetadataValue(string $name): mixed + { + return $this->$name ?? null; + } + + /** + * Map Embedded Class + * + * @psalm-param array{ + * fieldName: string, + * class?: class-string, + * declaredField?: string, + * columnPrefix?: string|false|null, + * originalField?: string + * } $mapping + * + * @throws MappingException + */ + public function mapEmbedded(array $mapping): void + { + $this->assertFieldNotMapped($mapping['fieldName']); + + if (! isset($mapping['class']) && $this->isTypedProperty($mapping['fieldName'])) { + $type = $this->reflClass->getProperty($mapping['fieldName'])->getType(); + if ($type instanceof ReflectionNamedType) { + $mapping['class'] = $type->getName(); + } + } + + if (! (isset($mapping['class']) && $mapping['class'])) { + throw MappingException::missingEmbeddedClass($mapping['fieldName']); + } + + $this->embeddedClasses[$mapping['fieldName']] = EmbeddedClassMapping::fromMappingArray([ + 'class' => $this->fullyQualifiedClassName($mapping['class']), + 'columnPrefix' => $mapping['columnPrefix'] ?? null, + 'declaredField' => $mapping['declaredField'] ?? null, + 'originalField' => $mapping['originalField'] ?? null, + ]); + } + + /** + * Inline the embeddable class + */ + public function inlineEmbeddable(string $property, ClassMetadata $embeddable): void + { + foreach ($embeddable->fieldMappings as $originalFieldMapping) { + $fieldMapping = (array) $originalFieldMapping; + $fieldMapping['originalClass'] ??= $embeddable->name; + $fieldMapping['declaredField'] = isset($fieldMapping['declaredField']) + ? $property . '.' . $fieldMapping['declaredField'] + : $property; + $fieldMapping['originalField'] ??= $fieldMapping['fieldName']; + $fieldMapping['fieldName'] = $property . '.' . $fieldMapping['fieldName']; + + if (! empty($this->embeddedClasses[$property]->columnPrefix)) { + $fieldMapping['columnName'] = $this->embeddedClasses[$property]->columnPrefix . $fieldMapping['columnName']; + } elseif ($this->embeddedClasses[$property]->columnPrefix !== false) { + assert($this->reflClass !== null); + assert($embeddable->reflClass !== null); + $fieldMapping['columnName'] = $this->namingStrategy + ->embeddedFieldToColumnName( + $property, + $fieldMapping['columnName'], + $this->reflClass->name, + $embeddable->reflClass->name, + ); + } + + $this->mapField($fieldMapping); + } + } + + /** @throws MappingException */ + private function assertFieldNotMapped(string $fieldName): void + { + if ( + isset($this->fieldMappings[$fieldName]) || + isset($this->associationMappings[$fieldName]) || + isset($this->embeddedClasses[$fieldName]) + ) { + throw MappingException::duplicateFieldMapping($this->name, $fieldName); + } + } + + /** + * Gets the sequence name based on class metadata. + * + * @todo Sequence names should be computed in DBAL depending on the platform + */ + public function getSequenceName(AbstractPlatform $platform): string + { + $sequencePrefix = $this->getSequencePrefix($platform); + $columnName = $this->getSingleIdentifierColumnName(); + + return $sequencePrefix . '_' . $columnName . '_seq'; + } + + /** + * Gets the sequence name prefix based on class metadata. + * + * @todo Sequence names should be computed in DBAL depending on the platform + */ + public function getSequencePrefix(AbstractPlatform $platform): string + { + $tableName = $this->getTableName(); + $sequencePrefix = $tableName; + + // Prepend the schema name to the table name if there is one + $schemaName = $this->getSchemaName(); + if ($schemaName) { + $sequencePrefix = $schemaName . '.' . $tableName; + } + + return $sequencePrefix; + } + + /** @psalm-param class-string $class */ + private function getAccessibleProperty(ReflectionService $reflService, string $class, string $field): ReflectionProperty|null + { + $reflectionProperty = $reflService->getAccessibleProperty($class, $field); + if ($reflectionProperty?->isReadOnly()) { + $declaringClass = $reflectionProperty->class; + if ($declaringClass !== $class) { + $reflectionProperty = $reflService->getAccessibleProperty($declaringClass, $field); + } + + if ($reflectionProperty !== null) { + $reflectionProperty = new ReflectionReadonlyProperty($reflectionProperty); + } + } + + return $reflectionProperty; + } +} 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 @@ + + */ +class ClassMetadataFactory extends AbstractClassMetadataFactory +{ + private EntityManagerInterface|null $em = null; + private AbstractPlatform|null $targetPlatform = null; + private MappingDriver|null $driver = null; + private EventManager|null $evm = null; + + /** @var mixed[] */ + private array $embeddablesActiveNesting = []; + + private const NON_IDENTITY_DEFAULT_STRATEGY = [ + Platforms\OraclePlatform::class => ClassMetadata::GENERATOR_TYPE_SEQUENCE, + ]; + + public function setEntityManager(EntityManagerInterface $em): void + { + parent::setProxyClassNameResolver(new DefaultProxyClassNameResolver()); + + $this->em = $em; + } + + /** + * @param A $maybeOwningSide + * + * @return (A is ManyToManyAssociationMapping ? ManyToManyOwningSideMapping : ( + * A is OneToOneAssociationMapping ? OneToOneOwningSideMapping : ( + * A is OneToManyAssociationMapping ? ManyToOneAssociationMapping : ( + * A is ManyToOneAssociationMapping ? ManyToOneAssociationMapping : + * ManyToManyOwningSideMapping|OneToOneOwningSideMapping|ManyToOneAssociationMapping + * )))) + * + * @template A of AssociationMapping + */ + final public function getOwningSide(AssociationMapping $maybeOwningSide): OwningSideMapping + { + if ($maybeOwningSide instanceof OwningSideMapping) { + assert($maybeOwningSide instanceof ManyToManyOwningSideMapping || + $maybeOwningSide instanceof OneToOneOwningSideMapping || + $maybeOwningSide instanceof ManyToOneAssociationMapping); + + return $maybeOwningSide; + } + + assert($maybeOwningSide instanceof InverseSideMapping); + + $owningSide = $this->getMetadataFor($maybeOwningSide->targetEntity) + ->associationMappings[$maybeOwningSide->mappedBy]; + + assert($owningSide instanceof ManyToManyOwningSideMapping || + $owningSide instanceof OneToOneOwningSideMapping || + $owningSide instanceof ManyToOneAssociationMapping); + + return $owningSide; + } + + protected function initialize(): void + { + $this->driver = $this->em->getConfiguration()->getMetadataDriverImpl(); + $this->evm = $this->em->getEventManager(); + $this->initialized = true; + } + + protected function onNotFoundMetadata(string $className): ClassMetadata|null + { + if (! $this->evm->hasListeners(Events::onClassMetadataNotFound)) { + return null; + } + + $eventArgs = new OnClassMetadataNotFoundEventArgs($className, $this->em); + + $this->evm->dispatchEvent(Events::onClassMetadataNotFound, $eventArgs); + $classMetadata = $eventArgs->getFoundMetadata(); + assert($classMetadata instanceof ClassMetadata || $classMetadata === null); + + return $classMetadata; + } + + /** + * {@inheritDoc} + */ + protected function doLoadMetadata( + ClassMetadataInterface $class, + ClassMetadataInterface|null $parent, + bool $rootEntityFound, + array $nonSuperclassParents, + ): void { + if ($parent) { + $class->setInheritanceType($parent->inheritanceType); + $class->setDiscriminatorColumn($parent->discriminatorColumn === null ? null : clone $parent->discriminatorColumn); + $class->setIdGeneratorType($parent->generatorType); + $this->addInheritedFields($class, $parent); + $this->addInheritedRelations($class, $parent); + $this->addInheritedEmbeddedClasses($class, $parent); + $class->setIdentifier($parent->identifier); + $class->setVersioned($parent->isVersioned); + $class->setVersionField($parent->versionField); + $class->setDiscriminatorMap($parent->discriminatorMap); + $class->addSubClasses($parent->subClasses); + $class->setLifecycleCallbacks($parent->lifecycleCallbacks); + $class->setChangeTrackingPolicy($parent->changeTrackingPolicy); + + if (! empty($parent->customGeneratorDefinition)) { + $class->setCustomGeneratorDefinition($parent->customGeneratorDefinition); + } + + if ($parent->isMappedSuperclass) { + $class->setCustomRepositoryClass($parent->customRepositoryClassName); + } + } + + // Invoke driver + try { + $this->driver->loadMetadataForClass($class->getName(), $class); + } catch (ReflectionException $e) { + throw MappingException::reflectionFailure($class->getName(), $e); + } + + // If this class has a parent the id generator strategy is inherited. + // However this is only true if the hierarchy of parents contains the root entity, + // if it consists of mapped superclasses these don't necessarily include the id field. + if ($parent && $rootEntityFound) { + $this->inheritIdGeneratorMapping($class, $parent); + } else { + $this->completeIdGeneratorMapping($class); + } + + if (! $class->isMappedSuperclass) { + if ($rootEntityFound && $class->isInheritanceTypeNone()) { + throw MappingException::missingInheritanceTypeDeclaration(end($nonSuperclassParents), $class->name); + } + + foreach ($class->embeddedClasses as $property => $embeddableClass) { + if (isset($embeddableClass->inherited)) { + continue; + } + + if (isset($this->embeddablesActiveNesting[$embeddableClass->class])) { + throw MappingException::infiniteEmbeddableNesting($class->name, $property); + } + + $this->embeddablesActiveNesting[$class->name] = true; + + $embeddableMetadata = $this->getMetadataFor($embeddableClass->class); + + if ($embeddableMetadata->isEmbeddedClass) { + $this->addNestedEmbeddedClasses($embeddableMetadata, $class, $property); + } + + $identifier = $embeddableMetadata->getIdentifier(); + + if (! empty($identifier)) { + $this->inheritIdGeneratorMapping($class, $embeddableMetadata); + } + + $class->inlineEmbeddable($property, $embeddableMetadata); + + unset($this->embeddablesActiveNesting[$class->name]); + } + } + + if ($parent) { + if ($parent->isInheritanceTypeSingleTable()) { + $class->setPrimaryTable($parent->table); + } + + $this->addInheritedIndexes($class, $parent); + + if ($parent->cache) { + $class->cache = $parent->cache; + } + + if ($parent->containsForeignIdentifier) { + $class->containsForeignIdentifier = true; + } + + if ($parent->containsEnumIdentifier) { + $class->containsEnumIdentifier = true; + } + + if (! empty($parent->entityListeners) && empty($class->entityListeners)) { + $class->entityListeners = $parent->entityListeners; + } + } + + $class->setParentClasses($nonSuperclassParents); + + if ($class->isRootEntity() && ! $class->isInheritanceTypeNone() && ! $class->discriminatorMap) { + $this->addDefaultDiscriminatorMap($class); + } + + // During the following event, there may also be updates to the discriminator map as per GH-1257/GH-8402. + // So, we must not discover the missing subclasses before that. + + if ($this->evm->hasListeners(Events::loadClassMetadata)) { + $eventArgs = new LoadClassMetadataEventArgs($class, $this->em); + $this->evm->dispatchEvent(Events::loadClassMetadata, $eventArgs); + } + + $this->findAbstractEntityClassesNotListedInDiscriminatorMap($class); + + $this->validateRuntimeMetadata($class, $parent); + } + + /** + * Validate runtime metadata is correctly defined. + * + * @throws MappingException + */ + protected function validateRuntimeMetadata(ClassMetadata $class, ClassMetadataInterface|null $parent): void + { + if (! $class->reflClass) { + // only validate if there is a reflection class instance + return; + } + + $class->validateIdentifier(); + $class->validateAssociations(); + $class->validateLifecycleCallbacks($this->getReflectionService()); + + // verify inheritance + if (! $class->isMappedSuperclass && ! $class->isInheritanceTypeNone()) { + if (! $parent) { + if (count($class->discriminatorMap) === 0) { + throw MappingException::missingDiscriminatorMap($class->name); + } + + if (! $class->discriminatorColumn) { + throw MappingException::missingDiscriminatorColumn($class->name); + } + + foreach ($class->subClasses as $subClass) { + if ((new ReflectionClass($subClass))->name !== $subClass) { + throw MappingException::invalidClassInDiscriminatorMap($subClass, $class->name); + } + } + } else { + assert($parent instanceof ClassMetadata); // https://github.com/doctrine/orm/issues/8746 + if ( + ! $class->reflClass->isAbstract() + && ! in_array($class->name, $class->discriminatorMap, true) + ) { + throw MappingException::mappedClassNotPartOfDiscriminatorMap($class->name, $class->rootEntityName); + } + } + } elseif ($class->isMappedSuperclass && $class->name === $class->rootEntityName && (count($class->discriminatorMap) || $class->discriminatorColumn)) { + // second condition is necessary for mapped superclasses in the middle of an inheritance hierarchy + throw MappingException::noInheritanceOnMappedSuperClass($class->name); + } + } + + protected function newClassMetadataInstance(string $className): ClassMetadata + { + return new ClassMetadata( + $className, + $this->em->getConfiguration()->getNamingStrategy(), + $this->em->getConfiguration()->getTypedFieldMapper(), + ); + } + + /** + * Adds a default discriminator map if no one is given + * + * If an entity is of any inheritance type and does not contain a + * discriminator map, then the map is generated automatically. This process + * is expensive computation wise. + * + * The automatically generated discriminator map contains the lowercase short name of + * each class as key. + * + * @throws MappingException + */ + private function addDefaultDiscriminatorMap(ClassMetadata $class): void + { + $allClasses = $this->driver->getAllClassNames(); + $fqcn = $class->getName(); + $map = [$this->getShortName($class->name) => $fqcn]; + + $duplicates = []; + foreach ($allClasses as $subClassCandidate) { + if (is_subclass_of($subClassCandidate, $fqcn)) { + $shortName = $this->getShortName($subClassCandidate); + + if (isset($map[$shortName])) { + $duplicates[] = $shortName; + } + + $map[$shortName] = $subClassCandidate; + } + } + + if ($duplicates) { + throw MappingException::duplicateDiscriminatorEntry($class->name, $duplicates, $map); + } + + $class->setDiscriminatorMap($map); + } + + private function findAbstractEntityClassesNotListedInDiscriminatorMap(ClassMetadata $rootEntityClass): void + { + // Only root classes in inheritance hierarchies need contain a discriminator map, + // so skip for other classes. + if (! $rootEntityClass->isRootEntity() || $rootEntityClass->isInheritanceTypeNone()) { + return; + } + + $processedClasses = [$rootEntityClass->name => true]; + foreach ($rootEntityClass->subClasses as $knownSubClass) { + $processedClasses[$knownSubClass] = true; + } + + foreach ($rootEntityClass->discriminatorMap as $declaredClassName) { + // This fetches non-transient parent classes only + $parentClasses = $this->getParentClasses($declaredClassName); + + foreach ($parentClasses as $parentClass) { + if (isset($processedClasses[$parentClass])) { + continue; + } + + $processedClasses[$parentClass] = true; + + // All non-abstract entity classes must be listed in the discriminator map, and + // this will be validated/enforced at runtime (possibly at a later time, when the + // subclass is loaded, but anyways). Also, subclasses is about entity classes only. + // That means we can ignore non-abstract classes here. The (expensive) driver + // check for mapped superclasses need only be run for abstract candidate classes. + if (! (new ReflectionClass($parentClass))->isAbstract() || $this->peekIfIsMappedSuperclass($parentClass)) { + continue; + } + + // We have found a non-transient, non-mapped-superclass = an entity class (possibly abstract, but that does not matter) + $rootEntityClass->addSubClass($parentClass); + } + } + } + + /** @param class-string $className */ + private function peekIfIsMappedSuperclass(string $className): bool + { + $reflService = $this->getReflectionService(); + $class = $this->newClassMetadataInstance($className); + $this->initializeReflection($class, $reflService); + + $this->getDriver()->loadMetadataForClass($className, $class); + + return $class->isMappedSuperclass; + } + + /** + * Gets the lower-case short name of a class. + * + * @psalm-param class-string $className + */ + private function getShortName(string $className): string + { + if (! str_contains($className, '\\')) { + return strtolower($className); + } + + $parts = explode('\\', $className); + + return strtolower(end($parts)); + } + + /** + * Puts the `inherited` and `declared` values into mapping information for fields, associations + * and embedded classes. + */ + private function addMappingInheritanceInformation( + AssociationMapping|EmbeddedClassMapping|FieldMapping $mapping, + ClassMetadata $parentClass, + ): void { + if (! isset($mapping->inherited) && ! $parentClass->isMappedSuperclass) { + $mapping->inherited = $parentClass->name; + } + + if (! isset($mapping->declared)) { + $mapping->declared = $parentClass->name; + } + } + + /** + * Adds inherited fields to the subclass mapping. + */ + private function addInheritedFields(ClassMetadata $subClass, ClassMetadata $parentClass): void + { + foreach ($parentClass->fieldMappings as $mapping) { + $subClassMapping = clone $mapping; + $this->addMappingInheritanceInformation($subClassMapping, $parentClass); + $subClass->addInheritedFieldMapping($subClassMapping); + } + + foreach ($parentClass->reflFields as $name => $field) { + $subClass->reflFields[$name] = $field; + } + } + + /** + * Adds inherited association mappings to the subclass mapping. + * + * @throws MappingException + */ + private function addInheritedRelations(ClassMetadata $subClass, ClassMetadata $parentClass): void + { + foreach ($parentClass->associationMappings as $field => $mapping) { + $subClassMapping = clone $mapping; + $this->addMappingInheritanceInformation($subClassMapping, $parentClass); + // When the class inheriting the relation ($subClass) is the first entity class since the + // relation has been defined in a mapped superclass (or in a chain + // of mapped superclasses) above, then declare this current entity class as the source of + // the relationship. + // According to the definitions given in https://github.com/doctrine/orm/pull/10396/, + // this is the case <=> ! isset($mapping['inherited']). + if (! isset($subClassMapping->inherited)) { + $subClassMapping->sourceEntity = $subClass->name; + } + + $subClass->addInheritedAssociationMapping($subClassMapping); + } + } + + private function addInheritedEmbeddedClasses(ClassMetadata $subClass, ClassMetadata $parentClass): void + { + foreach ($parentClass->embeddedClasses as $field => $embeddedClass) { + $subClassMapping = clone $embeddedClass; + $this->addMappingInheritanceInformation($subClassMapping, $parentClass); + $subClass->embeddedClasses[$field] = $subClassMapping; + } + } + + /** + * Adds nested embedded classes metadata to a parent class. + * + * @param ClassMetadata $subClass Sub embedded class metadata to add nested embedded classes metadata from. + * @param ClassMetadata $parentClass Parent class to add nested embedded classes metadata to. + * @param string $prefix Embedded classes' prefix to use for nested embedded classes field names. + */ + private function addNestedEmbeddedClasses( + ClassMetadata $subClass, + ClassMetadata $parentClass, + string $prefix, + ): void { + foreach ($subClass->embeddedClasses as $property => $embeddableClass) { + if (isset($embeddableClass->inherited)) { + continue; + } + + $embeddableMetadata = $this->getMetadataFor($embeddableClass->class); + + $parentClass->mapEmbedded( + [ + 'fieldName' => $prefix . '.' . $property, + 'class' => $embeddableMetadata->name, + 'columnPrefix' => $embeddableClass->columnPrefix, + 'declaredField' => $embeddableClass->declaredField + ? $prefix . '.' . $embeddableClass->declaredField + : $prefix, + 'originalField' => $embeddableClass->originalField ?: $property, + ], + ); + } + } + + /** + * Copy the table indices from the parent class superclass to the child class + */ + private function addInheritedIndexes(ClassMetadata $subClass, ClassMetadata $parentClass): void + { + if (! $parentClass->isMappedSuperclass) { + return; + } + + foreach (['uniqueConstraints', 'indexes'] as $indexType) { + if (isset($parentClass->table[$indexType])) { + foreach ($parentClass->table[$indexType] as $indexName => $index) { + if (isset($subClass->table[$indexType][$indexName])) { + continue; // Let the inheriting table override indices + } + + $subClass->table[$indexType][$indexName] = $index; + } + } + } + } + + /** + * Completes the ID generator mapping. If "auto" is specified we choose the generator + * most appropriate for the targeted database platform. + * + * @throws ORMException + */ + private function completeIdGeneratorMapping(ClassMetadata $class): void + { + $idGenType = $class->generatorType; + if ($idGenType === ClassMetadata::GENERATOR_TYPE_AUTO) { + $class->setIdGeneratorType($this->determineIdGeneratorStrategy($this->getTargetPlatform())); + } + + // Create & assign an appropriate ID generator instance + switch ($class->generatorType) { + case ClassMetadata::GENERATOR_TYPE_IDENTITY: + $sequenceName = null; + $fieldName = $class->identifier ? $class->getSingleIdentifierFieldName() : null; + $platform = $this->getTargetPlatform(); + + $generator = $fieldName && $class->fieldMappings[$fieldName]->type === 'bigint' + ? new BigIntegerIdentityGenerator() + : new IdentityGenerator(); + + $class->setIdGenerator($generator); + + break; + + case ClassMetadata::GENERATOR_TYPE_SEQUENCE: + // If there is no sequence definition yet, create a default definition + $definition = $class->sequenceGeneratorDefinition; + + if (! $definition) { + $fieldName = $class->getSingleIdentifierFieldName(); + $sequenceName = $class->getSequenceName($this->getTargetPlatform()); + $quoted = isset($class->fieldMappings[$fieldName]->quoted) || isset($class->table['quoted']); + + $definition = [ + 'sequenceName' => $this->truncateSequenceName($sequenceName), + 'allocationSize' => 1, + 'initialValue' => 1, + ]; + + if ($quoted) { + $definition['quoted'] = true; + } + + $class->setSequenceGeneratorDefinition($definition); + } + + $sequenceGenerator = new SequenceGenerator( + $this->em->getConfiguration()->getQuoteStrategy()->getSequenceName($definition, $class, $this->getTargetPlatform()), + (int) $definition['allocationSize'], + ); + $class->setIdGenerator($sequenceGenerator); + break; + + case ClassMetadata::GENERATOR_TYPE_NONE: + $class->setIdGenerator(new AssignedGenerator()); + break; + + case ClassMetadata::GENERATOR_TYPE_CUSTOM: + $definition = $class->customGeneratorDefinition; + if ($definition === null) { + throw InvalidCustomGenerator::onClassNotConfigured(); + } + + if (! class_exists($definition['class'])) { + throw InvalidCustomGenerator::onMissingClass($definition); + } + + $class->setIdGenerator(new $definition['class']()); + break; + + default: + throw UnknownGeneratorType::create($class->generatorType); + } + } + + /** @psalm-return ClassMetadata::GENERATOR_TYPE_* */ + private function determineIdGeneratorStrategy(AbstractPlatform $platform): int + { + assert($this->em !== null); + foreach ($this->em->getConfiguration()->getIdentityGenerationPreferences() as $platformFamily => $strategy) { + if (is_a($platform, $platformFamily)) { + return $strategy; + } + } + + $nonIdentityDefaultStrategy = self::NON_IDENTITY_DEFAULT_STRATEGY; + + // DBAL 3 + if (method_exists($platform, 'getIdentitySequenceName')) { + $nonIdentityDefaultStrategy[Platforms\PostgreSQLPlatform::class] = ClassMetadata::GENERATOR_TYPE_SEQUENCE; + } + + foreach ($nonIdentityDefaultStrategy as $platformFamily => $strategy) { + if (is_a($platform, $platformFamily)) { + if ($platform instanceof Platforms\PostgreSQLPlatform) { + Deprecation::trigger( + 'doctrine/orm', + 'https://github.com/doctrine/orm/issues/8893', + <<<'DEPRECATION' + Relying on non-optimal defaults for ID generation is deprecated, and IDENTITY + results in SERIAL, which is not recommended. + Instead, configure identifier generation strategies explicitly through + configuration. + We currently recommend "SEQUENCE" for "%s", when using DBAL 3, + and "IDENTITY" when using DBAL 4, + so you should probably use the following configuration before upgrading to DBAL 4, + and remove it after deploying that upgrade: + + $configuration->setIdentityGenerationPreferences([ + "%s" => ClassMetadata::GENERATOR_TYPE_SEQUENCE, + ]); + + DEPRECATION, + $platformFamily, + $platformFamily, + ); + } + + return $strategy; + } + } + + return ClassMetadata::GENERATOR_TYPE_IDENTITY; + } + + private function truncateSequenceName(string $schemaElementName): string + { + $platform = $this->getTargetPlatform(); + if (! $platform instanceof Platforms\OraclePlatform) { + return $schemaElementName; + } + + $maxIdentifierLength = $platform->getMaxIdentifierLength(); + + if (strlen($schemaElementName) > $maxIdentifierLength) { + return substr($schemaElementName, 0, $maxIdentifierLength); + } + + return $schemaElementName; + } + + /** + * Inherits the ID generator mapping from a parent class. + */ + private function inheritIdGeneratorMapping(ClassMetadata $class, ClassMetadata $parent): void + { + if ($parent->isIdGeneratorSequence()) { + $class->setSequenceGeneratorDefinition($parent->sequenceGeneratorDefinition); + } + + if ($parent->generatorType) { + $class->setIdGeneratorType($parent->generatorType); + } + + if ($parent->idGenerator ?? null) { + $class->setIdGenerator($parent->idGenerator); + } + } + + protected function wakeupReflection(ClassMetadataInterface $class, ReflectionService $reflService): void + { + $class->wakeupReflection($reflService); + } + + protected function initializeReflection(ClassMetadataInterface $class, ReflectionService $reflService): void + { + $class->initializeReflection($reflService); + } + + protected function getDriver(): MappingDriver + { + assert($this->driver !== null); + + return $this->driver; + } + + protected function isEntity(ClassMetadataInterface $class): bool + { + return ! $class->isMappedSuperclass; + } + + private function getTargetPlatform(): Platforms\AbstractPlatform + { + if (! $this->targetPlatform) { + $this->targetPlatform = $this->em->getConnection()->getDatabasePlatform(); + } + + return $this->targetPlatform; + } +} 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 @@ +|null $enumType + * @param array $options + * @psalm-param 'NEVER'|'INSERT'|'ALWAYS'|null $generated + */ + public function __construct( + public readonly string|null $name = null, + public readonly string|null $type = null, + public readonly int|null $length = null, + public readonly int|null $precision = null, + public readonly int|null $scale = null, + public readonly bool $unique = false, + public readonly bool $nullable = false, + public readonly bool $insertable = true, + public readonly bool $updatable = true, + public readonly string|null $enumType = null, + public readonly array $options = [], + public readonly string|null $columnDefinition = null, + public readonly string|null $generated = null, + ) { + } +} 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 @@ + Map to store entity listener instances. */ + private array $instances = []; + + public function clear(string|null $className = null): void + { + if ($className === null) { + $this->instances = []; + + return; + } + + $className = trim($className, '\\'); + unset($this->instances[$className]); + } + + public function register(object $object): void + { + $this->instances[$object::class] = $object; + } + + public function resolve(string $className): object + { + $className = trim($className, '\\'); + + return $this->instances[$className] ??= new $className(); + } +} 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 @@ +referenceColumnName(); + } + + public function joinTableName( + string $sourceEntity, + string $targetEntity, + string $propertyName, + ): string { + return strtolower($this->classToTableName($sourceEntity) . '_' . + $this->classToTableName($targetEntity)); + } + + public function joinKeyColumnName( + string $entityName, + string|null $referencedColumnName, + ): string { + return strtolower($this->classToTableName($entityName) . '_' . + ($referencedColumnName ?: $this->referenceColumnName())); + } +} 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 @@ +fieldMappings[$fieldName]->quoted) + ? $platform->quoteIdentifier($class->fieldMappings[$fieldName]->columnName) + : $class->fieldMappings[$fieldName]->columnName; + } + + /** + * {@inheritDoc} + * + * @todo Table names should be computed in DBAL depending on the platform + */ + public function getTableName(ClassMetadata $class, AbstractPlatform $platform): string + { + $tableName = $class->table['name']; + + if (! empty($class->table['schema'])) { + $tableName = $class->table['schema'] . '.' . $class->table['name']; + } + + return isset($class->table['quoted']) + ? $platform->quoteIdentifier($tableName) + : $tableName; + } + + /** + * {@inheritDoc} + */ + public function getSequenceName(array $definition, ClassMetadata $class, AbstractPlatform $platform): string + { + return isset($definition['quoted']) + ? $platform->quoteIdentifier($definition['sequenceName']) + : $definition['sequenceName']; + } + + public function getJoinColumnName(JoinColumnMapping $joinColumn, ClassMetadata $class, AbstractPlatform $platform): string + { + return isset($joinColumn->quoted) + ? $platform->quoteIdentifier($joinColumn->name) + : $joinColumn->name; + } + + public function getReferencedJoinColumnName( + JoinColumnMapping $joinColumn, + ClassMetadata $class, + AbstractPlatform $platform, + ): string { + return isset($joinColumn->quoted) + ? $platform->quoteIdentifier($joinColumn->referencedColumnName) + : $joinColumn->referencedColumnName; + } + + public function getJoinTableName( + ManyToManyOwningSideMapping $association, + ClassMetadata $class, + AbstractPlatform $platform, + ): string { + $schema = ''; + + if (isset($association->joinTable->schema)) { + $schema = $association->joinTable->schema . '.'; + } + + $tableName = $association->joinTable->name; + + if (isset($association->joinTable->quoted)) { + $tableName = $platform->quoteIdentifier($tableName); + } + + return $schema . $tableName; + } + + /** + * {@inheritDoc} + */ + public function getIdentifierColumnNames(ClassMetadata $class, AbstractPlatform $platform): array + { + $quotedColumnNames = []; + + foreach ($class->identifier as $fieldName) { + if (isset($class->fieldMappings[$fieldName])) { + $quotedColumnNames[] = $this->getColumnName($fieldName, $class, $platform); + + continue; + } + + // Association defined as Id field + $assoc = $class->associationMappings[$fieldName]; + assert($assoc->isToOneOwningSide()); + $joinColumns = $assoc->joinColumns; + $assocQuotedColumnNames = array_map( + static fn (JoinColumnMapping $joinColumn) => isset($joinColumn->quoted) + ? $platform->quoteIdentifier($joinColumn->name) + : $joinColumn->name, + $joinColumns, + ); + + $quotedColumnNames = array_merge($quotedColumnNames, $assocQuotedColumnNames); + } + + return $quotedColumnNames; + } + + public function getColumnAlias( + string $columnName, + int $counter, + AbstractPlatform $platform, + ClassMetadata|null $class = null, + ): string { + // 1 ) Concatenate column name and counter + // 2 ) Trim the column alias to the maximum identifier length of the platform. + // If the alias is to long, characters are cut off from the beginning. + // 3 ) Strip non alphanumeric characters + // 4 ) Prefix with "_" if the result its numeric + $columnName .= '_' . $counter; + $columnName = substr($columnName, -$platform->getMaxIdentifierLength()); + $columnName = preg_replace('/[^A-Za-z0-9_]/', '', $columnName); + $columnName = is_numeric($columnName) ? '_' . $columnName : $columnName; + + return $this->getSQLResultCasing($platform, $columnName); + } +} 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 @@ +|string> $typedFieldMappings */ + private array $typedFieldMappings; + + private const DEFAULT_TYPED_FIELD_MAPPINGS = [ + DateInterval::class => Types::DATEINTERVAL, + DateTime::class => Types::DATETIME_MUTABLE, + DateTimeImmutable::class => Types::DATETIME_IMMUTABLE, + 'array' => Types::JSON, + 'bool' => Types::BOOLEAN, + 'float' => Types::FLOAT, + 'int' => Types::INTEGER, + 'string' => Types::STRING, + ]; + + /** @param array|string> $typedFieldMappings */ + public function __construct(array $typedFieldMappings = []) + { + $this->typedFieldMappings = array_merge(self::DEFAULT_TYPED_FIELD_MAPPINGS, $typedFieldMappings); + } + + /** + * {@inheritDoc} + */ + public function validateAndComplete(array $mapping, ReflectionProperty $field): array + { + $type = $field->getType(); + + if ( + ! isset($mapping['type']) + && ($type instanceof ReflectionNamedType) + ) { + if (! $type->isBuiltin() && enum_exists($type->getName())) { + $reflection = new ReflectionEnum($type->getName()); + if (! $reflection->isBacked()) { + throw MappingException::backedEnumTypeRequired( + $field->class, + $mapping['fieldName'], + $type->getName(), + ); + } + + assert(is_a($type->getName(), BackedEnum::class, true)); + $mapping['enumType'] = $type->getName(); + $type = $reflection->getBackingType(); + + assert($type instanceof ReflectionNamedType); + } + + if (isset($this->typedFieldMappings[$type->getName()])) { + $mapping['type'] = $this->typedFieldMappings[$type->getName()]; + } + } + + return $mapping; + } +} 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 @@ +|null */ + public readonly string|null $enumType = null, + /** @var array */ + public readonly array $options = [], + ) { + } +} 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 @@ + */ +final class DiscriminatorColumnMapping implements ArrayAccess +{ + use ArrayAccessImplementation; + + /** The database length of the column. Optional. Default value taken from the type. */ + public int|null $length = null; + + public string|null $columnDefinition = null; + + /** @var class-string|null */ + public string|null $enumType = null; + + /** @var array */ + public array $options = []; + + public function __construct( + public string $type, + public string $fieldName, + public string $name, + ) { + } + + /** + * @psalm-param array{ + * type: string, + * fieldName: string, + * name: string, + * length?: int|null, + * columnDefinition?: string|null, + * enumType?: class-string|null, + * options?: array|null, + * } $mappingArray + */ + public static function fromMappingArray(array $mappingArray): self + { + $mapping = new self( + $mappingArray['type'], + $mappingArray['fieldName'], + $mappingArray['name'], + ); + foreach ($mappingArray as $key => $value) { + if (in_array($key, ['type', 'fieldName', 'name'])) { + continue; + } + + if (property_exists($mapping, $key)) { + $mapping->$key = $value ?? $mapping->$key; + } else { + throw new Exception('Unknown property ' . $key . ' on class ' . static::class); + } + } + + return $mapping; + } + + /** @return list */ + public function __sleep(): array + { + $serialized = ['type', 'fieldName', 'name']; + + foreach (['length', 'columnDefinition', 'enumType', 'options'] as $stringOrArrayKey) { + if ($this->$stringOrArrayKey !== null) { + $serialized[] = $stringOrArrayKey; + } + } + + return $serialized; + } +} 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 @@ + $value */ + public function __construct( + public readonly array $value, + ) { + } +} 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, + Mapping\MappedSuperclass::class => 2, + ]; + + private readonly AttributeReader $reader; + + /** + * @param array $paths + * @param true $reportFieldsWhereDeclared no-op, to be removed in 4.0 + */ + public function __construct(array $paths, bool $reportFieldsWhereDeclared = true) + { + if (! $reportFieldsWhereDeclared) { + throw new InvalidArgumentException(sprintf( + 'The $reportFieldsWhereDeclared argument is no longer supported, make sure to omit it when calling %s.', + __METHOD__, + )); + } + + $this->reader = new AttributeReader(); + $this->addPaths($paths); + } + + public function isTransient(string $className): bool + { + $classAttributes = $this->reader->getClassAttributes(new ReflectionClass($className)); + + foreach ($classAttributes as $a) { + $attr = $a instanceof RepeatableAttributeCollection ? $a[0] : $a; + if (isset(self::ENTITY_ATTRIBUTE_CLASSES[$attr::class])) { + return false; + } + } + + return true; + } + + /** + * {@inheritDoc} + * + * @psalm-param class-string $className + * @psalm-param ClassMetadata $metadata + * + * @template T of object + */ + public function loadMetadataForClass(string $className, PersistenceClassMetadata $metadata): void + { + $reflectionClass = $metadata->getReflectionClass() + // this happens when running attribute driver in combination with + // static reflection services. This is not the nicest fix + ?? new ReflectionClass($metadata->name); + + $classAttributes = $this->reader->getClassAttributes($reflectionClass); + + // Evaluate Entity attribute + if (isset($classAttributes[Mapping\Entity::class])) { + $entityAttribute = $classAttributes[Mapping\Entity::class]; + if ($entityAttribute->repositoryClass !== null) { + $metadata->setCustomRepositoryClass($entityAttribute->repositoryClass); + } + + if ($entityAttribute->readOnly) { + $metadata->markReadOnly(); + } + } elseif (isset($classAttributes[Mapping\MappedSuperclass::class])) { + $mappedSuperclassAttribute = $classAttributes[Mapping\MappedSuperclass::class]; + + $metadata->setCustomRepositoryClass($mappedSuperclassAttribute->repositoryClass); + $metadata->isMappedSuperclass = true; + } elseif (isset($classAttributes[Mapping\Embeddable::class])) { + $metadata->isEmbeddedClass = true; + } else { + throw MappingException::classIsNotAValidEntityOrMappedSuperClass($className); + } + + $primaryTable = []; + + if (isset($classAttributes[Mapping\Table::class])) { + $tableAnnot = $classAttributes[Mapping\Table::class]; + $primaryTable['name'] = $tableAnnot->name; + $primaryTable['schema'] = $tableAnnot->schema; + + if ($tableAnnot->options) { + $primaryTable['options'] = $tableAnnot->options; + } + } + + if (isset($classAttributes[Mapping\Index::class])) { + if ($metadata->isEmbeddedClass) { + throw MappingException::invalidAttributeOnEmbeddable($metadata->name, Mapping\Index::class); + } + + foreach ($classAttributes[Mapping\Index::class] as $idx => $indexAnnot) { + $index = []; + + if (! empty($indexAnnot->columns)) { + $index['columns'] = $indexAnnot->columns; + } + + if (! empty($indexAnnot->fields)) { + $index['fields'] = $indexAnnot->fields; + } + + if ( + isset($index['columns'], $index['fields']) + || ( + ! isset($index['columns']) + && ! isset($index['fields']) + ) + ) { + throw MappingException::invalidIndexConfiguration( + $className, + (string) ($indexAnnot->name ?? $idx), + ); + } + + if (! empty($indexAnnot->flags)) { + $index['flags'] = $indexAnnot->flags; + } + + if (! empty($indexAnnot->options)) { + $index['options'] = $indexAnnot->options; + } + + if (! empty($indexAnnot->name)) { + $primaryTable['indexes'][$indexAnnot->name] = $index; + } else { + $primaryTable['indexes'][] = $index; + } + } + } + + if (isset($classAttributes[Mapping\UniqueConstraint::class])) { + if ($metadata->isEmbeddedClass) { + throw MappingException::invalidAttributeOnEmbeddable($metadata->name, Mapping\UniqueConstraint::class); + } + + foreach ($classAttributes[Mapping\UniqueConstraint::class] as $idx => $uniqueConstraintAnnot) { + $uniqueConstraint = []; + + if (! empty($uniqueConstraintAnnot->columns)) { + $uniqueConstraint['columns'] = $uniqueConstraintAnnot->columns; + } + + if (! empty($uniqueConstraintAnnot->fields)) { + $uniqueConstraint['fields'] = $uniqueConstraintAnnot->fields; + } + + if ( + isset($uniqueConstraint['columns'], $uniqueConstraint['fields']) + || ( + ! isset($uniqueConstraint['columns']) + && ! isset($uniqueConstraint['fields']) + ) + ) { + throw MappingException::invalidUniqueConstraintConfiguration( + $className, + (string) ($uniqueConstraintAnnot->name ?? $idx), + ); + } + + if (! empty($uniqueConstraintAnnot->options)) { + $uniqueConstraint['options'] = $uniqueConstraintAnnot->options; + } + + if (! empty($uniqueConstraintAnnot->name)) { + $primaryTable['uniqueConstraints'][$uniqueConstraintAnnot->name] = $uniqueConstraint; + } else { + $primaryTable['uniqueConstraints'][] = $uniqueConstraint; + } + } + } + + $metadata->setPrimaryTable($primaryTable); + + // Evaluate #[Cache] attribute + if (isset($classAttributes[Mapping\Cache::class])) { + if ($metadata->isEmbeddedClass) { + throw MappingException::invalidAttributeOnEmbeddable($metadata->name, Mapping\Cache::class); + } + + $cacheAttribute = $classAttributes[Mapping\Cache::class]; + $cacheMap = [ + 'region' => $cacheAttribute->region, + 'usage' => constant('Doctrine\ORM\Mapping\ClassMetadata::CACHE_USAGE_' . $cacheAttribute->usage), + ]; + + $metadata->enableCache($cacheMap); + } + + // Evaluate InheritanceType attribute + if (isset($classAttributes[Mapping\InheritanceType::class])) { + if ($metadata->isEmbeddedClass) { + throw MappingException::invalidAttributeOnEmbeddable($metadata->name, Mapping\InheritanceType::class); + } + + $inheritanceTypeAttribute = $classAttributes[Mapping\InheritanceType::class]; + + $metadata->setInheritanceType( + constant('Doctrine\ORM\Mapping\ClassMetadata::INHERITANCE_TYPE_' . $inheritanceTypeAttribute->value), + ); + + if ($metadata->inheritanceType !== ClassMetadata::INHERITANCE_TYPE_NONE) { + // Evaluate DiscriminatorColumn attribute + if (isset($classAttributes[Mapping\DiscriminatorColumn::class])) { + $discrColumnAttribute = $classAttributes[Mapping\DiscriminatorColumn::class]; + assert($discrColumnAttribute instanceof Mapping\DiscriminatorColumn); + + $columnDef = [ + 'name' => $discrColumnAttribute->name, + 'type' => $discrColumnAttribute->type ?? 'string', + 'length' => $discrColumnAttribute->length ?? 255, + 'columnDefinition' => $discrColumnAttribute->columnDefinition, + 'enumType' => $discrColumnAttribute->enumType, + ]; + + if ($discrColumnAttribute->options) { + $columnDef['options'] = $discrColumnAttribute->options; + } + + $metadata->setDiscriminatorColumn($columnDef); + } else { + $metadata->setDiscriminatorColumn(['name' => 'dtype', 'type' => 'string', 'length' => 255]); + } + + // Evaluate DiscriminatorMap attribute + if (isset($classAttributes[Mapping\DiscriminatorMap::class])) { + $discrMapAttribute = $classAttributes[Mapping\DiscriminatorMap::class]; + $metadata->setDiscriminatorMap($discrMapAttribute->value); + } + } + } + + // Evaluate DoctrineChangeTrackingPolicy attribute + if (isset($classAttributes[Mapping\ChangeTrackingPolicy::class])) { + if ($metadata->isEmbeddedClass) { + throw MappingException::invalidAttributeOnEmbeddable($metadata->name, Mapping\ChangeTrackingPolicy::class); + } + + $changeTrackingAttribute = $classAttributes[Mapping\ChangeTrackingPolicy::class]; + $metadata->setChangeTrackingPolicy(constant('Doctrine\ORM\Mapping\ClassMetadata::CHANGETRACKING_' . $changeTrackingAttribute->value)); + } + + foreach ($reflectionClass->getProperties() as $property) { + assert($property instanceof ReflectionProperty); + + if ($this->isRepeatedPropertyDeclaration($property, $metadata)) { + continue; + } + + $mapping = []; + $mapping['fieldName'] = $property->name; + + // Evaluate #[Cache] attribute + $cacheAttribute = $this->reader->getPropertyAttribute($property, Mapping\Cache::class); + if ($cacheAttribute !== null) { + assert($cacheAttribute instanceof Mapping\Cache); + + $mapping['cache'] = $metadata->getAssociationCacheDefaults( + $mapping['fieldName'], + [ + 'usage' => (int) constant('Doctrine\ORM\Mapping\ClassMetadata::CACHE_USAGE_' . $cacheAttribute->usage), + 'region' => $cacheAttribute->region, + ], + ); + } + + // Check for JoinColumn/JoinColumns attributes + $joinColumns = []; + + $joinColumnAttributes = $this->reader->getPropertyAttributeCollection($property, Mapping\JoinColumn::class); + + foreach ($joinColumnAttributes as $joinColumnAttribute) { + $joinColumns[] = $this->joinColumnToArray($joinColumnAttribute); + } + + // Field can only be attributed with one of: + // Column, OneToOne, OneToMany, ManyToOne, ManyToMany, Embedded + $columnAttribute = $this->reader->getPropertyAttribute($property, Mapping\Column::class); + $oneToOneAttribute = $this->reader->getPropertyAttribute($property, Mapping\OneToOne::class); + $oneToManyAttribute = $this->reader->getPropertyAttribute($property, Mapping\OneToMany::class); + $manyToOneAttribute = $this->reader->getPropertyAttribute($property, Mapping\ManyToOne::class); + $manyToManyAttribute = $this->reader->getPropertyAttribute($property, Mapping\ManyToMany::class); + $embeddedAttribute = $this->reader->getPropertyAttribute($property, Mapping\Embedded::class); + + if ($columnAttribute !== null) { + $mapping = $this->columnToArray($property->name, $columnAttribute); + + if ($this->reader->getPropertyAttribute($property, Mapping\Id::class)) { + $mapping['id'] = true; + } + + $generatedValueAttribute = $this->reader->getPropertyAttribute($property, Mapping\GeneratedValue::class); + + if ($generatedValueAttribute !== null) { + $metadata->setIdGeneratorType(constant('Doctrine\ORM\Mapping\ClassMetadata::GENERATOR_TYPE_' . $generatedValueAttribute->strategy)); + } + + if ($this->reader->getPropertyAttribute($property, Mapping\Version::class)) { + $metadata->setVersionMapping($mapping); + } + + $metadata->mapField($mapping); + + // Check for SequenceGenerator/TableGenerator definition + $seqGeneratorAttribute = $this->reader->getPropertyAttribute($property, Mapping\SequenceGenerator::class); + $customGeneratorAttribute = $this->reader->getPropertyAttribute($property, Mapping\CustomIdGenerator::class); + + if ($seqGeneratorAttribute !== null) { + $metadata->setSequenceGeneratorDefinition( + [ + 'sequenceName' => $seqGeneratorAttribute->sequenceName, + 'allocationSize' => $seqGeneratorAttribute->allocationSize, + 'initialValue' => $seqGeneratorAttribute->initialValue, + ], + ); + } elseif ($customGeneratorAttribute !== null) { + $metadata->setCustomGeneratorDefinition( + [ + 'class' => $customGeneratorAttribute->class, + ], + ); + } + } elseif ($oneToOneAttribute !== null) { + if ($metadata->isEmbeddedClass) { + throw MappingException::invalidAttributeOnEmbeddable($metadata->name, Mapping\OneToOne::class); + } + + if ($this->reader->getPropertyAttribute($property, Mapping\Id::class)) { + $mapping['id'] = true; + } + + $mapping['targetEntity'] = $oneToOneAttribute->targetEntity; + $mapping['joinColumns'] = $joinColumns; + $mapping['mappedBy'] = $oneToOneAttribute->mappedBy; + $mapping['inversedBy'] = $oneToOneAttribute->inversedBy; + $mapping['cascade'] = $oneToOneAttribute->cascade; + $mapping['orphanRemoval'] = $oneToOneAttribute->orphanRemoval; + $mapping['fetch'] = $this->getFetchMode($className, $oneToOneAttribute->fetch); + $metadata->mapOneToOne($mapping); + } elseif ($oneToManyAttribute !== null) { + if ($metadata->isEmbeddedClass) { + throw MappingException::invalidAttributeOnEmbeddable($metadata->name, Mapping\OneToMany::class); + } + + $mapping['mappedBy'] = $oneToManyAttribute->mappedBy; + $mapping['targetEntity'] = $oneToManyAttribute->targetEntity; + $mapping['cascade'] = $oneToManyAttribute->cascade; + $mapping['indexBy'] = $oneToManyAttribute->indexBy; + $mapping['orphanRemoval'] = $oneToManyAttribute->orphanRemoval; + $mapping['fetch'] = $this->getFetchMode($className, $oneToManyAttribute->fetch); + + $orderByAttribute = $this->reader->getPropertyAttribute($property, Mapping\OrderBy::class); + + if ($orderByAttribute !== null) { + $mapping['orderBy'] = $orderByAttribute->value; + } + + $metadata->mapOneToMany($mapping); + } elseif ($manyToOneAttribute !== null) { + if ($metadata->isEmbeddedClass) { + throw MappingException::invalidAttributeOnEmbeddable($metadata->name, Mapping\OneToMany::class); + } + + $idAttribute = $this->reader->getPropertyAttribute($property, Mapping\Id::class); + + if ($idAttribute !== null) { + $mapping['id'] = true; + } + + $mapping['joinColumns'] = $joinColumns; + $mapping['cascade'] = $manyToOneAttribute->cascade; + $mapping['inversedBy'] = $manyToOneAttribute->inversedBy; + $mapping['targetEntity'] = $manyToOneAttribute->targetEntity; + $mapping['fetch'] = $this->getFetchMode($className, $manyToOneAttribute->fetch); + $metadata->mapManyToOne($mapping); + } elseif ($manyToManyAttribute !== null) { + if ($metadata->isEmbeddedClass) { + throw MappingException::invalidAttributeOnEmbeddable($metadata->name, Mapping\ManyToMany::class); + } + + $joinTable = []; + $joinTableAttribute = $this->reader->getPropertyAttribute($property, Mapping\JoinTable::class); + + if ($joinTableAttribute !== null) { + $joinTable = [ + 'name' => $joinTableAttribute->name, + 'schema' => $joinTableAttribute->schema, + ]; + + if ($joinTableAttribute->options) { + $joinTable['options'] = $joinTableAttribute->options; + } + + foreach ($joinTableAttribute->joinColumns as $joinColumn) { + $joinTable['joinColumns'][] = $this->joinColumnToArray($joinColumn); + } + + foreach ($joinTableAttribute->inverseJoinColumns as $joinColumn) { + $joinTable['inverseJoinColumns'][] = $this->joinColumnToArray($joinColumn); + } + } + + foreach ($this->reader->getPropertyAttributeCollection($property, Mapping\JoinColumn::class) as $joinColumn) { + $joinTable['joinColumns'][] = $this->joinColumnToArray($joinColumn); + } + + foreach ($this->reader->getPropertyAttributeCollection($property, Mapping\InverseJoinColumn::class) as $joinColumn) { + $joinTable['inverseJoinColumns'][] = $this->joinColumnToArray($joinColumn); + } + + $mapping['joinTable'] = $joinTable; + $mapping['targetEntity'] = $manyToManyAttribute->targetEntity; + $mapping['mappedBy'] = $manyToManyAttribute->mappedBy; + $mapping['inversedBy'] = $manyToManyAttribute->inversedBy; + $mapping['cascade'] = $manyToManyAttribute->cascade; + $mapping['indexBy'] = $manyToManyAttribute->indexBy; + $mapping['orphanRemoval'] = $manyToManyAttribute->orphanRemoval; + $mapping['fetch'] = $this->getFetchMode($className, $manyToManyAttribute->fetch); + + $orderByAttribute = $this->reader->getPropertyAttribute($property, Mapping\OrderBy::class); + + if ($orderByAttribute !== null) { + $mapping['orderBy'] = $orderByAttribute->value; + } + + $metadata->mapManyToMany($mapping); + } elseif ($embeddedAttribute !== null) { + $mapping['class'] = $embeddedAttribute->class; + $mapping['columnPrefix'] = $embeddedAttribute->columnPrefix; + + $metadata->mapEmbedded($mapping); + } + } + + // Evaluate AssociationOverrides attribute + if (isset($classAttributes[Mapping\AssociationOverrides::class])) { + if ($metadata->isEmbeddedClass) { + throw MappingException::invalidAttributeOnEmbeddable($metadata->name, Mapping\AssociationOverride::class); + } + + $associationOverride = $classAttributes[Mapping\AssociationOverrides::class]; + + foreach ($associationOverride->overrides as $associationOverride) { + $override = []; + $fieldName = $associationOverride->name; + + // Check for JoinColumn/JoinColumns attributes + if ($associationOverride->joinColumns) { + $joinColumns = []; + + foreach ($associationOverride->joinColumns as $joinColumn) { + $joinColumns[] = $this->joinColumnToArray($joinColumn); + } + + $override['joinColumns'] = $joinColumns; + } + + if ($associationOverride->inverseJoinColumns) { + $joinColumns = []; + + foreach ($associationOverride->inverseJoinColumns as $joinColumn) { + $joinColumns[] = $this->joinColumnToArray($joinColumn); + } + + $override['inverseJoinColumns'] = $joinColumns; + } + + // Check for JoinTable attributes + if ($associationOverride->joinTable) { + $joinTableAnnot = $associationOverride->joinTable; + $joinTable = [ + 'name' => $joinTableAnnot->name, + 'schema' => $joinTableAnnot->schema, + 'joinColumns' => $override['joinColumns'] ?? [], + 'inverseJoinColumns' => $override['inverseJoinColumns'] ?? [], + ]; + + unset($override['joinColumns'], $override['inverseJoinColumns']); + + $override['joinTable'] = $joinTable; + } + + // Check for inversedBy + if ($associationOverride->inversedBy) { + $override['inversedBy'] = $associationOverride->inversedBy; + } + + // Check for `fetch` + if ($associationOverride->fetch) { + $override['fetch'] = constant(ClassMetadata::class . '::FETCH_' . $associationOverride->fetch); + } + + $metadata->setAssociationOverride($fieldName, $override); + } + } + + // Evaluate AttributeOverrides attribute + if (isset($classAttributes[Mapping\AttributeOverrides::class])) { + if ($metadata->isEmbeddedClass) { + throw MappingException::invalidAttributeOnEmbeddable($metadata->name, Mapping\AttributeOverrides::class); + } + + $attributeOverridesAnnot = $classAttributes[Mapping\AttributeOverrides::class]; + + foreach ($attributeOverridesAnnot->overrides as $attributeOverride) { + $mapping = $this->columnToArray($attributeOverride->name, $attributeOverride->column); + + $metadata->setAttributeOverride($attributeOverride->name, $mapping); + } + } + + // Evaluate EntityListeners attribute + if (isset($classAttributes[Mapping\EntityListeners::class])) { + if ($metadata->isEmbeddedClass) { + throw MappingException::invalidAttributeOnEmbeddable($metadata->name, Mapping\EntityListeners::class); + } + + $entityListenersAttribute = $classAttributes[Mapping\EntityListeners::class]; + + foreach ($entityListenersAttribute->value as $item) { + $listenerClassName = $metadata->fullyQualifiedClassName($item); + + if (! class_exists($listenerClassName)) { + throw MappingException::entityListenerClassNotFound($listenerClassName, $className); + } + + $hasMapping = false; + $listenerClass = new ReflectionClass($listenerClassName); + + foreach ($listenerClass->getMethods(ReflectionMethod::IS_PUBLIC) as $method) { + assert($method instanceof ReflectionMethod); + // find method callbacks. + $callbacks = $this->getMethodCallbacks($method); + $hasMapping = $hasMapping ?: ! empty($callbacks); + + foreach ($callbacks as $value) { + $metadata->addEntityListener($value[1], $listenerClassName, $value[0]); + } + } + + // Evaluate the listener using naming convention. + if (! $hasMapping) { + EntityListenerBuilder::bindEntityListener($metadata, $listenerClassName); + } + } + } + + // Evaluate #[HasLifecycleCallbacks] attribute + if (isset($classAttributes[Mapping\HasLifecycleCallbacks::class])) { + if ($metadata->isEmbeddedClass) { + throw MappingException::invalidAttributeOnEmbeddable($metadata->name, Mapping\HasLifecycleCallbacks::class); + } + + foreach ($reflectionClass->getMethods(ReflectionMethod::IS_PUBLIC) as $method) { + assert($method instanceof ReflectionMethod); + foreach ($this->getMethodCallbacks($method) as $value) { + $metadata->addLifecycleCallback($value[0], $value[1]); + } + } + } + } + + /** + * Attempts to resolve the fetch mode. + * + * @param class-string $className The class name. + * @param string $fetchMode The fetch mode. + * + * @return ClassMetadata::FETCH_* The fetch mode as defined in ClassMetadata. + * + * @throws MappingException If the fetch mode is not valid. + */ + private function getFetchMode(string $className, string $fetchMode): int + { + if (! defined('Doctrine\ORM\Mapping\ClassMetadata::FETCH_' . $fetchMode)) { + throw MappingException::invalidFetchMode($className, $fetchMode); + } + + return constant('Doctrine\ORM\Mapping\ClassMetadata::FETCH_' . $fetchMode); + } + + /** + * Attempts to resolve the generated mode. + * + * @throws MappingException If the fetch mode is not valid. + */ + private function getGeneratedMode(string $generatedMode): int + { + if (! defined('Doctrine\ORM\Mapping\ClassMetadata::GENERATED_' . $generatedMode)) { + throw MappingException::invalidGeneratedMode($generatedMode); + } + + return constant('Doctrine\ORM\Mapping\ClassMetadata::GENERATED_' . $generatedMode); + } + + /** + * Parses the given method. + * + * @return list + * @psalm-return list + */ + private function getMethodCallbacks(ReflectionMethod $method): array + { + $callbacks = []; + $attributes = $this->reader->getMethodAttributes($method); + + foreach ($attributes as $attribute) { + if ($attribute instanceof Mapping\PrePersist) { + $callbacks[] = [$method->name, Events::prePersist]; + } + + if ($attribute instanceof Mapping\PostPersist) { + $callbacks[] = [$method->name, Events::postPersist]; + } + + if ($attribute instanceof Mapping\PreUpdate) { + $callbacks[] = [$method->name, Events::preUpdate]; + } + + if ($attribute instanceof Mapping\PostUpdate) { + $callbacks[] = [$method->name, Events::postUpdate]; + } + + if ($attribute instanceof Mapping\PreRemove) { + $callbacks[] = [$method->name, Events::preRemove]; + } + + if ($attribute instanceof Mapping\PostRemove) { + $callbacks[] = [$method->name, Events::postRemove]; + } + + if ($attribute instanceof Mapping\PostLoad) { + $callbacks[] = [$method->name, Events::postLoad]; + } + + if ($attribute instanceof Mapping\PreFlush) { + $callbacks[] = [$method->name, Events::preFlush]; + } + } + + return $callbacks; + } + + /** + * Parse the given JoinColumn as array + * + * @return mixed[] + * @psalm-return array{ + * name: string|null, + * unique: bool, + * nullable: bool, + * onDelete: mixed, + * columnDefinition: string|null, + * referencedColumnName: string, + * options?: array + * } + */ + private function joinColumnToArray(Mapping\JoinColumn|Mapping\InverseJoinColumn $joinColumn): array + { + $mapping = [ + 'name' => $joinColumn->name, + 'unique' => $joinColumn->unique, + 'nullable' => $joinColumn->nullable, + 'onDelete' => $joinColumn->onDelete, + 'columnDefinition' => $joinColumn->columnDefinition, + 'referencedColumnName' => $joinColumn->referencedColumnName, + ]; + + if ($joinColumn->options) { + $mapping['options'] = $joinColumn->options; + } + + return $mapping; + } + + /** + * Parse the given Column as array + * + * @return mixed[] + * @psalm-return array{ + * fieldName: string, + * type: mixed, + * scale: int, + * length: int, + * unique: bool, + * nullable: bool, + * precision: int, + * enumType?: class-string, + * options?: mixed[], + * columnName?: string, + * columnDefinition?: string + * } + */ + private function columnToArray(string $fieldName, Mapping\Column $column): array + { + $mapping = [ + 'fieldName' => $fieldName, + 'type' => $column->type, + 'scale' => $column->scale, + 'length' => $column->length, + 'unique' => $column->unique, + 'nullable' => $column->nullable, + 'precision' => $column->precision, + ]; + + if ($column->options) { + $mapping['options'] = $column->options; + } + + if (isset($column->name)) { + $mapping['columnName'] = $column->name; + } + + if (isset($column->columnDefinition)) { + $mapping['columnDefinition'] = $column->columnDefinition; + } + + if ($column->updatable === false) { + $mapping['notUpdatable'] = true; + } + + if ($column->insertable === false) { + $mapping['notInsertable'] = true; + } + + if ($column->generated !== null) { + $mapping['generated'] = $this->getGeneratedMode($column->generated); + } + + if ($column->enumType) { + $mapping['enumType'] = $column->enumType; + } + + return $mapping; + } +} 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 @@ +, bool> */ + private array $isRepeatableAttribute = []; + + /** + * @psalm-return class-string-map> + * + * @template T of MappingAttribute + */ + public function getClassAttributes(ReflectionClass $class): array + { + return $this->convertToAttributeInstances($class->getAttributes()); + } + + /** + * @return class-string-map> + * + * @template T of MappingAttribute + */ + public function getMethodAttributes(ReflectionMethod $method): array + { + return $this->convertToAttributeInstances($method->getAttributes()); + } + + /** + * @return class-string-map> + * + * @template T of MappingAttribute + */ + public function getPropertyAttributes(ReflectionProperty $property): array + { + return $this->convertToAttributeInstances($property->getAttributes()); + } + + /** + * @param class-string $attributeName The name of the annotation. + * + * @return T|null + * + * @template T of MappingAttribute + */ + public function getPropertyAttribute(ReflectionProperty $property, string $attributeName) + { + if ($this->isRepeatable($attributeName)) { + throw new LogicException(sprintf( + 'The attribute "%s" is repeatable. Call getPropertyAttributeCollection() instead.', + $attributeName, + )); + } + + return $this->getPropertyAttributes($property)[$attributeName] ?? null; + } + + /** + * @param class-string $attributeName The name of the annotation. + * + * @return RepeatableAttributeCollection + * + * @template T of MappingAttribute + */ + public function getPropertyAttributeCollection( + ReflectionProperty $property, + string $attributeName, + ): RepeatableAttributeCollection { + if (! $this->isRepeatable($attributeName)) { + throw new LogicException(sprintf( + 'The attribute "%s" is not repeatable. Call getPropertyAttribute() instead.', + $attributeName, + )); + } + + return $this->getPropertyAttributes($property)[$attributeName] ?? new RepeatableAttributeCollection(); + } + + /** + * @param array $attributes + * + * @return class-string-map> + * + * @template T of MappingAttribute + */ + private function convertToAttributeInstances(array $attributes): array + { + $instances = []; + + foreach ($attributes as $attribute) { + $attributeName = $attribute->getName(); + assert(is_string($attributeName)); + // Make sure we only get Doctrine Attributes + if (! is_subclass_of($attributeName, MappingAttribute::class)) { + continue; + } + + $instance = $attribute->newInstance(); + assert($instance instanceof MappingAttribute); + + if ($this->isRepeatable($attributeName)) { + if (! isset($instances[$attributeName])) { + $instances[$attributeName] = new RepeatableAttributeCollection(); + } + + $collection = $instances[$attributeName]; + assert($collection instanceof RepeatableAttributeCollection); + $collection[] = $instance; + } else { + $instances[$attributeName] = $instance; + } + } + + return $instances; + } + + /** @param class-string $attributeClassName */ + private function isRepeatable(string $attributeClassName): bool + { + if (isset($this->isRepeatableAttribute[$attributeClassName])) { + return $this->isRepeatableAttribute[$attributeClassName]; + } + + $reflectionClass = new ReflectionClass($attributeClassName); + $attribute = $reflectionClass->getAttributes()[0]->newInstance(); + + return $this->isRepeatableAttribute[$attributeClassName] = ($attribute->flags & Attribute::IS_REPEATABLE) > 0; + } +} 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 @@ +|null */ + private array|null $tables = null; + + /** @var array */ + private array $classToTableNames = []; + + /** @psalm-var array */ + private array $manyToManyTables = []; + + /** @var mixed[] */ + private array $classNamesForTables = []; + + /** @var mixed[] */ + private array $fieldNamesForColumns = []; + + /** + * The namespace for the generated entities. + */ + private string|null $namespace = null; + + private Inflector $inflector; + + public function __construct(private readonly AbstractSchemaManager $sm) + { + $this->inflector = InflectorFactory::create()->build(); + } + + /** + * Set the namespace for the generated entities. + */ + public function setNamespace(string $namespace): void + { + $this->namespace = $namespace; + } + + public function isTransient(string $className): bool + { + return true; + } + + /** + * {@inheritDoc} + */ + public function getAllClassNames(): array + { + $this->reverseEngineerMappingFromDatabase(); + + return array_keys($this->classToTableNames); + } + + /** + * Sets class name for a table. + */ + public function setClassNameForTable(string $tableName, string $className): void + { + $this->classNamesForTables[$tableName] = $className; + } + + /** + * Sets field name for a column on a specific table. + */ + public function setFieldNameForColumn(string $tableName, string $columnName, string $fieldName): void + { + $this->fieldNamesForColumns[$tableName][$columnName] = $fieldName; + } + + /** + * Sets tables manually instead of relying on the reverse engineering capabilities of SchemaManager. + * + * @param Table[] $entityTables + * @param Table[] $manyToManyTables + * @psalm-param list $entityTables + * @psalm-param list
$manyToManyTables + */ + public function setTables(array $entityTables, array $manyToManyTables): void + { + $this->tables = $this->manyToManyTables = $this->classToTableNames = []; + + foreach ($entityTables as $table) { + $className = $this->getClassNameForTable($table->getName()); + + $this->classToTableNames[$className] = $table->getName(); + $this->tables[$table->getName()] = $table; + } + + foreach ($manyToManyTables as $table) { + $this->manyToManyTables[$table->getName()] = $table; + } + } + + public function setInflector(Inflector $inflector): void + { + $this->inflector = $inflector; + } + + /** + * {@inheritDoc} + * + * @psalm-param class-string $className + * @psalm-param ClassMetadata $metadata + * + * @template T of object + */ + public function loadMetadataForClass(string $className, PersistenceClassMetadata $metadata): void + { + if (! $metadata instanceof ClassMetadata) { + throw new TypeError(sprintf( + 'Argument #2 passed to %s() must be an instance of %s, %s given.', + __METHOD__, + ClassMetadata::class, + get_debug_type($metadata), + )); + } + + $this->reverseEngineerMappingFromDatabase(); + + if (! isset($this->classToTableNames[$className])) { + throw new InvalidArgumentException('Unknown class ' . $className); + } + + $tableName = $this->classToTableNames[$className]; + + $metadata->name = $className; + $metadata->table['name'] = $tableName; + + $this->buildIndexes($metadata); + $this->buildFieldMappings($metadata); + $this->buildToOneAssociationMappings($metadata); + + foreach ($this->manyToManyTables as $manyTable) { + foreach ($manyTable->getForeignKeys() as $foreignKey) { + // foreign key maps to the table of the current entity, many to many association probably exists + if (! (strtolower($tableName) === strtolower($foreignKey->getForeignTableName()))) { + continue; + } + + $myFk = $foreignKey; + $otherFk = null; + + foreach ($manyTable->getForeignKeys() as $foreignKey) { + if ($foreignKey !== $myFk) { + $otherFk = $foreignKey; + break; + } + } + + if (! $otherFk) { + // the definition of this many to many table does not contain + // enough foreign key information to continue reverse engineering. + continue; + } + + $localColumn = current($myFk->getLocalColumns()); + + $associationMapping = []; + $associationMapping['fieldName'] = $this->getFieldNameForColumn($manyTable->getName(), current($otherFk->getLocalColumns()), true); + $associationMapping['targetEntity'] = $this->getClassNameForTable($otherFk->getForeignTableName()); + + if (current($manyTable->getColumns())->getName() === $localColumn) { + $associationMapping['inversedBy'] = $this->getFieldNameForColumn($manyTable->getName(), current($myFk->getLocalColumns()), true); + $associationMapping['joinTable'] = [ + 'name' => strtolower($manyTable->getName()), + 'joinColumns' => [], + 'inverseJoinColumns' => [], + ]; + + $fkCols = $myFk->getForeignColumns(); + $cols = $myFk->getLocalColumns(); + + for ($i = 0, $colsCount = count($cols); $i < $colsCount; $i++) { + $associationMapping['joinTable']['joinColumns'][] = [ + 'name' => $cols[$i], + 'referencedColumnName' => $fkCols[$i], + ]; + } + + $fkCols = $otherFk->getForeignColumns(); + $cols = $otherFk->getLocalColumns(); + + for ($i = 0, $colsCount = count($cols); $i < $colsCount; $i++) { + $associationMapping['joinTable']['inverseJoinColumns'][] = [ + 'name' => $cols[$i], + 'referencedColumnName' => $fkCols[$i], + ]; + } + } else { + $associationMapping['mappedBy'] = $this->getFieldNameForColumn($manyTable->getName(), current($myFk->getLocalColumns()), true); + } + + $metadata->mapManyToMany($associationMapping); + + break; + } + } + } + + /** @throws MappingException */ + private function reverseEngineerMappingFromDatabase(): void + { + if ($this->tables !== null) { + return; + } + + $this->tables = $this->manyToManyTables = $this->classToTableNames = []; + + foreach ($this->sm->listTables() as $table) { + $tableName = $table->getName(); + $foreignKeys = $table->getForeignKeys(); + + $allForeignKeyColumns = []; + + foreach ($foreignKeys as $foreignKey) { + $allForeignKeyColumns = array_merge($allForeignKeyColumns, $foreignKey->getLocalColumns()); + } + + $primaryKey = $table->getPrimaryKey(); + if ($primaryKey === null) { + throw new MappingException( + 'Table ' . $tableName . ' has no primary key. Doctrine does not ' . + "support reverse engineering from tables that don't have a primary key.", + ); + } + + $pkColumns = $primaryKey->getColumns(); + + sort($pkColumns); + sort($allForeignKeyColumns); + + if ($pkColumns === $allForeignKeyColumns && count($foreignKeys) === 2) { + $this->manyToManyTables[$tableName] = $table; + } else { + // lower-casing is necessary because of Oracle Uppercase Tablenames, + // assumption is lower-case + underscore separated. + $className = $this->getClassNameForTable($tableName); + + $this->tables[$tableName] = $table; + $this->classToTableNames[$className] = $tableName; + } + } + } + + /** + * Build indexes from a class metadata. + */ + private function buildIndexes(ClassMetadata $metadata): void + { + $tableName = $metadata->table['name']; + $indexes = $this->tables[$tableName]->getIndexes(); + + foreach ($indexes as $index) { + if ($index->isPrimary()) { + continue; + } + + $indexName = $index->getName(); + $indexColumns = $index->getColumns(); + $constraintType = $index->isUnique() + ? 'uniqueConstraints' + : 'indexes'; + + $metadata->table[$constraintType][$indexName]['columns'] = $indexColumns; + } + } + + /** + * Build field mapping from class metadata. + */ + private function buildFieldMappings(ClassMetadata $metadata): void + { + $tableName = $metadata->table['name']; + $columns = $this->tables[$tableName]->getColumns(); + $primaryKeys = $this->getTablePrimaryKeys($this->tables[$tableName]); + $foreignKeys = $this->tables[$tableName]->getForeignKeys(); + $allForeignKeys = []; + + foreach ($foreignKeys as $foreignKey) { + $allForeignKeys = array_merge($allForeignKeys, $foreignKey->getLocalColumns()); + } + + $ids = []; + $fieldMappings = []; + + foreach ($columns as $column) { + if (in_array($column->getName(), $allForeignKeys, true)) { + continue; + } + + $fieldMapping = $this->buildFieldMapping($tableName, $column); + + if ($primaryKeys && in_array($column->getName(), $primaryKeys, true)) { + $fieldMapping['id'] = true; + $ids[] = $fieldMapping; + } + + $fieldMappings[] = $fieldMapping; + } + + // We need to check for the columns here, because we might have associations as id as well. + if ($ids && count($primaryKeys) === 1) { + $metadata->setIdGeneratorType(ClassMetadata::GENERATOR_TYPE_AUTO); + } + + foreach ($fieldMappings as $fieldMapping) { + $metadata->mapField($fieldMapping); + } + } + + /** + * Build field mapping from a schema column definition + * + * @return mixed[] + * @psalm-return array{ + * fieldName: string, + * columnName: string, + * type: string, + * nullable: bool, + * options: array{ + * unsigned?: bool, + * fixed?: bool, + * comment: string|null, + * default?: mixed + * }, + * precision?: int, + * scale?: int, + * length?: int|null + * } + */ + private function buildFieldMapping(string $tableName, Column $column): array + { + $fieldMapping = [ + 'fieldName' => $this->getFieldNameForColumn($tableName, $column->getName(), false), + 'columnName' => $column->getName(), + 'type' => Type::getTypeRegistry()->lookupName($column->getType()), + 'nullable' => ! $column->getNotnull(), + 'options' => [ + 'comment' => $column->getComment(), + ], + ]; + + // Type specific elements + switch ($fieldMapping['type']) { + case self::ARRAY: + case Types::BLOB: + case Types::GUID: + case self::OBJECT: + case Types::SIMPLE_ARRAY: + case Types::STRING: + case Types::TEXT: + $fieldMapping['length'] = $column->getLength(); + $fieldMapping['options']['fixed'] = $column->getFixed(); + break; + + case Types::DECIMAL: + case Types::FLOAT: + $fieldMapping['precision'] = $column->getPrecision(); + $fieldMapping['scale'] = $column->getScale(); + break; + + case Types::INTEGER: + case Types::BIGINT: + case Types::SMALLINT: + $fieldMapping['options']['unsigned'] = $column->getUnsigned(); + break; + } + + // Default + $default = $column->getDefault(); + if ($default !== null) { + $fieldMapping['options']['default'] = $default; + } + + return $fieldMapping; + } + + /** + * Build to one (one to one, many to one) association mapping from class metadata. + */ + private function buildToOneAssociationMappings(ClassMetadata $metadata): void + { + assert($this->tables !== null); + + $tableName = $metadata->table['name']; + $primaryKeys = $this->getTablePrimaryKeys($this->tables[$tableName]); + $foreignKeys = $this->tables[$tableName]->getForeignKeys(); + + foreach ($foreignKeys as $foreignKey) { + $foreignTableName = $foreignKey->getForeignTableName(); + $fkColumns = $foreignKey->getLocalColumns(); + $fkForeignColumns = $foreignKey->getForeignColumns(); + $localColumn = current($fkColumns); + $associationMapping = [ + 'fieldName' => $this->getFieldNameForColumn($tableName, $localColumn, true), + 'targetEntity' => $this->getClassNameForTable($foreignTableName), + ]; + + if (isset($metadata->fieldMappings[$associationMapping['fieldName']])) { + $associationMapping['fieldName'] .= '2'; // "foo" => "foo2" + } + + if ($primaryKeys && in_array($localColumn, $primaryKeys, true)) { + $associationMapping['id'] = true; + } + + for ($i = 0, $fkColumnsCount = count($fkColumns); $i < $fkColumnsCount; $i++) { + $associationMapping['joinColumns'][] = [ + 'name' => $fkColumns[$i], + 'referencedColumnName' => $fkForeignColumns[$i], + ]; + } + + // Here we need to check if $fkColumns are the same as $primaryKeys + if (! array_diff($fkColumns, $primaryKeys)) { + $metadata->mapOneToOne($associationMapping); + } else { + $metadata->mapManyToOne($associationMapping); + } + } + } + + /** + * Retrieve schema table definition primary keys. + * + * @return string[] + */ + private function getTablePrimaryKeys(Table $table): array + { + try { + return $table->getPrimaryKey()->getColumns(); + } catch (SchemaException) { + // Do nothing + } + + return []; + } + + /** + * Returns the mapped class name for a table if it exists. Otherwise return "classified" version. + * + * @psalm-return class-string + */ + private function getClassNameForTable(string $tableName): string + { + if (isset($this->classNamesForTables[$tableName])) { + return $this->namespace . $this->classNamesForTables[$tableName]; + } + + return $this->namespace . $this->inflector->classify(strtolower($tableName)); + } + + /** + * Return the mapped field name for a column, if it exists. Otherwise return camelized version. + * + * @param bool $fk Whether the column is a foreignkey or not. + */ + private function getFieldNameForColumn( + string $tableName, + string $columnName, + bool $fk = false, + ): string { + if (isset($this->fieldNamesForColumns[$tableName], $this->fieldNamesForColumns[$tableName][$columnName])) { + return $this->fieldNamesForColumns[$tableName][$columnName]; + } + + $columnName = strtolower($columnName); + + // Replace _id if it is a foreignkey column + if ($fk) { + $columnName = preg_replace('/_id$/', '', $columnName); + } + + return $this->inflector->camelize($columnName); + } +} 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 @@ +class; + + if ( + isset($metadata->fieldMappings[$property->name]->declared) + && $metadata->fieldMappings[$property->name]->declared === $declaringClass + ) { + return true; + } + + if ( + isset($metadata->associationMappings[$property->name]->declared) + && $metadata->associationMappings[$property->name]->declared === $declaringClass + ) { + return true; + } + + return isset($metadata->embeddedClasses[$property->name]->declared) + && $metadata->embeddedClasses[$property->name]->declared === $declaringClass; + } +} 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 @@ + + * @template T of MappingAttribute + */ +final class RepeatableAttributeCollection extends ArrayObject +{ +} 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 @@ + + */ +class XmlDriver extends FileDriver +{ + public const DEFAULT_FILE_EXTENSION = '.dcm.xml'; + + /** + * {@inheritDoc} + */ + public function __construct( + string|array|FileLocator $locator, + string $fileExtension = self::DEFAULT_FILE_EXTENSION, + private readonly bool $isXsdValidationEnabled = true, + ) { + if (! extension_loaded('simplexml')) { + throw new LogicException( + 'The XML metadata driver cannot be enabled because the SimpleXML PHP extension is missing.' + . ' Please configure PHP with SimpleXML or choose a different metadata driver.', + ); + } + + if ($isXsdValidationEnabled && ! extension_loaded('dom')) { + throw new LogicException( + 'XSD validation cannot be enabled because the DOM extension is missing.', + ); + } + + parent::__construct($locator, $fileExtension); + } + + /** + * {@inheritDoc} + * + * @psalm-param class-string $className + * @psalm-param ClassMetadata $metadata + * + * @template T of object + */ + public function loadMetadataForClass($className, PersistenceClassMetadata $metadata): void + { + $xmlRoot = $this->getElement($className); + + if ($xmlRoot->getName() === 'entity') { + if (isset($xmlRoot['repository-class'])) { + $metadata->setCustomRepositoryClass((string) $xmlRoot['repository-class']); + } + + if (isset($xmlRoot['read-only']) && $this->evaluateBoolean($xmlRoot['read-only'])) { + $metadata->markReadOnly(); + } + } elseif ($xmlRoot->getName() === 'mapped-superclass') { + $metadata->setCustomRepositoryClass( + isset($xmlRoot['repository-class']) ? (string) $xmlRoot['repository-class'] : null, + ); + $metadata->isMappedSuperclass = true; + } elseif ($xmlRoot->getName() === 'embeddable') { + $metadata->isEmbeddedClass = true; + } else { + throw MappingException::classIsNotAValidEntityOrMappedSuperClass($className); + } + + // Evaluate attributes + $primaryTable = []; + + if (isset($xmlRoot['table'])) { + $primaryTable['name'] = (string) $xmlRoot['table']; + } + + if (isset($xmlRoot['schema'])) { + $primaryTable['schema'] = (string) $xmlRoot['schema']; + } + + $metadata->setPrimaryTable($primaryTable); + + // Evaluate second level cache + if (isset($xmlRoot->cache)) { + $metadata->enableCache($this->cacheToArray($xmlRoot->cache)); + } + + if (isset($xmlRoot['inheritance-type'])) { + $inheritanceType = (string) $xmlRoot['inheritance-type']; + $metadata->setInheritanceType(constant('Doctrine\ORM\Mapping\ClassMetadata::INHERITANCE_TYPE_' . $inheritanceType)); + + if ($metadata->inheritanceType !== ClassMetadata::INHERITANCE_TYPE_NONE) { + // Evaluate + if (isset($xmlRoot->{'discriminator-column'})) { + $discrColumn = $xmlRoot->{'discriminator-column'}; + $columnDef = [ + 'name' => isset($discrColumn['name']) ? (string) $discrColumn['name'] : null, + 'type' => isset($discrColumn['type']) ? (string) $discrColumn['type'] : 'string', + 'length' => isset($discrColumn['length']) ? (int) $discrColumn['length'] : 255, + 'columnDefinition' => isset($discrColumn['column-definition']) ? (string) $discrColumn['column-definition'] : null, + 'enumType' => isset($discrColumn['enum-type']) ? (string) $discrColumn['enum-type'] : null, + ]; + + if (isset($discrColumn['options'])) { + assert($discrColumn['options'] instanceof SimpleXMLElement); + $columnDef['options'] = $this->parseOptions($discrColumn['options']->children()); + } + + $metadata->setDiscriminatorColumn($columnDef); + } else { + $metadata->setDiscriminatorColumn(['name' => 'dtype', 'type' => 'string', 'length' => 255]); + } + + // Evaluate + if (isset($xmlRoot->{'discriminator-map'})) { + $map = []; + assert($xmlRoot->{'discriminator-map'}->{'discriminator-mapping'} instanceof SimpleXMLElement); + foreach ($xmlRoot->{'discriminator-map'}->{'discriminator-mapping'} as $discrMapElement) { + $map[(string) $discrMapElement['value']] = (string) $discrMapElement['class']; + } + + $metadata->setDiscriminatorMap($map); + } + } + } + + // Evaluate + if (isset($xmlRoot['change-tracking-policy'])) { + $metadata->setChangeTrackingPolicy(constant('Doctrine\ORM\Mapping\ClassMetadata::CHANGETRACKING_' + . strtoupper((string) $xmlRoot['change-tracking-policy']))); + } + + // Evaluate + if (isset($xmlRoot->indexes)) { + $metadata->table['indexes'] = []; + foreach ($xmlRoot->indexes->index ?? [] as $indexXml) { + $index = []; + + if (isset($indexXml['columns']) && ! empty($indexXml['columns'])) { + $index['columns'] = explode(',', (string) $indexXml['columns']); + } + + if (isset($indexXml['fields'])) { + $index['fields'] = explode(',', (string) $indexXml['fields']); + } + + if ( + isset($index['columns'], $index['fields']) + || ( + ! isset($index['columns']) + && ! isset($index['fields']) + ) + ) { + throw MappingException::invalidIndexConfiguration( + $className, + (string) ($indexXml['name'] ?? count($metadata->table['indexes'])), + ); + } + + if (isset($indexXml['flags'])) { + $index['flags'] = explode(',', (string) $indexXml['flags']); + } + + if (isset($indexXml->options)) { + $index['options'] = $this->parseOptions($indexXml->options->children()); + } + + if (isset($indexXml['name'])) { + $metadata->table['indexes'][(string) $indexXml['name']] = $index; + } else { + $metadata->table['indexes'][] = $index; + } + } + } + + // Evaluate + if (isset($xmlRoot->{'unique-constraints'})) { + $metadata->table['uniqueConstraints'] = []; + foreach ($xmlRoot->{'unique-constraints'}->{'unique-constraint'} ?? [] as $uniqueXml) { + $unique = []; + + if (isset($uniqueXml['columns']) && ! empty($uniqueXml['columns'])) { + $unique['columns'] = explode(',', (string) $uniqueXml['columns']); + } + + if (isset($uniqueXml['fields'])) { + $unique['fields'] = explode(',', (string) $uniqueXml['fields']); + } + + if ( + isset($unique['columns'], $unique['fields']) + || ( + ! isset($unique['columns']) + && ! isset($unique['fields']) + ) + ) { + throw MappingException::invalidUniqueConstraintConfiguration( + $className, + (string) ($uniqueXml['name'] ?? count($metadata->table['uniqueConstraints'])), + ); + } + + if (isset($uniqueXml->options)) { + $unique['options'] = $this->parseOptions($uniqueXml->options->children()); + } + + if (isset($uniqueXml['name'])) { + $metadata->table['uniqueConstraints'][(string) $uniqueXml['name']] = $unique; + } else { + $metadata->table['uniqueConstraints'][] = $unique; + } + } + } + + if (isset($xmlRoot->options)) { + $metadata->table['options'] = $this->parseOptions($xmlRoot->options->children()); + } + + // The mapping assignment is done in 2 times as a bug might occurs on some php/xml lib versions + // The internal SimpleXmlIterator get resetted, to this generate a duplicate field exception + // Evaluate mappings + if (isset($xmlRoot->field)) { + foreach ($xmlRoot->field as $fieldMapping) { + $mapping = $this->columnToArray($fieldMapping); + + if (isset($mapping['version'])) { + $metadata->setVersionMapping($mapping); + unset($mapping['version']); + } + + $metadata->mapField($mapping); + } + } + + if (isset($xmlRoot->embedded)) { + foreach ($xmlRoot->embedded as $embeddedMapping) { + $columnPrefix = isset($embeddedMapping['column-prefix']) + ? (string) $embeddedMapping['column-prefix'] + : null; + + $useColumnPrefix = isset($embeddedMapping['use-column-prefix']) + ? $this->evaluateBoolean($embeddedMapping['use-column-prefix']) + : true; + + $mapping = [ + 'fieldName' => (string) $embeddedMapping['name'], + 'class' => isset($embeddedMapping['class']) ? (string) $embeddedMapping['class'] : null, + 'columnPrefix' => $useColumnPrefix ? $columnPrefix : false, + ]; + + $metadata->mapEmbedded($mapping); + } + } + + // Evaluate mappings + $associationIds = []; + foreach ($xmlRoot->id ?? [] as $idElement) { + if (isset($idElement['association-key']) && $this->evaluateBoolean($idElement['association-key'])) { + $associationIds[(string) $idElement['name']] = true; + continue; + } + + $mapping = $this->columnToArray($idElement); + $mapping['id'] = true; + + $metadata->mapField($mapping); + + if (isset($idElement->generator)) { + $strategy = isset($idElement->generator['strategy']) ? + (string) $idElement->generator['strategy'] : 'AUTO'; + $metadata->setIdGeneratorType(constant('Doctrine\ORM\Mapping\ClassMetadata::GENERATOR_TYPE_' + . $strategy)); + } + + // Check for SequenceGenerator/TableGenerator definition + if (isset($idElement->{'sequence-generator'})) { + $seqGenerator = $idElement->{'sequence-generator'}; + $metadata->setSequenceGeneratorDefinition( + [ + 'sequenceName' => (string) $seqGenerator['sequence-name'], + 'allocationSize' => (string) $seqGenerator['allocation-size'], + 'initialValue' => (string) $seqGenerator['initial-value'], + ], + ); + } elseif (isset($idElement->{'custom-id-generator'})) { + $customGenerator = $idElement->{'custom-id-generator'}; + $metadata->setCustomGeneratorDefinition( + [ + 'class' => (string) $customGenerator['class'], + ], + ); + } + } + + // Evaluate mappings + if (isset($xmlRoot->{'one-to-one'})) { + foreach ($xmlRoot->{'one-to-one'} as $oneToOneElement) { + $mapping = [ + 'fieldName' => (string) $oneToOneElement['field'], + ]; + + if (isset($oneToOneElement['target-entity'])) { + $mapping['targetEntity'] = (string) $oneToOneElement['target-entity']; + } + + if (isset($associationIds[$mapping['fieldName']])) { + $mapping['id'] = true; + } + + if (isset($oneToOneElement['fetch'])) { + $mapping['fetch'] = constant('Doctrine\ORM\Mapping\ClassMetadata::FETCH_' . (string) $oneToOneElement['fetch']); + } + + if (isset($oneToOneElement['mapped-by'])) { + $mapping['mappedBy'] = (string) $oneToOneElement['mapped-by']; + } else { + if (isset($oneToOneElement['inversed-by'])) { + $mapping['inversedBy'] = (string) $oneToOneElement['inversed-by']; + } + + $joinColumns = []; + + if (isset($oneToOneElement->{'join-column'})) { + $joinColumns[] = $this->joinColumnToArray($oneToOneElement->{'join-column'}); + } elseif (isset($oneToOneElement->{'join-columns'})) { + foreach ($oneToOneElement->{'join-columns'}->{'join-column'} ?? [] as $joinColumnElement) { + $joinColumns[] = $this->joinColumnToArray($joinColumnElement); + } + } + + $mapping['joinColumns'] = $joinColumns; + } + + if (isset($oneToOneElement->cascade)) { + $mapping['cascade'] = $this->getCascadeMappings($oneToOneElement->cascade); + } + + if (isset($oneToOneElement['orphan-removal'])) { + $mapping['orphanRemoval'] = $this->evaluateBoolean($oneToOneElement['orphan-removal']); + } + + // Evaluate second level cache + if (isset($oneToOneElement->cache)) { + $mapping['cache'] = $metadata->getAssociationCacheDefaults($mapping['fieldName'], $this->cacheToArray($oneToOneElement->cache)); + } + + $metadata->mapOneToOne($mapping); + } + } + + // Evaluate mappings + if (isset($xmlRoot->{'one-to-many'})) { + foreach ($xmlRoot->{'one-to-many'} as $oneToManyElement) { + $mapping = [ + 'fieldName' => (string) $oneToManyElement['field'], + 'mappedBy' => (string) $oneToManyElement['mapped-by'], + ]; + + if (isset($oneToManyElement['target-entity'])) { + $mapping['targetEntity'] = (string) $oneToManyElement['target-entity']; + } + + if (isset($oneToManyElement['fetch'])) { + $mapping['fetch'] = constant('Doctrine\ORM\Mapping\ClassMetadata::FETCH_' . (string) $oneToManyElement['fetch']); + } + + if (isset($oneToManyElement->cascade)) { + $mapping['cascade'] = $this->getCascadeMappings($oneToManyElement->cascade); + } + + if (isset($oneToManyElement['orphan-removal'])) { + $mapping['orphanRemoval'] = $this->evaluateBoolean($oneToManyElement['orphan-removal']); + } + + if (isset($oneToManyElement->{'order-by'})) { + $orderBy = []; + foreach ($oneToManyElement->{'order-by'}->{'order-by-field'} ?? [] as $orderByField) { + /** @psalm-suppress DeprecatedConstant */ + $orderBy[(string) $orderByField['name']] = isset($orderByField['direction']) + ? (string) $orderByField['direction'] + : (enum_exists(Order::class) ? Order::Ascending->value : Criteria::ASC); + } + + $mapping['orderBy'] = $orderBy; + } + + if (isset($oneToManyElement['index-by'])) { + $mapping['indexBy'] = (string) $oneToManyElement['index-by']; + } elseif (isset($oneToManyElement->{'index-by'})) { + throw new InvalidArgumentException(' is not a valid tag'); + } + + // Evaluate second level cache + if (isset($oneToManyElement->cache)) { + $mapping['cache'] = $metadata->getAssociationCacheDefaults($mapping['fieldName'], $this->cacheToArray($oneToManyElement->cache)); + } + + $metadata->mapOneToMany($mapping); + } + } + + // Evaluate mappings + if (isset($xmlRoot->{'many-to-one'})) { + foreach ($xmlRoot->{'many-to-one'} as $manyToOneElement) { + $mapping = [ + 'fieldName' => (string) $manyToOneElement['field'], + ]; + + if (isset($manyToOneElement['target-entity'])) { + $mapping['targetEntity'] = (string) $manyToOneElement['target-entity']; + } + + if (isset($associationIds[$mapping['fieldName']])) { + $mapping['id'] = true; + } + + if (isset($manyToOneElement['fetch'])) { + $mapping['fetch'] = constant('Doctrine\ORM\Mapping\ClassMetadata::FETCH_' . (string) $manyToOneElement['fetch']); + } + + if (isset($manyToOneElement['inversed-by'])) { + $mapping['inversedBy'] = (string) $manyToOneElement['inversed-by']; + } + + $joinColumns = []; + + if (isset($manyToOneElement->{'join-column'})) { + $joinColumns[] = $this->joinColumnToArray($manyToOneElement->{'join-column'}); + } elseif (isset($manyToOneElement->{'join-columns'})) { + foreach ($manyToOneElement->{'join-columns'}->{'join-column'} ?? [] as $joinColumnElement) { + $joinColumns[] = $this->joinColumnToArray($joinColumnElement); + } + } + + $mapping['joinColumns'] = $joinColumns; + + if (isset($manyToOneElement->cascade)) { + $mapping['cascade'] = $this->getCascadeMappings($manyToOneElement->cascade); + } + + // Evaluate second level cache + if (isset($manyToOneElement->cache)) { + $mapping['cache'] = $metadata->getAssociationCacheDefaults($mapping['fieldName'], $this->cacheToArray($manyToOneElement->cache)); + } + + $metadata->mapManyToOne($mapping); + } + } + + // Evaluate mappings + if (isset($xmlRoot->{'many-to-many'})) { + foreach ($xmlRoot->{'many-to-many'} as $manyToManyElement) { + $mapping = [ + 'fieldName' => (string) $manyToManyElement['field'], + ]; + + if (isset($manyToManyElement['target-entity'])) { + $mapping['targetEntity'] = (string) $manyToManyElement['target-entity']; + } + + if (isset($manyToManyElement['fetch'])) { + $mapping['fetch'] = constant('Doctrine\ORM\Mapping\ClassMetadata::FETCH_' . (string) $manyToManyElement['fetch']); + } + + if (isset($manyToManyElement['orphan-removal'])) { + $mapping['orphanRemoval'] = $this->evaluateBoolean($manyToManyElement['orphan-removal']); + } + + if (isset($manyToManyElement['mapped-by'])) { + $mapping['mappedBy'] = (string) $manyToManyElement['mapped-by']; + } elseif (isset($manyToManyElement->{'join-table'})) { + if (isset($manyToManyElement['inversed-by'])) { + $mapping['inversedBy'] = (string) $manyToManyElement['inversed-by']; + } + + $joinTableElement = $manyToManyElement->{'join-table'}; + $joinTable = [ + 'name' => (string) $joinTableElement['name'], + ]; + + if (isset($joinTableElement['schema'])) { + $joinTable['schema'] = (string) $joinTableElement['schema']; + } + + if (isset($joinTableElement->options)) { + $joinTable['options'] = $this->parseOptions($joinTableElement->options->children()); + } + + foreach ($joinTableElement->{'join-columns'}->{'join-column'} ?? [] as $joinColumnElement) { + $joinTable['joinColumns'][] = $this->joinColumnToArray($joinColumnElement); + } + + foreach ($joinTableElement->{'inverse-join-columns'}->{'join-column'} ?? [] as $joinColumnElement) { + $joinTable['inverseJoinColumns'][] = $this->joinColumnToArray($joinColumnElement); + } + + $mapping['joinTable'] = $joinTable; + } + + if (isset($manyToManyElement->cascade)) { + $mapping['cascade'] = $this->getCascadeMappings($manyToManyElement->cascade); + } + + if (isset($manyToManyElement->{'order-by'})) { + $orderBy = []; + foreach ($manyToManyElement->{'order-by'}->{'order-by-field'} ?? [] as $orderByField) { + /** @psalm-suppress DeprecatedConstant */ + $orderBy[(string) $orderByField['name']] = isset($orderByField['direction']) + ? (string) $orderByField['direction'] + : (enum_exists(Order::class) ? Order::Ascending->value : Criteria::ASC); + } + + $mapping['orderBy'] = $orderBy; + } + + if (isset($manyToManyElement['index-by'])) { + $mapping['indexBy'] = (string) $manyToManyElement['index-by']; + } elseif (isset($manyToManyElement->{'index-by'})) { + throw new InvalidArgumentException(' is not a valid tag'); + } + + // Evaluate second level cache + if (isset($manyToManyElement->cache)) { + $mapping['cache'] = $metadata->getAssociationCacheDefaults($mapping['fieldName'], $this->cacheToArray($manyToManyElement->cache)); + } + + $metadata->mapManyToMany($mapping); + } + } + + // Evaluate association-overrides + if (isset($xmlRoot->{'attribute-overrides'})) { + foreach ($xmlRoot->{'attribute-overrides'}->{'attribute-override'} ?? [] as $overrideElement) { + $fieldName = (string) $overrideElement['name']; + foreach ($overrideElement->field ?? [] as $field) { + $mapping = $this->columnToArray($field); + $mapping['fieldName'] = $fieldName; + $metadata->setAttributeOverride($fieldName, $mapping); + } + } + } + + // Evaluate association-overrides + if (isset($xmlRoot->{'association-overrides'})) { + foreach ($xmlRoot->{'association-overrides'}->{'association-override'} ?? [] as $overrideElement) { + $fieldName = (string) $overrideElement['name']; + $override = []; + + // Check for join-columns + if (isset($overrideElement->{'join-columns'})) { + $joinColumns = []; + foreach ($overrideElement->{'join-columns'}->{'join-column'} ?? [] as $joinColumnElement) { + $joinColumns[] = $this->joinColumnToArray($joinColumnElement); + } + + $override['joinColumns'] = $joinColumns; + } + + // Check for join-table + if ($overrideElement->{'join-table'}) { + $joinTable = null; + $joinTableElement = $overrideElement->{'join-table'}; + + $joinTable = [ + 'name' => (string) $joinTableElement['name'], + 'schema' => (string) $joinTableElement['schema'], + ]; + + if (isset($joinTableElement->options)) { + $joinTable['options'] = $this->parseOptions($joinTableElement->options->children()); + } + + if (isset($joinTableElement->{'join-columns'})) { + foreach ($joinTableElement->{'join-columns'}->{'join-column'} ?? [] as $joinColumnElement) { + $joinTable['joinColumns'][] = $this->joinColumnToArray($joinColumnElement); + } + } + + if (isset($joinTableElement->{'inverse-join-columns'})) { + foreach ($joinTableElement->{'inverse-join-columns'}->{'join-column'} ?? [] as $joinColumnElement) { + $joinTable['inverseJoinColumns'][] = $this->joinColumnToArray($joinColumnElement); + } + } + + $override['joinTable'] = $joinTable; + } + + // Check for inversed-by + if (isset($overrideElement->{'inversed-by'})) { + $override['inversedBy'] = (string) $overrideElement->{'inversed-by'}['name']; + } + + // Check for `fetch` + if (isset($overrideElement['fetch'])) { + $override['fetch'] = constant(ClassMetadata::class . '::FETCH_' . (string) $overrideElement['fetch']); + } + + $metadata->setAssociationOverride($fieldName, $override); + } + } + + // Evaluate + if (isset($xmlRoot->{'lifecycle-callbacks'})) { + foreach ($xmlRoot->{'lifecycle-callbacks'}->{'lifecycle-callback'} ?? [] as $lifecycleCallback) { + $metadata->addLifecycleCallback((string) $lifecycleCallback['method'], constant('Doctrine\ORM\Events::' . (string) $lifecycleCallback['type'])); + } + } + + // Evaluate entity listener + if (isset($xmlRoot->{'entity-listeners'})) { + foreach ($xmlRoot->{'entity-listeners'}->{'entity-listener'} ?? [] as $listenerElement) { + $className = (string) $listenerElement['class']; + // Evaluate the listener using naming convention. + if ($listenerElement->count() === 0) { + EntityListenerBuilder::bindEntityListener($metadata, $className); + + continue; + } + + foreach ($listenerElement as $callbackElement) { + $eventName = (string) $callbackElement['type']; + $methodName = (string) $callbackElement['method']; + + $metadata->addEntityListener($eventName, $className, $methodName); + } + } + } + } + + /** + * Parses (nested) option elements. + * + * @return mixed[] The options array. + * @psalm-return array|bool|string> + */ + private function parseOptions(SimpleXMLElement|null $options): array + { + $array = []; + + foreach ($options ?? [] as $option) { + if ($option->count()) { + $value = $this->parseOptions($option->children()); + } else { + $value = (string) $option; + } + + $attributes = $option->attributes(); + + if (isset($attributes->name)) { + $nameAttribute = (string) $attributes->name; + $array[$nameAttribute] = in_array($nameAttribute, ['unsigned', 'fixed'], true) + ? $this->evaluateBoolean($value) + : $value; + } else { + $array[] = $value; + } + } + + return $array; + } + + /** + * Constructs a joinColumn mapping array based on the information + * found in the given SimpleXMLElement. + * + * @param SimpleXMLElement $joinColumnElement The XML element. + * + * @return mixed[] The mapping array. + * @psalm-return array{ + * name: string, + * referencedColumnName: string, + * unique?: bool, + * nullable?: bool, + * onDelete?: string, + * columnDefinition?: string, + * options?: mixed[] + * } + */ + private function joinColumnToArray(SimpleXMLElement $joinColumnElement): array + { + $joinColumn = [ + 'name' => (string) $joinColumnElement['name'], + 'referencedColumnName' => (string) $joinColumnElement['referenced-column-name'], + ]; + + if (isset($joinColumnElement['unique'])) { + $joinColumn['unique'] = $this->evaluateBoolean($joinColumnElement['unique']); + } + + if (isset($joinColumnElement['nullable'])) { + $joinColumn['nullable'] = $this->evaluateBoolean($joinColumnElement['nullable']); + } + + if (isset($joinColumnElement['on-delete'])) { + $joinColumn['onDelete'] = (string) $joinColumnElement['on-delete']; + } + + if (isset($joinColumnElement['column-definition'])) { + $joinColumn['columnDefinition'] = (string) $joinColumnElement['column-definition']; + } + + if (isset($joinColumnElement['options'])) { + $joinColumn['options'] = $this->parseOptions($joinColumnElement['options'] ? $joinColumnElement['options']->children() : null); + } + + return $joinColumn; + } + + /** + * Parses the given field as array. + * + * @return mixed[] + * @psalm-return array{ + * fieldName: string, + * type?: string, + * columnName?: string, + * length?: int, + * precision?: int, + * scale?: int, + * unique?: bool, + * nullable?: bool, + * notInsertable?: bool, + * notUpdatable?: bool, + * enumType?: string, + * version?: bool, + * columnDefinition?: string, + * options?: array + * } + */ + private function columnToArray(SimpleXMLElement $fieldMapping): array + { + $mapping = [ + 'fieldName' => (string) $fieldMapping['name'], + ]; + + if (isset($fieldMapping['type'])) { + $mapping['type'] = (string) $fieldMapping['type']; + } + + if (isset($fieldMapping['column'])) { + $mapping['columnName'] = (string) $fieldMapping['column']; + } + + if (isset($fieldMapping['length'])) { + $mapping['length'] = (int) $fieldMapping['length']; + } + + if (isset($fieldMapping['precision'])) { + $mapping['precision'] = (int) $fieldMapping['precision']; + } + + if (isset($fieldMapping['scale'])) { + $mapping['scale'] = (int) $fieldMapping['scale']; + } + + if (isset($fieldMapping['unique'])) { + $mapping['unique'] = $this->evaluateBoolean($fieldMapping['unique']); + } + + if (isset($fieldMapping['nullable'])) { + $mapping['nullable'] = $this->evaluateBoolean($fieldMapping['nullable']); + } + + if (isset($fieldMapping['insertable']) && ! $this->evaluateBoolean($fieldMapping['insertable'])) { + $mapping['notInsertable'] = true; + } + + if (isset($fieldMapping['updatable']) && ! $this->evaluateBoolean($fieldMapping['updatable'])) { + $mapping['notUpdatable'] = true; + } + + if (isset($fieldMapping['generated'])) { + $mapping['generated'] = constant('Doctrine\ORM\Mapping\ClassMetadata::GENERATED_' . (string) $fieldMapping['generated']); + } + + if (isset($fieldMapping['version']) && $fieldMapping['version']) { + $mapping['version'] = $this->evaluateBoolean($fieldMapping['version']); + } + + if (isset($fieldMapping['column-definition'])) { + $mapping['columnDefinition'] = (string) $fieldMapping['column-definition']; + } + + if (isset($fieldMapping['enum-type'])) { + $mapping['enumType'] = (string) $fieldMapping['enum-type']; + } + + if (isset($fieldMapping->options)) { + $mapping['options'] = $this->parseOptions($fieldMapping->options->children()); + } + + return $mapping; + } + + /** + * Parse / Normalize the cache configuration + * + * @return mixed[] + * @psalm-return array{usage: int|null, region?: string} + */ + private function cacheToArray(SimpleXMLElement $cacheMapping): array + { + $region = isset($cacheMapping['region']) ? (string) $cacheMapping['region'] : null; + $usage = isset($cacheMapping['usage']) ? strtoupper((string) $cacheMapping['usage']) : null; + + if ($usage && ! defined('Doctrine\ORM\Mapping\ClassMetadata::CACHE_USAGE_' . $usage)) { + throw new InvalidArgumentException(sprintf('Invalid cache usage "%s"', $usage)); + } + + if ($usage) { + $usage = (int) constant('Doctrine\ORM\Mapping\ClassMetadata::CACHE_USAGE_' . $usage); + } + + return [ + 'usage' => $usage, + 'region' => $region, + ]; + } + + /** + * Gathers a list of cascade options found in the given cascade element. + * + * @param SimpleXMLElement $cascadeElement The cascade element. + * + * @return string[] The list of cascade options. + * @psalm-return list + */ + private function getCascadeMappings(SimpleXMLElement $cascadeElement): array + { + $cascades = []; + $children = $cascadeElement->children(); + assert($children !== null); + + foreach ($children as $action) { + // According to the JPA specifications, XML uses "cascade-persist" + // instead of "persist". Here, both variations + // are supported because Attribute uses "persist" + // and we want to make sure that this driver doesn't need to know + // anything about the supported cascading actions + $cascades[] = str_replace('cascade-', '', $action->getName()); + } + + return $cascades; + } + + /** + * {@inheritDoc} + */ + protected function loadMappingFile($file) + { + $this->validateMapping($file); + $result = []; + // Note: we do not use `simplexml_load_file()` because of https://bugs.php.net/bug.php?id=62577 + $xmlElement = simplexml_load_string(file_get_contents($file)); + assert($xmlElement !== false); + + if (isset($xmlElement->entity)) { + foreach ($xmlElement->entity as $entityElement) { + /** @psalm-var class-string $entityName */ + $entityName = (string) $entityElement['name']; + $result[$entityName] = $entityElement; + } + } elseif (isset($xmlElement->{'mapped-superclass'})) { + foreach ($xmlElement->{'mapped-superclass'} as $mappedSuperClass) { + /** @psalm-var class-string $className */ + $className = (string) $mappedSuperClass['name']; + $result[$className] = $mappedSuperClass; + } + } elseif (isset($xmlElement->embeddable)) { + foreach ($xmlElement->embeddable as $embeddableElement) { + /** @psalm-var class-string $embeddableName */ + $embeddableName = (string) $embeddableElement['name']; + $result[$embeddableName] = $embeddableElement; + } + } + + return $result; + } + + private function validateMapping(string $file): void + { + if (! $this->isXsdValidationEnabled) { + return; + } + + $backedUpErrorSetting = libxml_use_internal_errors(true); + + try { + $document = new DOMDocument(); + $document->load($file); + + if (! $document->schemaValidate(__DIR__ . '/../../../doctrine-mapping.xsd')) { + throw MappingException::fromLibXmlErrors(libxml_get_errors()); + } + } finally { + libxml_clear_errors(); + libxml_use_internal_errors($backedUpErrorSetting); + } + } + + protected function evaluateBoolean(mixed $element): bool + { + $flag = (string) $element; + + return $flag === 'true' || $flag === '1'; + } +} 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 @@ + */ +final class EmbeddedClassMapping implements ArrayAccess +{ + use ArrayAccessImplementation; + + public string|false|null $columnPrefix = null; + public string|null $declaredField = null; + public string|null $originalField = null; + + /** + * This is set when this embedded-class field is inherited by this class + * from another (inheritance) parent entity class. The value is + * the FQCN of the topmost entity class that contains mapping information + * for this field. (If there are transient classes in the class hierarchy, + * these are ignored, so the class property may in fact come from a class + * further up in the PHP class hierarchy.) Fields initially declared in + * mapped superclasses are not considered 'inherited' in the + * nearest entity subclasses. + * + * @var class-string|null + */ + public string|null $inherited = null; + + /** + * This is set when the embedded-class field does not appear for the first + * time in this class, but is originally declared in another parent + * entity or mapped superclass. The value is the FQCN of the + * topmost non-transient class that contains mapping information for this + * field. + * + * @var class-string|null + */ + public string|null $declared = null; + + /** @param class-string $class */ + public function __construct(public string $class) + { + } + + /** + * @psalm-param array{ + * class: class-string, + * columnPrefix?: false|string|null, + * declaredField?: string|null, + * originalField?: string|null, + * inherited?: class-string|null, + * declared?: class-string|null, + * } $mappingArray + */ + public static function fromMappingArray(array $mappingArray): self + { + $mapping = new self($mappingArray['class']); + foreach ($mappingArray as $key => $value) { + if ($key === 'class') { + continue; + } + + if (property_exists($mapping, $key)) { + $mapping->$key = $value; + } + } + + return $mapping; + } + + /** @return list */ + public function __sleep(): array + { + $serialized = ['class']; + + if ($this->columnPrefix) { + $serialized[] = 'columnPrefix'; + } + + foreach (['declaredField', 'originalField', 'inherited', 'declared'] as $property) { + if ($this->$property !== null) { + $serialized[] = $property; + } + } + + return $serialized; + } +} 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 @@ +>|null $repositoryClass */ + public function __construct( + public readonly string|null $repositoryClass = null, + public readonly bool $readOnly = false, + ) { + } +} 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 @@ + $value */ + public function __construct( + public readonly array $value = [], + ) { + } +} 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 @@ + */ +final class FieldMapping implements ArrayAccess +{ + use ArrayAccessImplementation; + + /** The database length of the column. Optional. Default value taken from the type. */ + public int|null $length = null; + /** + * Marks the field as the primary key of the entity. Multiple + * fields of an entity can have the id attribute, forming a composite key. + */ + public bool|null $id = null; + public bool|null $nullable = null; + public bool|null $notInsertable = null; + public bool|null $notUpdatable = null; + public string|null $columnDefinition = null; + /** @psalm-var ClassMetadata::GENERATED_*|null */ + public int|null $generated = null; + /** @var class-string|null */ + public string|null $enumType = null; + /** + * The precision of a decimal column. + * Only valid if the column type is decimal + */ + public int|null $precision = null; + /** + * The scale of a decimal column. + * Only valid if the column type is decimal + */ + public int|null $scale = null; + /** Whether a unique constraint should be generated for the column. */ + public bool|null $unique = null; + /** + * @var class-string|null This is set when the field is inherited by this + * class from another (inheritance) parent entity class. The value + * is the FQCN of the topmost entity class that contains mapping information + * for this field. (If there are transient classes in the class hierarchy, + * these are ignored, so the class property may in fact come from a class + * further up in the PHP class hierarchy.) + * Fields initially declared in mapped superclasses are + * not considered 'inherited' in the nearest entity subclasses. + */ + public string|null $inherited = null; + + public string|null $originalClass = null; + public string|null $originalField = null; + public bool|null $quoted = null; + /** + * @var class-string|null This is set when the field does not appear for + * the first time in this class, but is originally declared in another + * parent entity or mapped superclass. The value is the FQCN of + * the topmost non-transient class that contains mapping information for + * this field. + */ + public string|null $declared = null; + public string|null $declaredField = null; + public array|null $options = null; + public bool|null $version = null; + public string|int|null $default = null; + + /** + * @param string $type The type name of the mapped field. Can be one of + * Doctrine's mapping types or a custom mapping type. + * @param string $fieldName The name of the field in the Entity. + * @param string $columnName The column name. Optional. Defaults to the field name. + */ + public function __construct( + public string $type, + public string $fieldName, + public string $columnName, + ) { + } + + /** + * @param array $mappingArray + * @psalm-param array{ + * type: string, + * fieldName: string, + * columnName: string, + * length?: int|null, + * id?: bool|null, + * nullable?: bool|null, + * notInsertable?: bool|null, + * notUpdatable?: bool|null, + * columnDefinition?: string|null, + * generated?: ClassMetadata::GENERATED_*|null, + * enumType?: string|null, + * precision?: int|null, + * scale?: int|null, + * unique?: bool|null, + * inherited?: string|null, + * originalClass?: string|null, + * originalField?: string|null, + * quoted?: bool|null, + * declared?: string|null, + * declaredField?: string|null, + * options?: array|null, + * version?: bool|null, + * default?: string|int|null, + * } $mappingArray + */ + public static function fromMappingArray(array $mappingArray): self + { + $mapping = new self( + $mappingArray['type'], + $mappingArray['fieldName'], + $mappingArray['columnName'], + ); + foreach ($mappingArray as $key => $value) { + if (in_array($key, ['type', 'fieldName', 'columnName'])) { + continue; + } + + if (property_exists($mapping, $key)) { + $mapping->$key = $value; + } + } + + return $mapping; + } + + /** @return list */ + public function __sleep(): array + { + $serialized = ['type', 'fieldName', 'columnName']; + + foreach (['nullable', 'notInsertable', 'notUpdatable', 'id', 'unique', 'version', 'quoted'] as $boolKey) { + if ($this->$boolKey) { + $serialized[] = $boolKey; + } + } + + foreach ( + [ + 'length', + 'columnDefinition', + 'generated', + 'enumType', + 'precision', + 'scale', + 'inherited', + 'originalClass', + 'originalField', + 'declared', + 'declaredField', + 'options', + 'default', + ] as $key + ) { + if ($this->$key !== null) { + $serialized[] = $key; + } + } + + return $serialized; + } +} 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 @@ +|null $columns + * @param array|null $fields + * @param array|null $flags + * @param array|null $options + */ + public function __construct( + public readonly string|null $name = null, + public readonly array|null $columns = null, + public readonly array|null $fields = null, + public readonly array|null $flags = null, + public readonly array|null $options = null, + ) { + } +} 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 @@ +mappedBy; + } + + /** @return list */ + public function __sleep(): array + { + return [ + ...parent::__sleep(), + 'mappedBy', + ]; + } +} 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 @@ + */ +final class JoinColumnMapping implements ArrayAccess +{ + use ArrayAccessImplementation; + + public bool|null $unique = null; + public bool|null $quoted = null; + public string|null $fieldName = null; + public string|null $onDelete = null; + public string|null $columnDefinition = null; + public bool|null $nullable = null; + + /** @var array|null */ + public array|null $options = null; + + public function __construct( + public string $name, + public string $referencedColumnName, + ) { + } + + /** + * @param array $mappingArray + * @psalm-param array{ + * name: string, + * referencedColumnName: string, + * unique?: bool|null, + * quoted?: bool|null, + * fieldName?: string|null, + * onDelete?: string|null, + * columnDefinition?: string|null, + * nullable?: bool|null, + * options?: array|null, + * } $mappingArray + */ + public static function fromMappingArray(array $mappingArray): self + { + $mapping = new self($mappingArray['name'], $mappingArray['referencedColumnName']); + foreach ($mappingArray as $key => $value) { + if (property_exists($mapping, $key) && $value !== null) { + $mapping->$key = $value; + } + } + + return $mapping; + } + + /** @return list */ + public function __sleep(): array + { + $serialized = []; + + foreach (['name', 'fieldName', 'onDelete', 'columnDefinition', 'referencedColumnName', 'options'] as $stringOrArrayKey) { + if ($this->$stringOrArrayKey !== null) { + $serialized[] = $stringOrArrayKey; + } + } + + foreach (['unique', 'quoted', 'nullable'] as $boolKey) { + if ($this->$boolKey !== null) { + $serialized[] = $boolKey; + } + } + + return $serialized; + } +} 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 @@ + $options */ + public function __construct( + public readonly string|null $name = null, + public readonly string $referencedColumnName = 'id', + public readonly bool $unique = false, + public readonly bool $nullable = true, + public readonly mixed $onDelete = null, + public readonly string|null $columnDefinition = null, + public readonly string|null $fieldName = null, + public readonly array $options = [], + ) { + } +} 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 @@ + $value */ + public function __construct( + public readonly array $value, + ) { + } +} 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 @@ + */ + public readonly array $joinColumns; + + /** @var array */ + public readonly array $inverseJoinColumns; + + /** + * @param array|JoinColumn $joinColumns + * @param array|JoinColumn $inverseJoinColumns + * @param array $options + */ + public function __construct( + public readonly string|null $name = null, + public readonly string|null $schema = null, + array|JoinColumn $joinColumns = [], + array|JoinColumn $inverseJoinColumns = [], + public readonly array $options = [], + ) { + $this->joinColumns = $joinColumns instanceof JoinColumn ? [$joinColumns] : $joinColumns; + $this->inverseJoinColumns = $inverseJoinColumns instanceof JoinColumn + ? [$inverseJoinColumns] + : $inverseJoinColumns; + } +} 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 @@ + */ +final class JoinTableMapping implements ArrayAccess +{ + use ArrayAccessImplementation; + + public bool|null $quoted = null; + + /** @var list */ + public array $joinColumns = []; + + /** @var list */ + public array $inverseJoinColumns = []; + + /** @var array */ + public array $options = []; + + public string|null $schema = null; + + public function __construct(public string $name) + { + } + + /** + * @param mixed[] $mappingArray + * @psalm-param array{ + * name: string, + * quoted?: bool|null, + * joinColumns?: mixed[], + * inverseJoinColumns?: mixed[], + * schema?: string|null, + * options?: array + * } $mappingArray + */ + public static function fromMappingArray(array $mappingArray): self + { + $mapping = new self($mappingArray['name']); + + foreach (['quoted', 'schema', 'options'] as $key) { + if (isset($mappingArray[$key])) { + $mapping->$key = $mappingArray[$key]; + } + } + + if (isset($mappingArray['joinColumns'])) { + foreach ($mappingArray['joinColumns'] as $column) { + $mapping->joinColumns[] = JoinColumnMapping::fromMappingArray($column); + } + } + + if (isset($mappingArray['inverseJoinColumns'])) { + foreach ($mappingArray['inverseJoinColumns'] as $column) { + $mapping->inverseJoinColumns[] = JoinColumnMapping::fromMappingArray($column); + } + } + + return $mapping; + } + + public function offsetSet(mixed $offset, mixed $value): void + { + if (in_array($offset, ['joinColumns', 'inverseJoinColumns'], true)) { + $joinColumns = []; + foreach ($value as $column) { + $joinColumns[] = JoinColumnMapping::fromMappingArray($column); + } + + $value = $joinColumns; + } + + $this->$offset = $value; + } + + /** @return mixed[] */ + public function toArray(): array + { + $array = (array) $this; + + $toArray = static fn (JoinColumnMapping $column): array => (array) $column; + $array['joinColumns'] = array_map($toArray, $array['joinColumns']); + $array['inverseJoinColumns'] = array_map($toArray, $array['inverseJoinColumns']); + + return $array; + } + + /** @return list */ + public function __sleep(): array + { + $serialized = []; + + foreach (['joinColumns', 'inverseJoinColumns', 'name', 'schema', 'options'] as $stringOrArrayKey) { + if ($this->$stringOrArrayKey !== null) { + $serialized[] = $stringOrArrayKey; + } + } + + foreach (['quoted'] as $boolKey) { + if ($this->$boolKey) { + $serialized[] = $boolKey; + } + } + + return $serialized; + } +} 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 @@ + */ + public array $joinTableColumns = []; + + /** @var array */ + public array $relationToSourceKeyColumns = []; + /** @var array */ + public array $relationToTargetKeyColumns = []; + + /** @return array */ + public function toArray(): array + { + $array = parent::toArray(); + + $array['joinTable'] = $this->joinTable->toArray(); + + return $array; + } + + /** + * @param mixed[] $mappingArray + * @psalm-param array{ + * fieldName: string, + * sourceEntity: class-string, + * targetEntity: class-string, + * cascade?: list<'persist'|'remove'|'detach'|'refresh'|'all'>, + * fetch?: ClassMetadata::FETCH_*|null, + * inherited?: class-string|null, + * declared?: class-string|null, + * cache?: array|null, + * id?: bool|null, + * isOnDeleteCascade?: bool|null, + * originalClass?: class-string|null, + * originalField?: string|null, + * orphanRemoval?: bool, + * unique?: bool|null, + * joinTable?: mixed[]|null, + * type?: int, + * isOwningSide: bool, + * } $mappingArray + */ + public static function fromMappingArrayAndNamingStrategy(array $mappingArray, NamingStrategy $namingStrategy): self + { + if (isset($mappingArray['joinTable']['joinColumns'])) { + foreach ($mappingArray['joinTable']['joinColumns'] as $key => $joinColumn) { + if (empty($joinColumn['name'])) { + $mappingArray['joinTable']['joinColumns'][$key]['name'] = $namingStrategy->joinKeyColumnName( + $mappingArray['sourceEntity'], + $joinColumn['referencedColumnName'] ?? null, + ); + } + } + } + + if (isset($mappingArray['joinTable']['inverseJoinColumns'])) { + foreach ($mappingArray['joinTable']['inverseJoinColumns'] as $key => $joinColumn) { + if (empty($joinColumn['name'])) { + $mappingArray['joinTable']['inverseJoinColumns'][$key]['name'] = $namingStrategy->joinKeyColumnName( + $mappingArray['targetEntity'], + $joinColumn['referencedColumnName'] ?? null, + ); + } + } + } + + // owning side MUST have a join table + if (! isset($mappingArray['joinTable']) || ! isset($mappingArray['joinTable']['name'])) { + $mappingArray['joinTable']['name'] = $namingStrategy->joinTableName( + $mappingArray['sourceEntity'], + $mappingArray['targetEntity'], + $mappingArray['fieldName'], + ); + } + + $mapping = parent::fromMappingArray($mappingArray); + + $selfReferencingEntityWithoutJoinColumns = $mapping->sourceEntity === $mapping->targetEntity + && $mapping->joinTable->joinColumns === [] + && $mapping->joinTable->inverseJoinColumns === []; + + if ($mapping->joinTable->joinColumns === []) { + $mapping->joinTable->joinColumns = [ + JoinColumnMapping::fromMappingArray([ + 'name' => $namingStrategy->joinKeyColumnName($mapping->sourceEntity, $selfReferencingEntityWithoutJoinColumns ? 'source' : null), + 'referencedColumnName' => $namingStrategy->referenceColumnName(), + 'onDelete' => 'CASCADE', + ]), + ]; + } + + if ($mapping->joinTable->inverseJoinColumns === []) { + $mapping->joinTable->inverseJoinColumns = [ + JoinColumnMapping::fromMappingArray([ + 'name' => $namingStrategy->joinKeyColumnName($mapping->targetEntity, $selfReferencingEntityWithoutJoinColumns ? 'target' : null), + 'referencedColumnName' => $namingStrategy->referenceColumnName(), + 'onDelete' => 'CASCADE', + ]), + ]; + } + + $mapping->joinTableColumns = []; + + foreach ($mapping->joinTable->joinColumns as $joinColumn) { + if (empty($joinColumn->referencedColumnName)) { + $joinColumn->referencedColumnName = $namingStrategy->referenceColumnName(); + } + + if ($joinColumn->name[0] === '`') { + $joinColumn->name = trim($joinColumn->name, '`'); + $joinColumn->quoted = true; + } + + if ($joinColumn->referencedColumnName[0] === '`') { + $joinColumn->referencedColumnName = trim($joinColumn->referencedColumnName, '`'); + $joinColumn->quoted = true; + } + + if (isset($joinColumn->onDelete) && strtolower($joinColumn->onDelete) === 'cascade') { + $mapping->isOnDeleteCascade = true; + } + + $mapping->relationToSourceKeyColumns[$joinColumn->name] = $joinColumn->referencedColumnName; + $mapping->joinTableColumns[] = $joinColumn->name; + } + + foreach ($mapping->joinTable->inverseJoinColumns as $inverseJoinColumn) { + if (empty($inverseJoinColumn->referencedColumnName)) { + $inverseJoinColumn->referencedColumnName = $namingStrategy->referenceColumnName(); + } + + if ($inverseJoinColumn->name[0] === '`') { + $inverseJoinColumn->name = trim($inverseJoinColumn->name, '`'); + $inverseJoinColumn->quoted = true; + } + + if ($inverseJoinColumn->referencedColumnName[0] === '`') { + $inverseJoinColumn->referencedColumnName = trim($inverseJoinColumn->referencedColumnName, '`'); + $inverseJoinColumn->quoted = true; + } + + if (isset($inverseJoinColumn->onDelete) && strtolower($inverseJoinColumn->onDelete) === 'cascade') { + $mapping->isOnDeleteCascade = true; + } + + $mapping->relationToTargetKeyColumns[$inverseJoinColumn->name] = $inverseJoinColumn->referencedColumnName; + $mapping->joinTableColumns[] = $inverseJoinColumn->name; + } + + return $mapping; + } + + /** @return list */ + public function __sleep(): array + { + $serialized = parent::__sleep(); + $serialized[] = 'joinTable'; + $serialized[] = 'joinTableColumns'; + + foreach (['relationToSourceKeyColumns', 'relationToTargetKeyColumns'] as $arrayKey) { + if ($this->$arrayKey !== null) { + $serialized[] = $arrayKey; + } + } + + return $serialized; + } +} 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 @@ +|null $repositoryClass */ + public function __construct( + public readonly string|null $repositoryClass = null, + ) { + } +} 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 @@ + $map + */ + public static function duplicateDiscriminatorEntry(string $className, array $entries, array $map): self + { + return new self( + 'The entries ' . implode(', ', $entries) . " in discriminator map of class '" . $className . "' is duplicated. " . + 'If the discriminator map is automatically generated you have to convert it to an explicit discriminator map now. ' . + 'The entries of the current map are: @DiscriminatorMap({' . implode(', ', array_map( + static fn ($a, $b) => sprintf("'%s': '%s'", $a, $b), + array_keys($map), + array_values($map), + )) . '})', + ); + } + + /** + * @param class-string $rootEntityClass + * @param class-string $childEntityClass + */ + public static function missingInheritanceTypeDeclaration(string $rootEntityClass, string $childEntityClass): self + { + return new self(sprintf( + "Entity class '%s' is a subclass of the root entity class '%s', but no inheritance mapping type was declared.", + $childEntityClass, + $rootEntityClass, + )); + } + + public static function missingDiscriminatorMap(string $className): self + { + return new self(sprintf( + "Entity class '%s' is using inheritance but no discriminator map was defined.", + $className, + )); + } + + public static function missingDiscriminatorColumn(string $className): self + { + return new self(sprintf( + "Entity class '%s' is using inheritance but no discriminator column was defined.", + $className, + )); + } + + public static function invalidDiscriminatorColumnType(string $className, string $type): self + { + return new self(sprintf( + "Discriminator column type on entity class '%s' is not allowed to be '%s'. 'string' or 'integer' type variables are suggested!", + $className, + $type, + )); + } + + public static function nameIsMandatoryForDiscriminatorColumns(string $className): self + { + return new self(sprintf("Discriminator column name on entity class '%s' is not defined.", $className)); + } + + public static function cannotVersionIdField(string $className, string $fieldName): self + { + return new self(sprintf( + "Setting Id field '%s' as versionable in entity class '%s' is not supported.", + $fieldName, + $className, + )); + } + + public static function duplicateColumnName(string $className, string $columnName): self + { + return new self("Duplicate definition of column '" . $columnName . "' on entity '" . $className . "' in a field or discriminator column mapping."); + } + + public static function illegalToManyAssociationOnMappedSuperclass(string $className, string $field): self + { + return new self("It is illegal to put an inverse side one-to-many or many-to-many association on mapped superclass '" . $className . '#' . $field . "'."); + } + + public static function cannotMapCompositePrimaryKeyEntitiesAsForeignId(string $className, string $targetEntity, string $targetField): self + { + return new self("It is not possible to map entity '" . $className . "' with a composite primary key " . + "as part of the primary key of another entity '" . $targetEntity . '#' . $targetField . "'."); + } + + public static function noSingleAssociationJoinColumnFound(string $className, string $field): self + { + return new self(sprintf("'%s#%s' is not an association with a single join column.", $className, $field)); + } + + public static function noFieldNameFoundForColumn(string $className, string $column): self + { + return new self(sprintf( + "Cannot find a field on '%s' that is mapped to column '%s'. Either the " . + 'field does not exist or an association exists but it has multiple join columns.', + $className, + $column, + )); + } + + public static function illegalOrphanRemovalOnIdentifierAssociation(string $className, string $field): self + { + return new self(sprintf( + "The orphan removal option is not allowed on an association that is part of the identifier in '%s#%s'.", + $className, + $field, + )); + } + + public static function illegalOrphanRemoval(string $className, string $field): self + { + return new self('Orphan removal is only allowed on one-to-one and one-to-many ' . + 'associations, but ' . $className . '#' . $field . ' is not.'); + } + + public static function illegalInverseIdentifierAssociation(string $className, string $field): self + { + return new self(sprintf( + "An inverse association is not allowed to be identifier in '%s#%s'.", + $className, + $field, + )); + } + + public static function illegalToManyIdentifierAssociation(string $className, string $field): self + { + return new self(sprintf( + "Many-to-many or one-to-many associations are not allowed to be identifier in '%s#%s'.", + $className, + $field, + )); + } + + public static function noInheritanceOnMappedSuperClass(string $className): self + { + return new self("It is not supported to define inheritance information on a mapped superclass '" . $className . "'."); + } + + public static function mappedClassNotPartOfDiscriminatorMap(string $className, string $rootClassName): self + { + return new self( + "Entity '" . $className . "' has to be part of the discriminator map of '" . $rootClassName . "' " . + "to be properly mapped in the inheritance hierarchy. Alternatively you can make '" . $className . "' an abstract class " . + 'to avoid this exception from occurring.', + ); + } + + public static function lifecycleCallbackMethodNotFound(string $className, string $methodName): self + { + return new self("Entity '" . $className . "' has no method '" . $methodName . "' to be registered as lifecycle callback."); + } + + /** @param class-string $className */ + public static function illegalLifecycleCallbackOnEmbeddedClass(string $event, string $className): self + { + return new self(sprintf( + <<<'EXCEPTION' + Context: Attempt to register lifecycle callback "%s" on embedded class "%s". + Problem: Registering lifecycle callbacks on embedded classes is not allowed. + EXCEPTION, + $event, + $className, + )); + } + + public static function entityListenerClassNotFound(string $listenerName, string $className): self + { + return new self(sprintf('Entity Listener "%s" declared on "%s" not found.', $listenerName, $className)); + } + + public static function entityListenerMethodNotFound(string $listenerName, string $methodName, string $className): self + { + return new self(sprintf('Entity Listener "%s" declared on "%s" has no method "%s".', $listenerName, $className, $methodName)); + } + + public static function duplicateEntityListener(string $listenerName, string $methodName, string $className): self + { + return new self(sprintf('Entity Listener "%s#%s()" in "%s" was already declared, but it must be declared only once.', $listenerName, $methodName, $className)); + } + + /** @param class-string $className */ + public static function invalidFetchMode(string $className, string $fetchMode): self + { + return new self("Entity '" . $className . "' has a mapping with invalid fetch mode '" . $fetchMode . "'"); + } + + public static function invalidGeneratedMode(int|string $generatedMode): self + { + return new self("Invalid generated mode '" . $generatedMode . "'"); + } + + public static function compositeKeyAssignedIdGeneratorRequired(string $className): self + { + return new self("Entity '" . $className . "' has a composite identifier but uses an ID generator other than manually assigning (Identity, Sequence). This is not supported."); + } + + public static function invalidTargetEntityClass(string $targetEntity, string $sourceEntity, string $associationName): self + { + return new self('The target-entity ' . $targetEntity . " cannot be found in '" . $sourceEntity . '#' . $associationName . "'."); + } + + /** @param string[] $cascades */ + public static function invalidCascadeOption(array $cascades, string $className, string $propertyName): self + { + $cascades = implode(', ', array_map(static fn (string $e): string => "'" . $e . "'", $cascades)); + + return new self(sprintf( + "You have specified invalid cascade options for %s::$%s: %s; available options: 'remove', 'persist', 'refresh', and 'detach'", + $className, + $propertyName, + $cascades, + )); + } + + public static function missingSequenceName(string $className): self + { + return new self( + sprintf('Missing "sequenceName" attribute for sequence id generator definition on class "%s".', $className), + ); + } + + public static function infiniteEmbeddableNesting(string $className, string $propertyName): self + { + return new self( + sprintf( + 'Infinite nesting detected for embedded property %s::%s. ' . + 'You cannot embed an embeddable from the same type inside an embeddable.', + $className, + $propertyName, + ), + ); + } + + public static function illegalOverrideOfInheritedProperty(string $className, string $propertyName, string $inheritFromClass): self + { + return new self( + sprintf( + '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.', + $className, + $propertyName, + $inheritFromClass, + ), + ); + } + + public static function invalidIndexConfiguration(string $className, string $indexName): self + { + return new self( + sprintf( + 'Index %s for entity %s should contain columns or fields values, but not both.', + $indexName, + $className, + ), + ); + } + + public static function invalidUniqueConstraintConfiguration(string $className, string $indexName): self + { + return new self( + sprintf( + 'Unique constraint %s for entity %s should contain columns or fields values, but not both.', + $indexName, + $className, + ), + ); + } + + public static function invalidOverrideType(string $expectdType, mixed $givenValue): self + { + return new self(sprintf( + 'Expected %s, but %s was given.', + $expectdType, + get_debug_type($givenValue), + )); + } + + public static function backedEnumTypeRequired(string $className, string $fieldName, string $enumType): self + { + return new self(sprintf( + 'Attempting to map a non-backed enum type %s in entity %s::$%s. Please use backed enums only', + $enumType, + $className, + $fieldName, + )); + } + + public static function nonEnumTypeMapped(string $className, string $fieldName, string $enumType): self + { + return new self(sprintf( + 'Attempting to map non-enum type %s as enum in entity %s::$%s', + $enumType, + $className, + $fieldName, + )); + } + + /** + * @param class-string $className + * @param class-string $enumType + */ + public static function invalidEnumValue( + string $className, + string $fieldName, + string $value, + string $enumType, + ValueError $previous, + ): self { + return new self(sprintf( + <<<'EXCEPTION' +Context: Trying to hydrate enum property "%s::$%s" +Problem: Case "%s" is not listed in enum "%s" +Solution: Either add the case to the enum type or migrate the database column to use another case of the enum +EXCEPTION + , + $className, + $fieldName, + $value, + $enumType, + ), 0, $previous); + } + + /** @param LibXMLError[] $errors */ + public static function fromLibXmlErrors(array $errors): self + { + $formatter = static fn (LibXMLError $error): string => sprintf( + 'libxml error: %s in %s at line %d', + $error->message, + $error->file, + $error->line, + ); + + return new self(implode(PHP_EOL, array_map($formatter, $errors))); + } + + public static function invalidAttributeOnEmbeddable(string $entityName, string $attributeName): self + { + return new self(sprintf( + 'Attribute "%s" on embeddable "%s" is not allowed.', + $attributeName, + $entityName, + )); + } +} 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 @@ +, + * fetch?: ClassMetadata::FETCH_*|null, + * inherited?: class-string|null, + * declared?: class-string|null, + * cache?: array|null, + * id?: bool|null, + * isOnDeleteCascade?: bool|null, + * originalClass?: class-string|null, + * originalField?: string|null, + * orphanRemoval?: bool, + * unique?: bool|null, + * joinTable?: mixed[]|null, + * type?: int, + * isOwningSide: bool, + * } $mappingArray + */ + public static function fromMappingArray(array $mappingArray): static + { + $mapping = parent::fromMappingArray($mappingArray); + + if ($mapping->orphanRemoval && ! $mapping->isCascadeRemove()) { + $mapping->cascade[] = 'remove'; + } + + return $mapping; + } + + /** + * @param mixed[] $mappingArray + * @psalm-param array{ + * fieldName: string, + * sourceEntity: class-string, + * targetEntity: class-string, + * cascade?: list<'persist'|'remove'|'detach'|'refresh'|'all'>, + * fetch?: ClassMetadata::FETCH_*|null, + * inherited?: class-string|null, + * declared?: class-string|null, + * cache?: array|null, + * id?: bool|null, + * isOnDeleteCascade?: bool|null, + * originalClass?: class-string|null, + * originalField?: string|null, + * orphanRemoval?: bool, + * unique?: bool|null, + * joinTable?: mixed[]|null, + * type?: int, + * isOwningSide: bool, + * } $mappingArray + */ + public static function fromMappingArrayAndName(array $mappingArray, string $name): static + { + $mapping = self::fromMappingArray($mappingArray); + + // OneToMany-side MUST be inverse (must have mappedBy) + if (! isset($mapping->mappedBy)) { + throw MappingException::oneToManyRequiresMappedBy($name, $mapping->fieldName); + } + + return $mapping; + } +} 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 @@ +|null $cascade + * @psalm-param 'LAZY'|'EAGER'|'EXTRA_LAZY' $fetch + */ + public function __construct( + public readonly string|null $targetEntity = null, + public readonly string|null $mappedBy = null, + public readonly string|null $inversedBy = null, + public readonly array|null $cascade = null, + public readonly string $fetch = 'LAZY', + public readonly bool $orphanRemoval = false, + ) { + } +} 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 @@ + $value */ + public function __construct( + public readonly array $value, + ) { + } +} 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 @@ + */ + public function __sleep(): array + { + $serialized = parent::__sleep(); + + if ($this->inversedBy !== null) { + $serialized[] = 'inversedBy'; + } + + return $serialized; + } +} 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 @@ + + */ + public function getIdentifierColumnNames(ClassMetadata $class, AbstractPlatform $platform): array; + + /** + * Gets the column alias. + */ + public function getColumnAlias( + string $columnName, + int $counter, + AbstractPlatform $platform, + ClassMetadata|null $class = null, + ): string; +} 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 @@ +getDeclaringClass()->name, $childProperty->getName()); + } + + public function getValue(object|null $object = null): mixed + { + $embeddedObject = $this->parentProperty->getValue($object); + + if ($embeddedObject === null) { + return null; + } + + return $this->childProperty->getValue($embeddedObject); + } + + public function setValue(mixed $object, mixed $value = null): void + { + $embeddedObject = $this->parentProperty->getValue($object); + + if ($embeddedObject === null) { + $this->instantiator ??= new Instantiator(); + + $embeddedObject = $this->instantiator->instantiate($this->embeddedClass); + + $this->parentProperty->setValue($object, $embeddedObject); + } + + $this->childProperty->setValue($embeddedObject, $value); + } +} 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 @@ + $enumType */ + public function __construct( + private readonly ReflectionProperty $originalReflectionProperty, + private readonly string $enumType, + ) { + parent::__construct( + $originalReflectionProperty->class, + $originalReflectionProperty->name, + ); + } + + public function getValue(object|null $object = null): int|string|array|null + { + if ($object === null) { + return null; + } + + $enum = $this->originalReflectionProperty->getValue($object); + + if ($enum === null) { + return null; + } + + if (is_array($enum)) { + return array_map( + static fn (BackedEnum $item): int|string => $item->value, + $enum, + ); + } + + return $enum->value; + } + + /** + * @param object $object + * @param int|string|int[]|string[]|BackedEnum|BackedEnum[]|null $value + */ + public function setValue(mixed $object, mixed $value = null): void + { + if ($value !== null) { + if (is_array($value)) { + $value = array_map(fn (int|string|BackedEnum $item): BackedEnum => $this->initializeEnumValue($object, $item), $value); + } else { + $value = $this->initializeEnumValue($object, $value); + } + } + + $this->originalReflectionProperty->setValue($object, $value); + } + + private function initializeEnumValue(object $object, int|string|BackedEnum $value): BackedEnum + { + if ($value instanceof BackedEnum) { + return $value; + } + + $enumType = $this->enumType; + + try { + return $enumType::from($value); + } catch (ValueError $e) { + throw MappingException::invalidEnumValue( + $object::class, + $this->originalReflectionProperty->name, + (string) $value, + $enumType, + $e, + ); + } + } +} 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 @@ +isReadOnly()) { + throw new InvalidArgumentException('Given property is not readonly.'); + } + + parent::__construct($wrappedProperty->class, $wrappedProperty->name); + } + + public function getValue(object|null $object = null): mixed + { + return $this->wrappedProperty->getValue(...func_get_args()); + } + + public function setValue(mixed $objectOrValue, mixed $value = null): void + { + if (func_num_args() < 2 || $objectOrValue === null || ! $this->isInitialized($objectOrValue)) { + $this->wrappedProperty->setValue(...func_get_args()); + + return; + } + + assert(is_object($objectOrValue)); + + if (parent::getValue($objectOrValue) !== $value) { + throw new LogicException(sprintf('Attempting to change readonly property %s::$%s.', $this->class, $this->name)); + } + } +} 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 @@ +|null $indexes + * @param array|null $uniqueConstraints + * @param array $options + */ + public function __construct( + public readonly string|null $name = null, + public readonly string|null $schema = null, + public readonly array|null $indexes = null, + public readonly array|null $uniqueConstraints = null, + public readonly array $options = [], + ) { + if ($this->indexes !== null) { + Deprecation::trigger( + 'doctrine/orm', + 'https://github.com/doctrine/orm/pull/11357', + '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.', + self::class, + Index::class, + ); + } + + if ($this->uniqueConstraints !== null) { + Deprecation::trigger( + 'doctrine/orm', + 'https://github.com/doctrine/orm/pull/11357', + '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.', + self::class, + UniqueConstraint::class, + ); + } + } +} 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 @@ +indexBy() */ + public function isIndexed(): bool; + + public function indexBy(): string; + + /** @return array */ + public function orderBy(): array; +} 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 @@ + + */ + public array $orderBy = []; + + /** @return array */ + final public function orderBy(): array + { + return $this->orderBy; + } + + /** @psalm-assert-if-true !null $this->indexBy */ + final public function isIndexed(): bool + { + return $this->indexBy !== null; + } + + final public function indexBy(): string + { + if (! $this->isIndexed()) { + throw new LogicException(sprintf( + 'This mapping is not indexed. Use %s::isIndexed() to check that before calling %s.', + self::class, + __METHOD__, + )); + } + + return $this->indexBy; + } + + /** @return list */ + public function __sleep(): array + { + $serialized = parent::__sleep(); + + if ($this->indexBy !== null) { + $serialized[] = 'indexBy'; + } + + if ($this->orderBy !== []) { + $serialized[] = 'orderBy'; + } + + return $serialized; + } +} 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 @@ +, + * fetch?: ClassMetadata::FETCH_*|null, + * inherited?: class-string|null, + * declared?: class-string|null, + * cache?: array|null, + * id?: bool|null, + * isOnDeleteCascade?: bool|null, + * originalClass?: class-string|null, + * originalField?: string|null, + * orphanRemoval?: bool, + * unique?: bool|null, + * joinTable?: mixed[]|null, + * type?: int, + * isOwningSide: bool, + * } $mappingArray + */ + public static function fromMappingArrayAndName( + array $mappingArray, + string $name, + ): static { + $mapping = static::fromMappingArray($mappingArray); + + if (isset($mapping->id) && $mapping->id === true) { + throw MappingException::illegalInverseIdentifierAssociation($name, $mapping->fieldName); + } + + if ($mapping->orphanRemoval) { + if (! $mapping->isCascadeRemove()) { + $mapping->cascade[] = 'remove'; + } + + $mapping->unique = null; + } + + return $mapping; + } +} 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 @@ + */ + public array $sourceToTargetKeyColumns = []; + + /** @var array */ + public array $targetToSourceKeyColumns = []; + + /** @var list */ + public array $joinColumns = []; + + /** @var array */ + public array $joinColumnFieldNames = []; + + /** + * @param array $mappingArray + * @psalm-param array{ + * fieldName: string, + * sourceEntity: class-string, + * targetEntity: class-string, + * cascade?: list<'persist'|'remove'|'detach'|'refresh'|'all'>, + * fetch?: ClassMetadata::FETCH_*|null, + * inherited?: class-string|null, + * declared?: class-string|null, + * cache?: array|null, + * id?: bool|null, + * isOnDeleteCascade?: bool|null, + * originalClass?: class-string|null, + * originalField?: string|null, + * orphanRemoval?: bool, + * unique?: bool|null, + * joinTable?: mixed[]|null, + * type?: int, + * isOwningSide: bool, + * joinColumns?: mixed[]|null, + * } $mappingArray + */ + public static function fromMappingArray(array $mappingArray): static + { + $joinColumns = $mappingArray['joinColumns'] ?? []; + unset($mappingArray['joinColumns']); + + $instance = parent::fromMappingArray($mappingArray); + assert($instance->isToOneOwningSide()); + + foreach ($joinColumns as $column) { + $instance->joinColumns[] = JoinColumnMapping::fromMappingArray($column); + } + + if ($instance->orphanRemoval) { + if (! $instance->isCascadeRemove()) { + $instance->cascade[] = 'remove'; + } + + $instance->unique = null; + } + + return $instance; + } + + /** + * @param mixed[] $mappingArray + * @param class-string $name + * @psalm-param array{ + * fieldName: string, + * sourceEntity: class-string, + * targetEntity: class-string, + * cascade?: list<'persist'|'remove'|'detach'|'refresh'|'all'>, + * fetch?: ClassMetadata::FETCH_*|null, + * inherited?: class-string|null, + * declared?: class-string|null, + * cache?: array|null, + * id?: bool|null, + * isOnDeleteCascade?: bool|null, + * originalClass?: class-string|null, + * originalField?: string|null, + * orphanRemoval?: bool, + * unique?: bool|null, + * joinTable?: mixed[]|null, + * type?: int, + * isOwningSide: bool, + * joinColumns?: mixed[]|null, + * } $mappingArray + */ + public static function fromMappingArrayAndName( + array $mappingArray, + NamingStrategy $namingStrategy, + string $name, + array|null $table, + bool $isInheritanceTypeSingleTable, + ): static { + if (isset($mappingArray['joinColumns'])) { + foreach ($mappingArray['joinColumns'] as $index => $joinColumn) { + if (empty($joinColumn['name'])) { + $mappingArray['joinColumns'][$index]['name'] = $namingStrategy->joinColumnName($mappingArray['fieldName'], $name); + } + } + } + + $mapping = static::fromMappingArray($mappingArray); + + assert($mapping->isToOneOwningSide()); + if (empty($mapping->joinColumns)) { + // Apply default join column + $mapping->joinColumns = [ + JoinColumnMapping::fromMappingArray([ + 'name' => $namingStrategy->joinColumnName($mapping->fieldName, $name), + 'referencedColumnName' => $namingStrategy->referenceColumnName(), + ]), + ]; + } + + $uniqueConstraintColumns = []; + + foreach ($mapping->joinColumns as $joinColumn) { + if ($mapping->isOneToOne() && ! $isInheritanceTypeSingleTable) { + if (count($mapping->joinColumns) === 1) { + if (empty($mapping->id)) { + $joinColumn->unique = true; + } + } else { + $uniqueConstraintColumns[] = $joinColumn->name; + } + } + + if (empty($joinColumn->referencedColumnName)) { + $joinColumn->referencedColumnName = $namingStrategy->referenceColumnName(); + } + + if ($joinColumn->name[0] === '`') { + $joinColumn->name = trim($joinColumn->name, '`'); + $joinColumn->quoted = true; + } + + if ($joinColumn->referencedColumnName[0] === '`') { + $joinColumn->referencedColumnName = trim($joinColumn->referencedColumnName, '`'); + $joinColumn->quoted = true; + } + + $mapping->sourceToTargetKeyColumns[$joinColumn->name] = $joinColumn->referencedColumnName; + $mapping->joinColumnFieldNames[$joinColumn->name] = $joinColumn->fieldName ?? $joinColumn->name; + } + + if ($uniqueConstraintColumns) { + if (! $table) { + throw new RuntimeException('ClassMetadata::setTable() has to be called before defining a one to one relationship.'); + } + + $table['uniqueConstraints'][$mapping->fieldName . '_uniq'] = ['columns' => $uniqueConstraintColumns]; + } + + $mapping->targetToSourceKeyColumns = array_flip($mapping->sourceToTargetKeyColumns); + + return $mapping; + } + + public function offsetSet(mixed $offset, mixed $value): void + { + if ($offset === 'joinColumns') { + $joinColumns = []; + foreach ($value as $column) { + $joinColumns[] = JoinColumnMapping::fromMappingArray($column); + } + + $this->joinColumns = $joinColumns; + + return; + } + + parent::offsetSet($offset, $value); + } + + /** @return array */ + public function toArray(): array + { + $array = parent::toArray(); + + $joinColumns = []; + foreach ($array['joinColumns'] as $column) { + $joinColumns[] = (array) $column; + } + + $array['joinColumns'] = $joinColumns; + + return $array; + } + + /** @return list */ + public function __sleep(): array + { + return [ + ...parent::__sleep(), + 'joinColumns', + 'joinColumnFieldNames', + 'sourceToTargetKeyColumns', + 'targetToSourceKeyColumns', + ]; + } +} 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 @@ +, type?: string} $mapping The field mapping to validate & complete. + * + * @return array{fieldName: string, enumType?: class-string, type?: string} The updated mapping. + */ + public function validateAndComplete(array $mapping, ReflectionProperty $field): array; +} 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 @@ +case; + } + + /** + * Sets string case CASE_LOWER | CASE_UPPER. + * Alphabetic characters converted to lowercase or uppercase. + */ + public function setCase(int $case): void + { + $this->case = $case; + } + + public function classToTableName(string $className): string + { + if (str_contains($className, '\\')) { + $className = substr($className, strrpos($className, '\\') + 1); + } + + return $this->underscore($className); + } + + public function propertyToColumnName(string $propertyName, string $className): string + { + return $this->underscore($propertyName); + } + + public function embeddedFieldToColumnName( + string $propertyName, + string $embeddedColumnName, + string $className, + string $embeddedClassName, + ): string { + return $this->underscore($propertyName) . '_' . $embeddedColumnName; + } + + public function referenceColumnName(): string + { + return $this->case === CASE_UPPER ? 'ID' : 'id'; + } + + public function joinColumnName(string $propertyName, string $className): string + { + return $this->underscore($propertyName) . '_' . $this->referenceColumnName(); + } + + public function joinTableName( + string $sourceEntity, + string $targetEntity, + string $propertyName, + ): string { + return $this->classToTableName($sourceEntity) . '_' . $this->classToTableName($targetEntity); + } + + public function joinKeyColumnName( + string $entityName, + string|null $referencedColumnName, + ): string { + return $this->classToTableName($entityName) . '_' . + ($referencedColumnName ?: $this->referenceColumnName()); + } + + private function underscore(string $string): string + { + $string = preg_replace('/(?<=[a-z0-9])([A-Z])/', '_$1', $string); + + if ($this->case === CASE_UPPER) { + return strtoupper($string); + } + + return strtolower($string); + } +} 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 @@ +|null $columns + * @param array|null $fields + * @param array|null $options + */ + public function __construct( + public readonly string|null $name = null, + public readonly array|null $columns = null, + public readonly array|null $fields = null, + public readonly array|null $options = null, + ) { + } +} 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 @@ +sql = $sql; + + return $this; + } + + public function getSQL(): string + { + return $this->sql; + } + + protected function _doExecute(): Result|int + { + $parameters = []; + $types = []; + + foreach ($this->getParameters() as $parameter) { + $name = $parameter->getName(); + $value = $this->processParameterValue($parameter->getValue()); + $type = $parameter->getValue() === $value + ? $parameter->getType() + : ParameterTypeInferer::inferType($value); + + $parameters[$name] = $value; + $types[$name] = $type; + } + + if ($parameters && is_int(key($parameters))) { + ksort($parameters); + ksort($types); + + $parameters = array_values($parameters); + $types = array_values($types); + } + + return $this->em->getConnection()->executeQuery( + $this->sql, + $parameters, + $types, + $this->queryCacheProfile, + ); + } +} 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 @@ + $newEntitiesWithAssociations */ + public static function newEntitiesFoundThroughRelationships(array $newEntitiesWithAssociations): self + { + $errorMessages = array_map( + static function (array $newEntityWithAssociation): string { + [$associationMapping, $entity] = $newEntityWithAssociation; + + return self::newEntityFoundThroughRelationshipMessage($associationMapping, $entity); + }, + $newEntitiesWithAssociations, + ); + + if (count($errorMessages) === 1) { + return new self(reset($errorMessages)); + } + + return new self( + 'Multiple non-persisted new entities were found through the given association graph:' + . "\n\n * " + . implode("\n * ", $errorMessages), + ); + } + + public static function newEntityFoundThroughRelationship(AssociationMapping $associationMapping, object $entry): self + { + return new self(self::newEntityFoundThroughRelationshipMessage($associationMapping, $entry)); + } + + public static function detachedEntityFoundThroughRelationship(AssociationMapping $assoc, object $entry): self + { + return new self('A detached entity of type ' . $assoc->targetEntity . ' (' . self::objToStr($entry) . ') ' + . " was found through the relationship '" . $assoc->sourceEntity . '#' . $assoc->fieldName . "' " + . 'during cascading a persist operation.'); + } + + public static function entityNotManaged(object $entity): self + { + return new self('Entity ' . self::objToStr($entity) . ' is not managed. An entity is managed if its fetched ' . + 'from the database or registered as new through EntityManager#persist'); + } + + public static function entityHasNoIdentity(object $entity, string $operation): self + { + return new self('Entity has no identity, therefore ' . $operation . ' cannot be performed. ' . self::objToStr($entity)); + } + + public static function entityIsRemoved(object $entity, string $operation): self + { + return new self('Entity is removed, therefore ' . $operation . ' cannot be performed. ' . self::objToStr($entity)); + } + + public static function detachedEntityCannot(object $entity, string $operation): self + { + return new self('Detached entity ' . self::objToStr($entity) . ' cannot be ' . $operation); + } + + public static function invalidObject(string $context, mixed $given, int $parameterIndex = 1): self + { + return new self($context . ' expects parameter ' . $parameterIndex . + ' to be an entity object, ' . gettype($given) . ' given.'); + } + + public static function invalidCompositeIdentifier(): self + { + return new self('Binding an entity with a composite primary key to a query is not supported. ' . + 'You should split the parameter into the explicit fields and bind them separately.'); + } + + public static function invalidIdentifierBindingEntity(string $class): self + { + return new self(sprintf( + <<<'EXCEPTION' +Binding entities to query parameters only allowed for entities that have an identifier. +Class "%s" does not have an identifier. +EXCEPTION + , + $class, + )); + } + + public static function invalidAssociation(ClassMetadata $targetClass, AssociationMapping $assoc, mixed $actualValue): self + { + $expectedType = $targetClass->getName(); + + return new self(sprintf( + 'Expected value of type "%s" for association field "%s#$%s", got "%s" instead.', + $expectedType, + $assoc->sourceEntity, + $assoc->fieldName, + get_debug_type($actualValue), + )); + } + + public static function invalidAutoGenerateMode(mixed $value): self + { + return new self(sprintf('Invalid auto generate mode "%s" given.', is_scalar($value) ? (string) $value : get_debug_type($value))); + } + + public static function missingPrimaryKeyValue(string $className, string $idField): self + { + return new self(sprintf('Missing value for primary key %s on %s', $idField, $className)); + } + + public static function proxyDirectoryRequired(): self + { + return new self('You must configure a proxy directory. See docs for details'); + } + + public static function proxyNamespaceRequired(): self + { + return new self('You must configure a proxy namespace'); + } + + public static function proxyDirectoryNotWritable(string $proxyDirectory): self + { + return new self(sprintf('Your proxy directory "%s" must be writable', $proxyDirectory)); + } + + /** + * Helper method to show an object as string. + */ + private static function objToStr(object $obj): string + { + return $obj instanceof Stringable ? (string) $obj : get_debug_type($obj) . '@' . spl_object_id($obj); + } + + private static function newEntityFoundThroughRelationshipMessage(AssociationMapping $associationMapping, object $entity): string + { + return 'A new entity was found through the relationship \'' + . $associationMapping->sourceEntity . '#' . $associationMapping->fieldName . '\' that was not' + . ' configured to cascade persist operations for entity: ' . self::objToStr($entity) . '.' + . ' To solve this issue: Either explicitly call EntityManager#persist()' + . ' on this unknown entity or configure cascade persist' + . ' this association in the mapping for example @ManyToOne(..,cascade={"persist"}).' + . ($entity instanceof Stringable + ? '' + : ' If you cannot find out which entity causes the problem implement \'' + . $associationMapping->targetEntity . '#__toString()\' to get a clue.' + ); + } +} 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 @@ +setMetadataDriverImpl(new AttributeDriver($paths)); + + return $config; + } + + /** + * Creates a configuration with an XML metadata driver. + * + * @param string[] $paths + */ + public static function createXMLMetadataConfiguration( + array $paths, + bool $isDevMode = false, + string|null $proxyDir = null, + CacheItemPoolInterface|null $cache = null, + bool $isXsdValidationEnabled = true, + ): Configuration { + $config = self::createConfiguration($isDevMode, $proxyDir, $cache); + $config->setMetadataDriverImpl(new XmlDriver($paths, XmlDriver::DEFAULT_FILE_EXTENSION, $isXsdValidationEnabled)); + + return $config; + } + + /** + * Creates a configuration without a metadata driver. + */ + public static function createConfiguration( + bool $isDevMode = false, + string|null $proxyDir = null, + CacheItemPoolInterface|null $cache = null, + ): Configuration { + $proxyDir = $proxyDir ?: sys_get_temp_dir(); + + $cache = self::createCacheInstance($isDevMode, $proxyDir, $cache); + + $config = new Configuration(); + + $config->setMetadataCache($cache); + $config->setQueryCache($cache); + $config->setResultCache($cache); + $config->setProxyDir($proxyDir); + $config->setProxyNamespace('DoctrineProxies'); + $config->setAutoGenerateProxyClasses($isDevMode); + + return $config; + } + + private static function createCacheInstance( + bool $isDevMode, + string $proxyDir, + CacheItemPoolInterface|null $cache, + ): CacheItemPoolInterface { + if ($cache !== null) { + return $cache; + } + + if (! class_exists(ArrayAdapter::class)) { + throw new RuntimeException( + 'The Doctrine setup tool cannot configure caches without symfony/cache.' + . ' Please add symfony/cache as explicit dependency or pass your own cache implementation.', + ); + } + + if ($isDevMode) { + return new ArrayAdapter(); + } + + $namespace = 'dc2_' . md5($proxyDir); + + if (extension_loaded('apcu') && apcu_enabled()) { + return new ApcuAdapter($namespace); + } + + if (MemcachedAdapter::isSupported()) { + return new MemcachedAdapter(MemcachedAdapter::createConnection('memcached://127.0.0.1'), $namespace); + } + + if (extension_loaded('redis')) { + $redis = new Redis(); + $redis->connect('127.0.0.1'); + + return new RedisAdapter($redis, $namespace); + } + + return new ArrayAdapter(); + } + + private function __construct() + { + } +} 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 @@ +entity; + } + + /** @param object|class-string $entity */ + public static function lockFailed(object|string $entity): self + { + return new self('The optimistic lock on an entity failed.', $entity); + } + + public static function lockFailedVersionMismatch( + object $entity, + int|string|DateTimeInterface $expectedLockVersion, + int|string|DateTimeInterface $actualLockVersion, + ): self { + $expectedLockVersion = $expectedLockVersion instanceof DateTimeInterface ? $expectedLockVersion->getTimestamp() : $expectedLockVersion; + $actualLockVersion = $actualLockVersion instanceof DateTimeInterface ? $actualLockVersion->getTimestamp() : $actualLockVersion; + + return new self('The optimistic lock failed, version ' . $expectedLockVersion . ' was expected, but is actually ' . $actualLockVersion, $entity); + } + + public static function notVersioned(string $entityName): self + { + return new self('Cannot obtain optimistic lock on unversioned entity ' . $entityName, null); + } +} 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 @@ + + * @template-implements Selectable + */ +final class PersistentCollection extends AbstractLazyCollection implements Selectable +{ + /** + * A snapshot of the collection at the moment it was fetched from the database. + * This is used to create a diff of the collection at commit time. + * + * @psalm-var array + */ + private array $snapshot = []; + + /** + * The entity that owns this collection. + */ + private object|null $owner = null; + + /** + * The association mapping the collection belongs to. + * This is currently either a OneToManyMapping or a ManyToManyMapping. + * + * @var (AssociationMapping&ToManyAssociationMapping)|null + */ + private AssociationMapping|null $association = null; + + /** + * The name of the field on the target entities that points to the owner + * of the collection. This is only set if the association is bi-directional. + */ + private string|null $backRefFieldName = null; + + /** + * Whether the collection is dirty and needs to be synchronized with the database + * when the UnitOfWork that manages its persistent state commits. + */ + private bool $isDirty = false; + + /** + * Creates a new persistent collection. + * + * @param EntityManagerInterface $em The EntityManager the collection will be associated with. + * @param ClassMetadata $typeClass The class descriptor of the entity type of this collection. + * @psalm-param Collection&Selectable $collection The collection elements. + */ + public function __construct( + private EntityManagerInterface|null $em, + private readonly ClassMetadata|null $typeClass, + Collection $collection, + ) { + $this->collection = $collection; + $this->initialized = true; + } + + /** + * INTERNAL: + * Sets the collection's owning entity together with the AssociationMapping that + * describes the association between the owner and the elements of the collection. + */ + public function setOwner(object $entity, AssociationMapping&ToManyAssociationMapping $assoc): void + { + $this->owner = $entity; + $this->association = $assoc; + $this->backRefFieldName = $assoc->isOwningSide() ? $assoc->inversedBy : $assoc->mappedBy; + } + + /** + * INTERNAL: + * Gets the collection owner. + */ + public function getOwner(): object|null + { + return $this->owner; + } + + public function getTypeClass(): ClassMetadata + { + assert($this->typeClass !== null); + + return $this->typeClass; + } + + private function getUnitOfWork(): UnitOfWork + { + assert($this->em !== null); + + return $this->em->getUnitOfWork(); + } + + /** + * INTERNAL: + * Adds an element to a collection during hydration. This will automatically + * complete bidirectional associations in the case of a one-to-many association. + */ + public function hydrateAdd(mixed $element): void + { + $this->unwrap()->add($element); + + // If _backRefFieldName is set and its a one-to-many association, + // we need to set the back reference. + if ($this->backRefFieldName && $this->getMapping()->isOneToMany()) { + assert($this->typeClass !== null); + // Set back reference to owner + $this->typeClass->reflFields[$this->backRefFieldName]->setValue( + $element, + $this->owner, + ); + + $this->getUnitOfWork()->setOriginalEntityProperty( + spl_object_id($element), + $this->backRefFieldName, + $this->owner, + ); + } + } + + /** + * INTERNAL: + * Sets a keyed element in the collection during hydration. + */ + public function hydrateSet(mixed $key, mixed $element): void + { + $this->unwrap()->set($key, $element); + + // If _backRefFieldName is set, then the association is bidirectional + // and we need to set the back reference. + if ($this->backRefFieldName && $this->getMapping()->isOneToMany()) { + assert($this->typeClass !== null); + // Set back reference to owner + $this->typeClass->reflFields[$this->backRefFieldName]->setValue( + $element, + $this->owner, + ); + } + } + + /** + * Initializes the collection by loading its contents from the database + * if the collection is not yet initialized. + */ + public function initialize(): void + { + if ($this->initialized || ! $this->association) { + return; + } + + $this->doInitialize(); + + $this->initialized = true; + } + + /** + * INTERNAL: + * Tells this collection to take a snapshot of its current state. + */ + public function takeSnapshot(): void + { + $this->snapshot = $this->unwrap()->toArray(); + $this->isDirty = false; + } + + /** + * INTERNAL: + * Returns the last snapshot of the elements in the collection. + * + * @psalm-return array The last snapshot of the elements. + */ + public function getSnapshot(): array + { + return $this->snapshot; + } + + /** + * INTERNAL: + * getDeleteDiff + * + * @return mixed[] + */ + public function getDeleteDiff(): array + { + $collectionItems = $this->unwrap()->toArray(); + + return array_values(array_diff_key( + array_combine(array_map('spl_object_id', $this->snapshot), $this->snapshot), + array_combine(array_map('spl_object_id', $collectionItems), $collectionItems), + )); + } + + /** + * INTERNAL: + * getInsertDiff + * + * @return mixed[] + */ + public function getInsertDiff(): array + { + $collectionItems = $this->unwrap()->toArray(); + + return array_values(array_diff_key( + array_combine(array_map('spl_object_id', $collectionItems), $collectionItems), + array_combine(array_map('spl_object_id', $this->snapshot), $this->snapshot), + )); + } + + /** INTERNAL: Gets the association mapping of the collection. */ + public function getMapping(): AssociationMapping&ToManyAssociationMapping + { + if ($this->association === null) { + throw new UnexpectedValueException('The underlying association mapping is null although it should not be'); + } + + return $this->association; + } + + /** + * Marks this collection as changed/dirty. + */ + private function changed(): void + { + if ($this->isDirty) { + return; + } + + $this->isDirty = true; + } + + /** + * Gets a boolean flag indicating whether this collection is dirty which means + * its state needs to be synchronized with the database. + */ + public function isDirty(): bool + { + return $this->isDirty; + } + + /** + * Sets a boolean flag, indicating whether this collection is dirty. + */ + public function setDirty(bool $dirty): void + { + $this->isDirty = $dirty; + } + + /** + * Sets the initialized flag of the collection, forcing it into that state. + */ + public function setInitialized(bool $bool): void + { + $this->initialized = $bool; + } + + public function remove(string|int $key): mixed + { + // TODO: If the keys are persistent as well (not yet implemented) + // and the collection is not initialized and orphanRemoval is + // not used we can issue a straight SQL delete/update on the + // association (table). Without initializing the collection. + $removed = parent::remove($key); + + if (! $removed) { + return $removed; + } + + $this->changed(); + + if ( + $this->association !== null && + $this->association->isToMany() && + $this->owner && + $this->getMapping()->orphanRemoval + ) { + $this->getUnitOfWork()->scheduleOrphanRemoval($removed); + } + + return $removed; + } + + public function removeElement(mixed $element): bool + { + $removed = parent::removeElement($element); + + if (! $removed) { + return $removed; + } + + $this->changed(); + + if ( + $this->association !== null && + $this->association->isToMany() && + $this->owner && + $this->getMapping()->orphanRemoval + ) { + $this->getUnitOfWork()->scheduleOrphanRemoval($element); + } + + return $removed; + } + + public function containsKey(mixed $key): bool + { + if ( + ! $this->initialized && $this->getMapping()->fetch === ClassMetadata::FETCH_EXTRA_LAZY + && isset($this->getMapping()->indexBy) + ) { + $persister = $this->getUnitOfWork()->getCollectionPersister($this->getMapping()); + + return $this->unwrap()->containsKey($key) || $persister->containsKey($this, $key); + } + + return parent::containsKey($key); + } + + public function contains(mixed $element): bool + { + if (! $this->initialized && $this->getMapping()->fetch === ClassMetadata::FETCH_EXTRA_LAZY) { + $persister = $this->getUnitOfWork()->getCollectionPersister($this->getMapping()); + + return $this->unwrap()->contains($element) || $persister->contains($this, $element); + } + + return parent::contains($element); + } + + public function get(string|int $key): mixed + { + if ( + ! $this->initialized + && $this->getMapping()->fetch === ClassMetadata::FETCH_EXTRA_LAZY + && isset($this->getMapping()->indexBy) + ) { + assert($this->em !== null); + assert($this->typeClass !== null); + if (! $this->typeClass->isIdentifierComposite && $this->typeClass->isIdentifier($this->getMapping()->indexBy)) { + return $this->em->find($this->typeClass->name, $key); + } + + return $this->getUnitOfWork()->getCollectionPersister($this->getMapping())->get($this, $key); + } + + return parent::get($key); + } + + public function count(): int + { + if (! $this->initialized && $this->association !== null && $this->getMapping()->fetch === ClassMetadata::FETCH_EXTRA_LAZY) { + $persister = $this->getUnitOfWork()->getCollectionPersister($this->association); + + return $persister->count($this) + ($this->isDirty ? $this->unwrap()->count() : 0); + } + + return parent::count(); + } + + public function set(string|int $key, mixed $value): void + { + parent::set($key, $value); + + $this->changed(); + + if (is_object($value) && $this->em) { + $this->getUnitOfWork()->cancelOrphanRemoval($value); + } + } + + public function add(mixed $value): bool + { + $this->unwrap()->add($value); + + $this->changed(); + + if (is_object($value) && $this->em) { + $this->getUnitOfWork()->cancelOrphanRemoval($value); + } + + return true; + } + + public function offsetExists(mixed $offset): bool + { + return $this->containsKey($offset); + } + + public function offsetGet(mixed $offset): mixed + { + return $this->get($offset); + } + + public function offsetSet(mixed $offset, mixed $value): void + { + if (! isset($offset)) { + $this->add($value); + + return; + } + + $this->set($offset, $value); + } + + public function offsetUnset(mixed $offset): void + { + $this->remove($offset); + } + + public function isEmpty(): bool + { + return $this->unwrap()->isEmpty() && $this->count() === 0; + } + + public function clear(): void + { + if ($this->initialized && $this->isEmpty()) { + $this->unwrap()->clear(); + + return; + } + + $uow = $this->getUnitOfWork(); + $association = $this->getMapping(); + + if ( + $association->isToMany() && + $association->orphanRemoval && + $this->owner + ) { + // we need to initialize here, as orphan removal acts like implicit cascadeRemove, + // hence for event listeners we need the objects in memory. + $this->initialize(); + + foreach ($this->unwrap() as $element) { + $uow->scheduleOrphanRemoval($element); + } + } + + $this->unwrap()->clear(); + + $this->initialized = true; // direct call, {@link initialize()} is too expensive + + if ($association->isOwningSide() && $this->owner) { + $this->changed(); + + $uow->scheduleCollectionDeletion($this); + + $this->takeSnapshot(); + } + } + + /** + * Called by PHP when this collection is serialized. Ensures that only the + * elements are properly serialized. + * + * Internal note: Tried to implement Serializable first but that did not work well + * with circular references. This solution seems simpler and works well. + * + * @return string[] + * @psalm-return array{0: string, 1: string} + */ + public function __sleep(): array + { + return ['collection', 'initialized']; + } + + public function __wakeup(): void + { + $this->em = null; + } + + /** + * Extracts a slice of $length elements starting at position $offset from the Collection. + * + * If $length is null it returns all elements from $offset to the end of the Collection. + * Keys have to be preserved by this method. Calling this method will only return the + * selected slice and NOT change the elements contained in the collection slice is called on. + * + * @return mixed[] + * @psalm-return array + */ + public function slice(int $offset, int|null $length = null): array + { + if (! $this->initialized && ! $this->isDirty && $this->getMapping()->fetch === ClassMetadata::FETCH_EXTRA_LAZY) { + $persister = $this->getUnitOfWork()->getCollectionPersister($this->getMapping()); + + return $persister->slice($this, $offset, $length); + } + + return parent::slice($offset, $length); + } + + /** + * Cleans up internal state of cloned persistent collection. + * + * The following problems have to be prevented: + * 1. Added entities are added to old PC + * 2. New collection is not dirty, if reused on other entity nothing + * changes. + * 3. Snapshot leads to invalid diffs being generated. + * 4. Lazy loading grabs entities from old owner object. + * 5. New collection is connected to old owner and leads to duplicate keys. + */ + public function __clone() + { + if (is_object($this->collection)) { + $this->collection = clone $this->collection; + } + + $this->initialize(); + + $this->owner = null; + $this->snapshot = []; + + $this->changed(); + } + + /** + * Selects all elements from a selectable that match the expression and + * return a new collection containing these elements. + * + * @psalm-return Collection + * + * @throws RuntimeException + */ + public function matching(Criteria $criteria): Collection + { + if ($this->isDirty) { + $this->initialize(); + } + + if ($this->initialized) { + return $this->unwrap()->matching($criteria); + } + + $association = $this->getMapping(); + if ($association->isManyToMany()) { + $persister = $this->getUnitOfWork()->getCollectionPersister($association); + + return new ArrayCollection($persister->loadCriteria($this, $criteria)); + } + + $builder = Criteria::expr(); + $ownerExpression = $builder->eq($this->backRefFieldName, $this->owner); + $expression = $criteria->getWhereExpression(); + $expression = $expression ? $builder->andX($expression, $ownerExpression) : $ownerExpression; + + $criteria = clone $criteria; + $criteria->where($expression); + $criteria->orderBy( + $criteria->orderings() ?: array_map( + static fn (string $order): Order => Order::from(strtoupper($order)), + $association->orderBy(), + ), + ); + + $persister = $this->getUnitOfWork()->getEntityPersister($association->targetEntity); + + return $association->fetch === ClassMetadata::FETCH_EXTRA_LAZY + ? new LazyCriteriaCollection($persister, $criteria) + : new ArrayCollection($persister->loadCriteria($criteria)); + } + + /** + * Retrieves the wrapped Collection instance. + * + * @return Collection&Selectable + */ + public function unwrap(): Selectable&Collection + { + assert($this->collection instanceof Collection); + assert($this->collection instanceof Selectable); + + return $this->collection; + } + + protected function doInitialize(): void + { + // Has NEW objects added through add(). Remember them. + $newlyAddedDirtyObjects = []; + + if ($this->isDirty) { + $newlyAddedDirtyObjects = $this->unwrap()->toArray(); + } + + $this->unwrap()->clear(); + $this->getUnitOfWork()->loadCollection($this); + $this->takeSnapshot(); + + if ($newlyAddedDirtyObjects) { + $this->restoreNewObjectsInDirtyCollection($newlyAddedDirtyObjects); + } + } + + /** + * @param object[] $newObjects + * + * Note: the only reason why this entire looping/complexity is performed via `spl_object_id` + * is because we want to prevent using `array_udiff()`, which is likely to cause very + * high overhead (complexity of O(n^2)). `array_diff_key()` performs the operation in + * core, which is faster than using a callback for comparisons + */ + private function restoreNewObjectsInDirtyCollection(array $newObjects): void + { + $loadedObjects = $this->unwrap()->toArray(); + $newObjectsByOid = array_combine(array_map('spl_object_id', $newObjects), $newObjects); + $loadedObjectsByOid = array_combine(array_map('spl_object_id', $loadedObjects), $loadedObjects); + $newObjectsThatWereNotLoaded = array_diff_key($newObjectsByOid, $loadedObjectsByOid); + + if ($newObjectsThatWereNotLoaded) { + // Reattach NEW objects added through add(), if any. + array_walk($newObjectsThatWereNotLoaded, [$this->unwrap(), 'add']); + + $this->isDirty = true; + } + } +} 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 @@ +uow = $em->getUnitOfWork(); + $this->conn = $em->getConnection(); + $this->platform = $this->conn->getDatabasePlatform(); + $this->quoteStrategy = $em->getConfiguration()->getQuoteStrategy(); + } + + /** + * Check if entity is in a valid state for operations. + */ + protected function isValidEntityState(object $entity): bool + { + $entityState = $this->uow->getEntityState($entity, UnitOfWork::STATE_NEW); + + if ($entityState === UnitOfWork::STATE_NEW) { + return false; + } + + // If Entity is scheduled for inclusion, it is not in this collection. + // We can assure that because it would have return true before on array check + return ! ($entityState === UnitOfWork::STATE_MANAGED && $this->uow->isScheduledForInsert($entity)); + } +} 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 @@ +getMapping($collection); + + if (! $mapping->isOwningSide()) { + return; // ignore inverse side + } + + assert($mapping->isManyToManyOwningSide()); + + $types = []; + $class = $this->em->getClassMetadata($mapping->sourceEntity); + + foreach ($mapping->joinTable->joinColumns as $joinColumn) { + $types[] = PersisterHelper::getTypeOfColumn($joinColumn->referencedColumnName, $class, $this->em); + } + + $this->conn->executeStatement($this->getDeleteSQL($collection), $this->getDeleteSQLParameters($collection), $types); + } + + public function update(PersistentCollection $collection): void + { + $mapping = $this->getMapping($collection); + + if (! $mapping->isOwningSide()) { + return; // ignore inverse side + } + + [$deleteSql, $deleteTypes] = $this->getDeleteRowSQL($collection); + [$insertSql, $insertTypes] = $this->getInsertRowSQL($collection); + + foreach ($collection->getDeleteDiff() as $element) { + $this->conn->executeStatement( + $deleteSql, + $this->getDeleteRowSQLParameters($collection, $element), + $deleteTypes, + ); + } + + foreach ($collection->getInsertDiff() as $element) { + $this->conn->executeStatement( + $insertSql, + $this->getInsertRowSQLParameters($collection, $element), + $insertTypes, + ); + } + } + + public function get(PersistentCollection $collection, mixed $index): object|null + { + $mapping = $this->getMapping($collection); + + if (! $mapping->isIndexed()) { + throw new BadMethodCallException('Selecting a collection by index is only supported on indexed collections.'); + } + + $persister = $this->uow->getEntityPersister($mapping->targetEntity); + $mappedKey = $mapping->isOwningSide() + ? $mapping->inversedBy + : $mapping->mappedBy; + + assert($mappedKey !== null); + + return $persister->load( + [$mappedKey => $collection->getOwner(), $mapping->indexBy() => $index], + null, + $mapping, + [], + LockMode::NONE, + 1, + ); + } + + public function count(PersistentCollection $collection): int + { + $conditions = []; + $params = []; + $types = []; + $mapping = $this->getMapping($collection); + $id = $this->uow->getEntityIdentifier($collection->getOwner()); + $sourceClass = $this->em->getClassMetadata($mapping->sourceEntity); + $association = $this->em->getMetadataFactory()->getOwningSide($mapping); + + $joinTableName = $this->quoteStrategy->getJoinTableName($association, $sourceClass, $this->platform); + $joinColumns = ! $mapping->isOwningSide() + ? $association->joinTable->inverseJoinColumns + : $association->joinTable->joinColumns; + + foreach ($joinColumns as $joinColumn) { + $columnName = $this->quoteStrategy->getJoinColumnName($joinColumn, $sourceClass, $this->platform); + $referencedName = $joinColumn->referencedColumnName; + $conditions[] = 't.' . $columnName . ' = ?'; + $params[] = $id[$sourceClass->getFieldForColumn($referencedName)]; + $types[] = PersisterHelper::getTypeOfColumn($referencedName, $sourceClass, $this->em); + } + + [$joinTargetEntitySQL, $filterSql] = $this->getFilterSql($mapping); + + if ($filterSql) { + $conditions[] = $filterSql; + } + + // If there is a provided criteria, make part of conditions + // @todo Fix this. Current SQL returns something like: + /*if ($criteria && ($expression = $criteria->getWhereExpression()) !== null) { + // A join is needed on the target entity + $targetTableName = $this->quoteStrategy->getTableName($targetClass, $this->platform); + $targetJoinSql = ' JOIN ' . $targetTableName . ' te' + . ' ON' . implode(' AND ', $this->getOnConditionSQL($association)); + + // And criteria conditions needs to be added + $persister = $this->uow->getEntityPersister($targetClass->name); + $visitor = new SqlExpressionVisitor($persister, $targetClass); + $conditions[] = $visitor->dispatch($expression); + + $joinTargetEntitySQL = $targetJoinSql . $joinTargetEntitySQL; + }*/ + + $sql = 'SELECT COUNT(*)' + . ' FROM ' . $joinTableName . ' t' + . $joinTargetEntitySQL + . ' WHERE ' . implode(' AND ', $conditions); + + return (int) $this->conn->fetchOne($sql, $params, $types); + } + + /** + * {@inheritDoc} + */ + public function slice(PersistentCollection $collection, int $offset, int|null $length = null): array + { + $mapping = $this->getMapping($collection); + $persister = $this->uow->getEntityPersister($mapping->targetEntity); + + return $persister->getManyToManyCollection($mapping, $collection->getOwner(), $offset, $length); + } + + public function containsKey(PersistentCollection $collection, mixed $key): bool + { + $mapping = $this->getMapping($collection); + + if (! $mapping->isIndexed()) { + throw new BadMethodCallException('Selecting a collection by index is only supported on indexed collections.'); + } + + [$quotedJoinTable, $whereClauses, $params, $types] = $this->getJoinTableRestrictionsWithKey( + $collection, + (string) $key, + true, + ); + + $sql = 'SELECT 1 FROM ' . $quotedJoinTable . ' WHERE ' . implode(' AND ', $whereClauses); + + return (bool) $this->conn->fetchOne($sql, $params, $types); + } + + public function contains(PersistentCollection $collection, object $element): bool + { + if (! $this->isValidEntityState($element)) { + return false; + } + + [$quotedJoinTable, $whereClauses, $params, $types] = $this->getJoinTableRestrictions( + $collection, + $element, + true, + ); + + $sql = 'SELECT 1 FROM ' . $quotedJoinTable . ' WHERE ' . implode(' AND ', $whereClauses); + + return (bool) $this->conn->fetchOne($sql, $params, $types); + } + + /** + * {@inheritDoc} + */ + public function loadCriteria(PersistentCollection $collection, Criteria $criteria): array + { + $mapping = $this->getMapping($collection); + $owner = $collection->getOwner(); + $ownerMetadata = $this->em->getClassMetadata($owner::class); + $id = $this->uow->getEntityIdentifier($owner); + $targetClass = $this->em->getClassMetadata($mapping->targetEntity); + $onConditions = $this->getOnConditionSQL($mapping); + $whereClauses = $params = []; + $paramTypes = []; + + if (! $mapping->isOwningSide()) { + assert($mapping instanceof InverseSideMapping); + $associationSourceClass = $targetClass; + $sourceRelationMode = 'relationToTargetKeyColumns'; + } else { + $associationSourceClass = $ownerMetadata; + $sourceRelationMode = 'relationToSourceKeyColumns'; + } + + $mapping = $this->em->getMetadataFactory()->getOwningSide($mapping); + + foreach ($mapping->$sourceRelationMode as $key => $value) { + $whereClauses[] = sprintf('t.%s = ?', $key); + $params[] = $ownerMetadata->containsForeignIdentifier + ? $id[$ownerMetadata->getFieldForColumn($value)] + : $id[$ownerMetadata->fieldNames[$value]]; + $paramTypes[] = PersisterHelper::getTypeOfColumn($value, $ownerMetadata, $this->em); + } + + $parameters = $this->expandCriteriaParameters($criteria); + + foreach ($parameters as $parameter) { + [$name, $value, $operator] = $parameter; + + $field = $this->quoteStrategy->getColumnName($name, $targetClass, $this->platform); + + if ($value === null && ($operator === Comparison::EQ || $operator === Comparison::NEQ)) { + $whereClauses[] = sprintf('te.%s %s NULL', $field, $operator === Comparison::EQ ? 'IS' : 'IS NOT'); + } else { + $whereClauses[] = sprintf('te.%s %s ?', $field, $operator); + $params[] = $value; + $paramTypes[] = PersisterHelper::getTypeOfField($name, $targetClass, $this->em)[0]; + } + } + + $tableName = $this->quoteStrategy->getTableName($targetClass, $this->platform); + $joinTable = $this->quoteStrategy->getJoinTableName($mapping, $associationSourceClass, $this->platform); + + $rsm = new Query\ResultSetMappingBuilder($this->em); + $rsm->addRootEntityFromClassMetadata($targetClass->name, 'te'); + + $sql = 'SELECT ' . $rsm->generateSelectClause() + . ' FROM ' . $tableName . ' te' + . ' JOIN ' . $joinTable . ' t ON' + . implode(' AND ', $onConditions) + . ' WHERE ' . implode(' AND ', $whereClauses); + + $sql .= $this->getOrderingSql($criteria, $targetClass); + + $sql .= $this->getLimitSql($criteria); + + $stmt = $this->conn->executeQuery($sql, $params, $paramTypes); + + return $this + ->em + ->newHydrator(Query::HYDRATE_OBJECT) + ->hydrateAll($stmt, $rsm); + } + + /** + * Generates the filter SQL for a given mapping. + * + * This method is not used for actually grabbing the related entities + * but when the extra-lazy collection methods are called on a filtered + * association. This is why besides the many to many table we also + * have to join in the actual entities table leading to additional + * JOIN. + * + * @param AssociationMapping $mapping Array containing mapping information. + * + * @return string[] ordered tuple: + * - JOIN condition to add to the SQL + * - WHERE condition to add to the SQL + * @psalm-return array{0: string, 1: string} + */ + public function getFilterSql(AssociationMapping $mapping): array + { + $targetClass = $this->em->getClassMetadata($mapping->targetEntity); + $rootClass = $this->em->getClassMetadata($targetClass->rootEntityName); + $filterSql = $this->generateFilterConditionSQL($rootClass, 'te'); + + if ($filterSql === '') { + return ['', '']; + } + + // A join is needed if there is filtering on the target entity + $tableName = $this->quoteStrategy->getTableName($rootClass, $this->platform); + $joinSql = ' JOIN ' . $tableName . ' te' + . ' ON' . implode(' AND ', $this->getOnConditionSQL($mapping)); + + return [$joinSql, $filterSql]; + } + + /** + * Generates the filter SQL for a given entity and table alias. + * + * @param ClassMetadata $targetEntity Metadata of the target entity. + * @param string $targetTableAlias The table alias of the joined/selected table. + * + * @return string The SQL query part to add to a query. + */ + protected function generateFilterConditionSQL(ClassMetadata $targetEntity, string $targetTableAlias): string + { + $filterClauses = []; + + foreach ($this->em->getFilters()->getEnabledFilters() as $filter) { + $filterExpr = $filter->addFilterConstraint($targetEntity, $targetTableAlias); + if ($filterExpr) { + $filterClauses[] = '(' . $filterExpr . ')'; + } + } + + return $filterClauses + ? '(' . implode(' AND ', $filterClauses) . ')' + : ''; + } + + /** + * Generate ON condition + * + * @return string[] + * @psalm-return list + */ + protected function getOnConditionSQL(AssociationMapping $mapping): array + { + $association = $this->em->getMetadataFactory()->getOwningSide($mapping); + $joinColumns = $mapping->isOwningSide() + ? $association->joinTable->inverseJoinColumns + : $association->joinTable->joinColumns; + + $conditions = []; + + $targetClass = $this->em->getClassMetadata($mapping->targetEntity); + foreach ($joinColumns as $joinColumn) { + $joinColumnName = $this->quoteStrategy->getJoinColumnName($joinColumn, $targetClass, $this->platform); + $refColumnName = $this->quoteStrategy->getReferencedJoinColumnName($joinColumn, $targetClass, $this->platform); + + $conditions[] = ' t.' . $joinColumnName . ' = te.' . $refColumnName; + } + + return $conditions; + } + + protected function getDeleteSQL(PersistentCollection $collection): string + { + $columns = []; + $mapping = $this->getMapping($collection); + assert($mapping->isManyToManyOwningSide()); + $class = $this->em->getClassMetadata($collection->getOwner()::class); + $joinTable = $this->quoteStrategy->getJoinTableName($mapping, $class, $this->platform); + + foreach ($mapping->joinTable->joinColumns as $joinColumn) { + $columns[] = $this->quoteStrategy->getJoinColumnName($joinColumn, $class, $this->platform); + } + + return 'DELETE FROM ' . $joinTable + . ' WHERE ' . implode(' = ? AND ', $columns) . ' = ?'; + } + + /** + * Internal note: Order of the parameters must be the same as the order of the columns in getDeleteSql. + * + * @return list + */ + protected function getDeleteSQLParameters(PersistentCollection $collection): array + { + $mapping = $this->getMapping($collection); + assert($mapping->isManyToManyOwningSide()); + $identifier = $this->uow->getEntityIdentifier($collection->getOwner()); + + // Optimization for single column identifier + if (count($mapping->relationToSourceKeyColumns) === 1) { + return [reset($identifier)]; + } + + // Composite identifier + $sourceClass = $this->em->getClassMetadata($mapping->sourceEntity); + $params = []; + + foreach ($mapping->relationToSourceKeyColumns as $columnName => $refColumnName) { + $params[] = isset($sourceClass->fieldNames[$refColumnName]) + ? $identifier[$sourceClass->fieldNames[$refColumnName]] + : $identifier[$sourceClass->getFieldForColumn($refColumnName)]; + } + + return $params; + } + + /** + * Gets the SQL statement used for deleting a row from the collection. + * + * @return string[]|string[][] ordered tuple containing the SQL to be executed and an array + * of types for bound parameters + * @psalm-return array{0: string, 1: list} + */ + protected function getDeleteRowSQL(PersistentCollection $collection): array + { + $mapping = $this->getMapping($collection); + assert($mapping->isManyToManyOwningSide()); + $class = $this->em->getClassMetadata($mapping->sourceEntity); + $targetClass = $this->em->getClassMetadata($mapping->targetEntity); + $columns = []; + $types = []; + + foreach ($mapping->joinTable->joinColumns as $joinColumn) { + $columns[] = $this->quoteStrategy->getJoinColumnName($joinColumn, $class, $this->platform); + $types[] = PersisterHelper::getTypeOfColumn($joinColumn->referencedColumnName, $class, $this->em); + } + + foreach ($mapping->joinTable->inverseJoinColumns as $joinColumn) { + $columns[] = $this->quoteStrategy->getJoinColumnName($joinColumn, $targetClass, $this->platform); + $types[] = PersisterHelper::getTypeOfColumn($joinColumn->referencedColumnName, $targetClass, $this->em); + } + + return [ + 'DELETE FROM ' . $this->quoteStrategy->getJoinTableName($mapping, $class, $this->platform) + . ' WHERE ' . implode(' = ? AND ', $columns) . ' = ?', + $types, + ]; + } + + /** + * Gets the SQL parameters for the corresponding SQL statement to delete the given + * element from the given collection. + * + * Internal note: Order of the parameters must be the same as the order of the columns in getDeleteRowSql. + * + * @return mixed[] + * @psalm-return list + */ + protected function getDeleteRowSQLParameters(PersistentCollection $collection, object $element): array + { + return $this->collectJoinTableColumnParameters($collection, $element); + } + + /** + * Gets the SQL statement used for inserting a row in the collection. + * + * @return string[]|string[][] ordered tuple containing the SQL to be executed and an array + * of types for bound parameters + * @psalm-return array{0: string, 1: list} + */ + protected function getInsertRowSQL(PersistentCollection $collection): array + { + $columns = []; + $types = []; + $mapping = $this->getMapping($collection); + assert($mapping->isManyToManyOwningSide()); + $class = $this->em->getClassMetadata($mapping->sourceEntity); + $targetClass = $this->em->getClassMetadata($mapping->targetEntity); + + foreach ($mapping->joinTable->joinColumns as $joinColumn) { + $columns[] = $this->quoteStrategy->getJoinColumnName($joinColumn, $targetClass, $this->platform); + $types[] = PersisterHelper::getTypeOfColumn($joinColumn->referencedColumnName, $class, $this->em); + } + + foreach ($mapping->joinTable->inverseJoinColumns as $joinColumn) { + $columns[] = $this->quoteStrategy->getJoinColumnName($joinColumn, $targetClass, $this->platform); + $types[] = PersisterHelper::getTypeOfColumn($joinColumn->referencedColumnName, $targetClass, $this->em); + } + + return [ + 'INSERT INTO ' . $this->quoteStrategy->getJoinTableName($mapping, $class, $this->platform) + . ' (' . implode(', ', $columns) . ')' + . ' VALUES' + . ' (' . implode(', ', array_fill(0, count($columns), '?')) . ')', + $types, + ]; + } + + /** + * Gets the SQL parameters for the corresponding SQL statement to insert the given + * element of the given collection into the database. + * + * Internal note: Order of the parameters must be the same as the order of the columns in getInsertRowSql. + * + * @return mixed[] + * @psalm-return list + */ + protected function getInsertRowSQLParameters(PersistentCollection $collection, object $element): array + { + return $this->collectJoinTableColumnParameters($collection, $element); + } + + /** + * Collects the parameters for inserting/deleting on the join table in the order + * of the join table columns as specified in ManyToManyMapping#joinTableColumns. + * + * @return mixed[] + * @psalm-return list + */ + private function collectJoinTableColumnParameters( + PersistentCollection $collection, + object $element, + ): array { + $params = []; + $mapping = $this->getMapping($collection); + assert($mapping->isManyToManyOwningSide()); + $isComposite = count($mapping->joinTableColumns) > 2; + + $identifier1 = $this->uow->getEntityIdentifier($collection->getOwner()); + $identifier2 = $this->uow->getEntityIdentifier($element); + + $class1 = $class2 = null; + if ($isComposite) { + $class1 = $this->em->getClassMetadata($collection->getOwner()::class); + $class2 = $collection->getTypeClass(); + } + + foreach ($mapping->joinTableColumns as $joinTableColumn) { + $isRelationToSource = isset($mapping->relationToSourceKeyColumns[$joinTableColumn]); + + if (! $isComposite) { + $params[] = $isRelationToSource ? array_pop($identifier1) : array_pop($identifier2); + + continue; + } + + if ($isRelationToSource) { + $params[] = $identifier1[$class1->getFieldForColumn($mapping->relationToSourceKeyColumns[$joinTableColumn])]; + + continue; + } + + $params[] = $identifier2[$class2->getFieldForColumn($mapping->relationToTargetKeyColumns[$joinTableColumn])]; + } + + return $params; + } + + /** + * @param bool $addFilters Whether the filter SQL should be included or not. + * + * @return mixed[] ordered vector: + * - quoted join table name + * - where clauses to be added for filtering + * - parameters to be bound for filtering + * - types of the parameters to be bound for filtering + * @psalm-return array{0: string, 1: list, 2: list, 3: list} + */ + private function getJoinTableRestrictionsWithKey( + PersistentCollection $collection, + string $key, + bool $addFilters, + ): array { + $filterMapping = $this->getMapping($collection); + $mapping = $filterMapping; + $indexBy = $mapping->indexBy(); + $id = $this->uow->getEntityIdentifier($collection->getOwner()); + $sourceClass = $this->em->getClassMetadata($mapping->sourceEntity); + $targetClass = $this->em->getClassMetadata($mapping->targetEntity); + + if (! $mapping->isOwningSide()) { + assert($mapping instanceof InverseSideMapping); + $associationSourceClass = $this->em->getClassMetadata($mapping->targetEntity); + $mapping = $associationSourceClass->associationMappings[$mapping->mappedBy]; + assert($mapping->isManyToManyOwningSide()); + $joinColumns = $mapping->joinTable->joinColumns; + $sourceRelationMode = 'relationToTargetKeyColumns'; + $targetRelationMode = 'relationToSourceKeyColumns'; + } else { + assert($mapping->isManyToManyOwningSide()); + $associationSourceClass = $this->em->getClassMetadata($mapping->sourceEntity); + $joinColumns = $mapping->joinTable->inverseJoinColumns; + $sourceRelationMode = 'relationToSourceKeyColumns'; + $targetRelationMode = 'relationToTargetKeyColumns'; + } + + $quotedJoinTable = $this->quoteStrategy->getJoinTableName($mapping, $associationSourceClass, $this->platform) . ' t'; + $whereClauses = []; + $params = []; + $types = []; + + $joinNeeded = ! in_array($indexBy, $targetClass->identifier, true); + + if ($joinNeeded) { // extra join needed if indexBy is not a @id + $joinConditions = []; + + foreach ($joinColumns as $joinTableColumn) { + $joinConditions[] = 't.' . $joinTableColumn->name . ' = tr.' . $joinTableColumn->referencedColumnName; + } + + $tableName = $this->quoteStrategy->getTableName($targetClass, $this->platform); + $quotedJoinTable .= ' JOIN ' . $tableName . ' tr ON ' . implode(' AND ', $joinConditions); + $columnName = $targetClass->getColumnName($indexBy); + + $whereClauses[] = 'tr.' . $columnName . ' = ?'; + $params[] = $key; + $types[] = PersisterHelper::getTypeOfColumn($columnName, $targetClass, $this->em); + } + + foreach ($mapping->joinTableColumns as $joinTableColumn) { + if (isset($mapping->{$sourceRelationMode}[$joinTableColumn])) { + $column = $mapping->{$sourceRelationMode}[$joinTableColumn]; + $whereClauses[] = 't.' . $joinTableColumn . ' = ?'; + $params[] = $sourceClass->containsForeignIdentifier + ? $id[$sourceClass->getFieldForColumn($column)] + : $id[$sourceClass->fieldNames[$column]]; + $types[] = PersisterHelper::getTypeOfColumn($column, $sourceClass, $this->em); + } elseif (! $joinNeeded) { + $column = $mapping->{$targetRelationMode}[$joinTableColumn]; + + $whereClauses[] = 't.' . $joinTableColumn . ' = ?'; + $params[] = $key; + $types[] = PersisterHelper::getTypeOfColumn($column, $targetClass, $this->em); + } + } + + if ($addFilters) { + [$joinTargetEntitySQL, $filterSql] = $this->getFilterSql($filterMapping); + + if ($filterSql) { + $quotedJoinTable .= ' ' . $joinTargetEntitySQL; + $whereClauses[] = $filterSql; + } + } + + return [$quotedJoinTable, $whereClauses, $params, $types]; + } + + /** + * @param bool $addFilters Whether the filter SQL should be included or not. + * + * @return mixed[] ordered vector: + * - quoted join table name + * - where clauses to be added for filtering + * - parameters to be bound for filtering + * - types of the parameters to be bound for filtering + * @psalm-return array{0: string, 1: list, 2: list, 3: list} + */ + private function getJoinTableRestrictions( + PersistentCollection $collection, + object $element, + bool $addFilters, + ): array { + $filterMapping = $this->getMapping($collection); + $mapping = $filterMapping; + + if (! $mapping->isOwningSide()) { + $sourceClass = $this->em->getClassMetadata($mapping->targetEntity); + $targetClass = $this->em->getClassMetadata($mapping->sourceEntity); + $sourceId = $this->uow->getEntityIdentifier($element); + $targetId = $this->uow->getEntityIdentifier($collection->getOwner()); + } else { + $sourceClass = $this->em->getClassMetadata($mapping->sourceEntity); + $targetClass = $this->em->getClassMetadata($mapping->targetEntity); + $sourceId = $this->uow->getEntityIdentifier($collection->getOwner()); + $targetId = $this->uow->getEntityIdentifier($element); + } + + $mapping = $this->em->getMetadataFactory()->getOwningSide($mapping); + + $quotedJoinTable = $this->quoteStrategy->getJoinTableName($mapping, $sourceClass, $this->platform); + $whereClauses = []; + $params = []; + $types = []; + + foreach ($mapping->joinTableColumns as $joinTableColumn) { + $whereClauses[] = ($addFilters ? 't.' : '') . $joinTableColumn . ' = ?'; + + if (isset($mapping->relationToTargetKeyColumns[$joinTableColumn])) { + $targetColumn = $mapping->relationToTargetKeyColumns[$joinTableColumn]; + $params[] = $targetId[$targetClass->getFieldForColumn($targetColumn)]; + $types[] = PersisterHelper::getTypeOfColumn($targetColumn, $targetClass, $this->em); + + continue; + } + + // relationToSourceKeyColumns + $targetColumn = $mapping->relationToSourceKeyColumns[$joinTableColumn]; + $params[] = $sourceId[$sourceClass->getFieldForColumn($targetColumn)]; + $types[] = PersisterHelper::getTypeOfColumn($targetColumn, $sourceClass, $this->em); + } + + if ($addFilters) { + $quotedJoinTable .= ' t'; + + [$joinTargetEntitySQL, $filterSql] = $this->getFilterSql($filterMapping); + + if ($filterSql) { + $quotedJoinTable .= ' ' . $joinTargetEntitySQL; + $whereClauses[] = $filterSql; + } + } + + return [$quotedJoinTable, $whereClauses, $params, $types]; + } + + /** + * Expands Criteria Parameters by walking the expressions and grabbing all + * parameters and types from it. + * + * @return mixed[][] + */ + private function expandCriteriaParameters(Criteria $criteria): array + { + $expression = $criteria->getWhereExpression(); + + if ($expression === null) { + return []; + } + + $valueVisitor = new SqlValueVisitor(); + + $valueVisitor->dispatch($expression); + + [, $types] = $valueVisitor->getParamsAndTypes(); + + return $types; + } + + private function getOrderingSql(Criteria $criteria, ClassMetadata $targetClass): string + { + $orderings = $criteria->orderings(); + if ($orderings) { + $orderBy = []; + foreach ($orderings as $name => $direction) { + $field = $this->quoteStrategy->getColumnName( + $name, + $targetClass, + $this->platform, + ); + $orderBy[] = $field . ' ' . $direction->value; + } + + return ' ORDER BY ' . implode(', ', $orderBy); + } + + return ''; + } + + /** @throws DBALException */ + private function getLimitSql(Criteria $criteria): string + { + $limit = $criteria->getMaxResults(); + $offset = $criteria->getFirstResult(); + + return $this->platform->modifyLimitQuery('', $limit, $offset ?? 0); + } + + private function getMapping(PersistentCollection $collection): AssociationMapping&ManyToManyAssociationMapping + { + $mapping = $collection->getMapping(); + + assert($mapping instanceof ManyToManyAssociationMapping); + + return $mapping; + } +} 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 @@ +getMapping($collection); + + if (! $mapping->orphanRemoval) { + // Handling non-orphan removal should never happen, as @OneToMany + // can only be inverse side. For owning side one to many, it is + // required to have a join table, which would classify as a ManyToManyPersister. + return; + } + + $targetClass = $this->em->getClassMetadata($mapping->targetEntity); + + $targetClass->isInheritanceTypeJoined() + ? $this->deleteJoinedEntityCollection($collection) + : $this->deleteEntityCollection($collection); + } + + public function update(PersistentCollection $collection): void + { + // This can never happen. One to many can only be inverse side. + // For owning side one to many, it is required to have a join table, + // then classifying it as a ManyToManyPersister. + return; + } + + public function get(PersistentCollection $collection, mixed $index): object|null + { + $mapping = $this->getMapping($collection); + + if (! $mapping->isIndexed()) { + throw new BadMethodCallException('Selecting a collection by index is only supported on indexed collections.'); + } + + $persister = $this->uow->getEntityPersister($mapping->targetEntity); + + return $persister->load( + [ + $mapping->mappedBy => $collection->getOwner(), + $mapping->indexBy() => $index, + ], + null, + $mapping, + [], + null, + 1, + ); + } + + public function count(PersistentCollection $collection): int + { + $mapping = $this->getMapping($collection); + $persister = $this->uow->getEntityPersister($mapping->targetEntity); + + // only works with single id identifier entities. Will throw an + // exception in Entity Persisters if that is not the case for the + // 'mappedBy' field. + $criteria = new Criteria(Criteria::expr()->eq($mapping->mappedBy, $collection->getOwner())); + + return $persister->count($criteria); + } + + /** + * {@inheritDoc} + */ + public function slice(PersistentCollection $collection, int $offset, int|null $length = null): array + { + $mapping = $this->getMapping($collection); + $persister = $this->uow->getEntityPersister($mapping->targetEntity); + + return $persister->getOneToManyCollection($mapping, $collection->getOwner(), $offset, $length); + } + + public function containsKey(PersistentCollection $collection, mixed $key): bool + { + $mapping = $this->getMapping($collection); + + if (! $mapping->isIndexed()) { + throw new BadMethodCallException('Selecting a collection by index is only supported on indexed collections.'); + } + + $persister = $this->uow->getEntityPersister($mapping->targetEntity); + + // only works with single id identifier entities. Will throw an + // exception in Entity Persisters if that is not the case for the + // 'mappedBy' field. + $criteria = new Criteria(); + + $criteria->andWhere(Criteria::expr()->eq($mapping->mappedBy, $collection->getOwner())); + $criteria->andWhere(Criteria::expr()->eq($mapping->indexBy(), $key)); + + return (bool) $persister->count($criteria); + } + + public function contains(PersistentCollection $collection, object $element): bool + { + if (! $this->isValidEntityState($element)) { + return false; + } + + $mapping = $this->getMapping($collection); + $persister = $this->uow->getEntityPersister($mapping->targetEntity); + + // only works with single id identifier entities. Will throw an + // exception in Entity Persisters if that is not the case for the + // 'mappedBy' field. + $criteria = new Criteria(Criteria::expr()->eq($mapping->mappedBy, $collection->getOwner())); + + return $persister->exists($element, $criteria); + } + + /** + * {@inheritDoc} + */ + public function loadCriteria(PersistentCollection $collection, Criteria $criteria): array + { + throw new BadMethodCallException('Filtering a collection by Criteria is not supported by this CollectionPersister.'); + } + + /** + * @throws DBALException + * @throws EntityNotFoundException + * @throws MappingException + */ + private function deleteEntityCollection(PersistentCollection $collection): int + { + $mapping = $this->getMapping($collection); + $identifier = $this->uow->getEntityIdentifier($collection->getOwner()); + $sourceClass = $this->em->getClassMetadata($mapping->sourceEntity); + $targetClass = $this->em->getClassMetadata($mapping->targetEntity); + $columns = []; + $parameters = []; + $types = []; + + foreach ($this->em->getMetadataFactory()->getOwningSide($mapping)->joinColumns as $joinColumn) { + $columns[] = $this->quoteStrategy->getJoinColumnName($joinColumn, $targetClass, $this->platform); + $parameters[] = $identifier[$sourceClass->getFieldForColumn($joinColumn->referencedColumnName)]; + $types[] = PersisterHelper::getTypeOfColumn($joinColumn->referencedColumnName, $sourceClass, $this->em); + } + + $statement = 'DELETE FROM ' . $this->quoteStrategy->getTableName($targetClass, $this->platform) + . ' WHERE ' . implode(' = ? AND ', $columns) . ' = ?'; + + if ($targetClass->isInheritanceTypeSingleTable()) { + $discriminatorColumn = $targetClass->getDiscriminatorColumn(); + $statement .= ' AND ' . $discriminatorColumn->name . ' = ?'; + $parameters[] = $targetClass->discriminatorValue; + $types[] = $discriminatorColumn->type; + } + + $numAffected = $this->conn->executeStatement($statement, $parameters, $types); + + assert(is_int($numAffected)); + + return $numAffected; + } + + /** + * Delete Class Table Inheritance entities. + * A temporary table is needed to keep IDs to be deleted in both parent and child class' tables. + * + * Thanks Steve Ebersole (Hibernate) for idea on how to tackle reliably this scenario, we owe him a beer! =) + * + * @throws DBALException + */ + private function deleteJoinedEntityCollection(PersistentCollection $collection): int + { + $mapping = $this->getMapping($collection); + $sourceClass = $this->em->getClassMetadata($mapping->sourceEntity); + $targetClass = $this->em->getClassMetadata($mapping->targetEntity); + $rootClass = $this->em->getClassMetadata($targetClass->rootEntityName); + + // 1) Build temporary table DDL + $tempTable = $this->platform->getTemporaryTableName($rootClass->getTemporaryIdTableName()); + $idColumnNames = $rootClass->getIdentifierColumnNames(); + $idColumnList = implode(', ', $idColumnNames); + $columnDefinitions = []; + + foreach ($idColumnNames as $idColumnName) { + $columnDefinitions[$idColumnName] = [ + 'name' => $idColumnName, + 'notnull' => true, + 'type' => Type::getType(PersisterHelper::getTypeOfColumn($idColumnName, $rootClass, $this->em)), + ]; + } + + $statement = $this->platform->getCreateTemporaryTableSnippetSQL() . ' ' . $tempTable + . ' (' . $this->platform->getColumnDeclarationListSQL($columnDefinitions) . ')'; + + $this->conn->executeStatement($statement); + + // 2) Build insert table records into temporary table + $query = $this->em->createQuery( + ' SELECT t0.' . implode(', t0.', $rootClass->getIdentifierFieldNames()) + . ' FROM ' . $targetClass->name . ' t0 WHERE t0.' . $mapping->mappedBy . ' = :owner', + )->setParameter('owner', $collection->getOwner()); + + $sql = $query->getSQL(); + assert(is_string($sql)); + $statement = 'INSERT INTO ' . $tempTable . ' (' . $idColumnList . ') ' . $sql; + $parameters = array_values($sourceClass->getIdentifierValues($collection->getOwner())); + $numDeleted = $this->conn->executeStatement($statement, $parameters); + + // 3) Delete records on each table in the hierarchy + $classNames = [...$targetClass->parentClasses, ...[$targetClass->name], ...$targetClass->subClasses]; + + foreach (array_reverse($classNames) as $className) { + $tableName = $this->quoteStrategy->getTableName($this->em->getClassMetadata($className), $this->platform); + $statement = 'DELETE FROM ' . $tableName . ' WHERE (' . $idColumnList . ')' + . ' IN (SELECT ' . $idColumnList . ' FROM ' . $tempTable . ')'; + + $this->conn->executeStatement($statement); + } + + // 4) Drop temporary table + $statement = $this->platform->getDropTemporaryTableSQL($tempTable); + + $this->conn->executeStatement($statement); + + assert(is_int($numDeleted)); + + return $numDeleted; + } + + private function getMapping(PersistentCollection $collection): OneToManyAssociationMapping + { + $mapping = $collection->getMapping(); + + assert($mapping->isOneToMany()); + + return $mapping; + } +} 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 @@ +class->getDiscriminatorColumn(); + $this->columnTypes[$discColumn->name] = $discColumn->type; + $data[$this->getDiscriminatorColumnTableName()][$discColumn->name] = $this->class->discriminatorValue; + + return $data; + } + + /** + * Gets the name of the table that contains the discriminator column. + */ + abstract protected function getDiscriminatorColumnTableName(): string; + + protected function getSelectColumnSQL(string $field, ClassMetadata $class, string $alias = 'r'): string + { + $tableAlias = $alias === 'r' ? '' : $alias; + $fieldMapping = $class->fieldMappings[$field]; + $columnAlias = $this->getSQLColumnAlias($fieldMapping->columnName); + $sql = sprintf( + '%s.%s', + $this->getSQLTableAlias($class->name, $tableAlias), + $this->quoteStrategy->getColumnName($field, $class, $this->platform), + ); + + $this->currentPersisterContext->rsm->addFieldResult($alias, $columnAlias, $field, $class->name); + + $type = Type::getType($fieldMapping->type); + $sql = $type->convertToPHPValueSQL($sql, $this->platform); + + return $sql . ' AS ' . $columnAlias; + } + + protected function getSelectJoinColumnSQL(string $tableAlias, string $joinColumnName, string $quotedColumnName, string $type): string + { + $columnAlias = $this->getSQLColumnAlias($joinColumnName); + + $this->currentPersisterContext->rsm->addMetaResult('r', $columnAlias, $joinColumnName, false, $type); + + return $tableAlias . '.' . $quotedColumnName . ' AS ' . $columnAlias; + } +} 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 @@ + */ + private static array $comparisonMap = [ + Comparison::EQ => '= %s', + Comparison::NEQ => '!= %s', + Comparison::GT => '> %s', + Comparison::GTE => '>= %s', + Comparison::LT => '< %s', + Comparison::LTE => '<= %s', + Comparison::IN => 'IN (%s)', + Comparison::NIN => 'NOT IN (%s)', + Comparison::CONTAINS => 'LIKE %s', + Comparison::STARTS_WITH => 'LIKE %s', + Comparison::ENDS_WITH => 'LIKE %s', + ]; + + /** + * The underlying DBAL Connection of the used EntityManager. + */ + protected Connection $conn; + + /** + * The database platform. + */ + protected AbstractPlatform $platform; + + /** + * Queued inserts. + * + * @psalm-var array + */ + protected array $queuedInserts = []; + + /** + * The map of column names to DBAL mapping types of all prepared columns used + * when INSERTing or UPDATEing an entity. + * + * @see prepareInsertData($entity) + * @see prepareUpdateData($entity) + * + * @var mixed[] + */ + protected array $columnTypes = []; + + /** + * The map of quoted column names. + * + * @see prepareInsertData($entity) + * @see prepareUpdateData($entity) + * + * @var mixed[] + */ + protected array $quotedColumns = []; + + /** + * The INSERT SQL statement used for entities handled by this persister. + * This SQL is only generated once per request, if at all. + */ + private string|null $insertSql = null; + + /** + * The quote strategy. + */ + protected QuoteStrategy $quoteStrategy; + + /** + * The IdentifierFlattener used for manipulating identifiers + */ + protected readonly IdentifierFlattener $identifierFlattener; + + protected CachedPersisterContext $currentPersisterContext; + private readonly CachedPersisterContext $limitsHandlingContext; + private readonly CachedPersisterContext $noLimitsContext; + + /** + * Initializes a new BasicEntityPersister that uses the given EntityManager + * and persists instances of the class described by the given ClassMetadata descriptor. + * + * @param ClassMetadata $class Metadata object that describes the mapping of the mapped entity class. + */ + public function __construct( + protected EntityManagerInterface $em, + protected ClassMetadata $class, + ) { + $this->conn = $em->getConnection(); + $this->platform = $this->conn->getDatabasePlatform(); + $this->quoteStrategy = $em->getConfiguration()->getQuoteStrategy(); + $this->identifierFlattener = new IdentifierFlattener($em->getUnitOfWork(), $em->getMetadataFactory()); + $this->noLimitsContext = $this->currentPersisterContext = new CachedPersisterContext( + $class, + new Query\ResultSetMapping(), + false, + ); + $this->limitsHandlingContext = new CachedPersisterContext( + $class, + new Query\ResultSetMapping(), + true, + ); + } + + public function getClassMetadata(): ClassMetadata + { + return $this->class; + } + + public function getResultSetMapping(): ResultSetMapping + { + return $this->currentPersisterContext->rsm; + } + + public function addInsert(object $entity): void + { + $this->queuedInserts[spl_object_id($entity)] = $entity; + } + + /** + * {@inheritDoc} + */ + public function getInserts(): array + { + return $this->queuedInserts; + } + + public function executeInserts(): void + { + if (! $this->queuedInserts) { + return; + } + + $uow = $this->em->getUnitOfWork(); + $idGenerator = $this->class->idGenerator; + $isPostInsertId = $idGenerator->isPostInsertGenerator(); + + $stmt = $this->conn->prepare($this->getInsertSQL()); + $tableName = $this->class->getTableName(); + + foreach ($this->queuedInserts as $key => $entity) { + $insertData = $this->prepareInsertData($entity); + + if (isset($insertData[$tableName])) { + $paramIndex = 1; + + foreach ($insertData[$tableName] as $column => $value) { + $stmt->bindValue($paramIndex++, $value, $this->columnTypes[$column]); + } + } + + $stmt->executeStatement(); + + if ($isPostInsertId) { + $generatedId = $idGenerator->generateId($this->em, $entity); + $id = [$this->class->identifier[0] => $generatedId]; + + $uow->assignPostInsertId($entity, $generatedId); + } else { + $id = $this->class->getIdentifierValues($entity); + } + + if ($this->class->requiresFetchAfterChange) { + $this->assignDefaultVersionAndUpsertableValues($entity, $id); + } + + // Unset this queued insert, so that the prepareUpdateData() method knows right away + // (for the next entity already) that the current entity has been written to the database + // and no extra updates need to be scheduled to refer to it. + // + // In \Doctrine\ORM\UnitOfWork::executeInserts(), the UoW already removed entities + // from its own list (\Doctrine\ORM\UnitOfWork::$entityInsertions) right after they + // were given to our addInsert() method. + unset($this->queuedInserts[$key]); + } + } + + /** + * Retrieves the default version value which was created + * by the preceding INSERT statement and assigns it back in to the + * entities version field if the given entity is versioned. + * Also retrieves values of columns marked as 'non insertable' and / or + * 'not updatable' and assigns them back to the entities corresponding fields. + * + * @param mixed[] $id + */ + protected function assignDefaultVersionAndUpsertableValues(object $entity, array $id): void + { + $values = $this->fetchVersionAndNotUpsertableValues($this->class, $id); + + foreach ($values as $field => $value) { + $value = Type::getType($this->class->fieldMappings[$field]->type)->convertToPHPValue($value, $this->platform); + + $this->class->setFieldValue($entity, $field, $value); + } + } + + /** + * Fetches the current version value of a versioned entity and / or the values of fields + * marked as 'not insertable' and / or 'not updatable'. + * + * @param mixed[] $id + */ + protected function fetchVersionAndNotUpsertableValues(ClassMetadata $versionedClass, array $id): mixed + { + $columnNames = []; + foreach ($this->class->fieldMappings as $key => $column) { + if (isset($column->generated) || ($this->class->isVersioned && $key === $versionedClass->versionField)) { + $columnNames[$key] = $this->quoteStrategy->getColumnName($key, $versionedClass, $this->platform); + } + } + + $tableName = $this->quoteStrategy->getTableName($versionedClass, $this->platform); + $identifier = $this->quoteStrategy->getIdentifierColumnNames($versionedClass, $this->platform); + + // FIXME: Order with composite keys might not be correct + $sql = 'SELECT ' . implode(', ', $columnNames) + . ' FROM ' . $tableName + . ' WHERE ' . implode(' = ? AND ', $identifier) . ' = ?'; + + $flatId = $this->identifierFlattener->flattenIdentifier($versionedClass, $id); + + $values = $this->conn->fetchNumeric( + $sql, + array_values($flatId), + $this->extractIdentifierTypes($id, $versionedClass), + ); + + if ($values === false) { + throw new LengthException('Unexpected empty result for database query.'); + } + + $values = array_combine(array_keys($columnNames), $values); + + if (! $values) { + throw new LengthException('Unexpected number of database columns.'); + } + + return $values; + } + + /** + * @param mixed[] $id + * + * @return list + * @psalm-return list + */ + final protected function extractIdentifierTypes(array $id, ClassMetadata $versionedClass): array + { + $types = []; + + foreach ($id as $field => $value) { + $types = [...$types, ...$this->getTypes($field, $value, $versionedClass)]; + } + + return $types; + } + + public function update(object $entity): void + { + $tableName = $this->class->getTableName(); + $updateData = $this->prepareUpdateData($entity); + + if (! isset($updateData[$tableName])) { + return; + } + + $data = $updateData[$tableName]; + + if (! $data) { + return; + } + + $isVersioned = $this->class->isVersioned; + $quotedTableName = $this->quoteStrategy->getTableName($this->class, $this->platform); + + $this->updateTable($entity, $quotedTableName, $data, $isVersioned); + + if ($this->class->requiresFetchAfterChange) { + $id = $this->class->getIdentifierValues($entity); + + $this->assignDefaultVersionAndUpsertableValues($entity, $id); + } + } + + /** + * Performs an UPDATE statement for an entity on a specific table. + * The UPDATE can optionally be versioned, which requires the entity to have a version field. + * + * @param object $entity The entity object being updated. + * @param string $quotedTableName The quoted name of the table to apply the UPDATE on. + * @param mixed[] $updateData The map of columns to update (column => value). + * @param bool $versioned Whether the UPDATE should be versioned. + * + * @throws UnrecognizedField + * @throws OptimisticLockException + */ + final protected function updateTable( + object $entity, + string $quotedTableName, + array $updateData, + bool $versioned = false, + ): void { + $set = []; + $types = []; + $params = []; + + foreach ($updateData as $columnName => $value) { + $placeholder = '?'; + $column = $columnName; + + switch (true) { + case isset($this->class->fieldNames[$columnName]): + $fieldName = $this->class->fieldNames[$columnName]; + $column = $this->quoteStrategy->getColumnName($fieldName, $this->class, $this->platform); + + if (isset($this->class->fieldMappings[$fieldName])) { + $type = Type::getType($this->columnTypes[$columnName]); + $placeholder = $type->convertToDatabaseValueSQL('?', $this->platform); + } + + break; + + case isset($this->quotedColumns[$columnName]): + $column = $this->quotedColumns[$columnName]; + + break; + } + + $params[] = $value; + $set[] = $column . ' = ' . $placeholder; + $types[] = $this->columnTypes[$columnName]; + } + + $where = []; + $identifier = $this->em->getUnitOfWork()->getEntityIdentifier($entity); + + foreach ($this->class->identifier as $idField) { + if (! isset($this->class->associationMappings[$idField])) { + $params[] = $identifier[$idField]; + $types[] = $this->class->fieldMappings[$idField]->type; + $where[] = $this->quoteStrategy->getColumnName($idField, $this->class, $this->platform); + + continue; + } + + assert($this->class->associationMappings[$idField]->isToOneOwningSide()); + + $params[] = $identifier[$idField]; + $where[] = $this->quoteStrategy->getJoinColumnName( + $this->class->associationMappings[$idField]->joinColumns[0], + $this->class, + $this->platform, + ); + + $targetMapping = $this->em->getClassMetadata($this->class->associationMappings[$idField]->targetEntity); + $targetType = PersisterHelper::getTypeOfField($targetMapping->identifier[0], $targetMapping, $this->em); + + if ($targetType === []) { + throw UnrecognizedField::byFullyQualifiedName($this->class->name, $targetMapping->identifier[0]); + } + + $types[] = reset($targetType); + } + + if ($versioned) { + $versionField = $this->class->versionField; + assert($versionField !== null); + $versionFieldType = $this->class->fieldMappings[$versionField]->type; + $versionColumn = $this->quoteStrategy->getColumnName($versionField, $this->class, $this->platform); + + $where[] = $versionColumn; + $types[] = $this->class->fieldMappings[$versionField]->type; + $params[] = $this->class->reflFields[$versionField]->getValue($entity); + + switch ($versionFieldType) { + case Types::SMALLINT: + case Types::INTEGER: + case Types::BIGINT: + $set[] = $versionColumn . ' = ' . $versionColumn . ' + 1'; + break; + + case Types::DATETIME_MUTABLE: + $set[] = $versionColumn . ' = CURRENT_TIMESTAMP'; + break; + } + } + + $sql = 'UPDATE ' . $quotedTableName + . ' SET ' . implode(', ', $set) + . ' WHERE ' . implode(' = ? AND ', $where) . ' = ?'; + + $result = $this->conn->executeStatement($sql, $params, $types); + + if ($versioned && ! $result) { + throw OptimisticLockException::lockFailed($entity); + } + } + + /** + * @param array $identifier + * @param string[] $types + * + * @todo Add check for platform if it supports foreign keys/cascading. + */ + protected function deleteJoinTableRecords(array $identifier, array $types): void + { + foreach ($this->class->associationMappings as $mapping) { + if (! $mapping->isManyToMany() || $mapping->isOnDeleteCascade) { + continue; + } + + // @Todo this only covers scenarios with no inheritance or of the same level. Is there something + // like self-referential relationship between different levels of an inheritance hierarchy? I hope not! + $selfReferential = ($mapping->targetEntity === $mapping->sourceEntity); + $class = $this->class; + $association = $mapping; + $otherColumns = []; + $otherKeys = []; + $keys = []; + + if (! $mapping->isOwningSide()) { + $class = $this->em->getClassMetadata($mapping->targetEntity); + } + + $association = $this->em->getMetadataFactory()->getOwningSide($association); + $joinColumns = $mapping->isOwningSide() + ? $association->joinTable->joinColumns + : $association->joinTable->inverseJoinColumns; + + if ($selfReferential) { + $otherColumns = ! $mapping->isOwningSide() + ? $association->joinTable->joinColumns + : $association->joinTable->inverseJoinColumns; + } + + foreach ($joinColumns as $joinColumn) { + $keys[] = $this->quoteStrategy->getJoinColumnName($joinColumn, $class, $this->platform); + } + + foreach ($otherColumns as $joinColumn) { + $otherKeys[] = $this->quoteStrategy->getJoinColumnName($joinColumn, $class, $this->platform); + } + + $joinTableName = $this->quoteStrategy->getJoinTableName($association, $this->class, $this->platform); + + $this->conn->delete($joinTableName, array_combine($keys, $identifier), $types); + + if ($selfReferential) { + $this->conn->delete($joinTableName, array_combine($otherKeys, $identifier), $types); + } + } + } + + public function delete(object $entity): bool + { + $class = $this->class; + $identifier = $this->em->getUnitOfWork()->getEntityIdentifier($entity); + $tableName = $this->quoteStrategy->getTableName($class, $this->platform); + $idColumns = $this->quoteStrategy->getIdentifierColumnNames($class, $this->platform); + $id = array_combine($idColumns, $identifier); + $types = $this->getClassIdentifiersTypes($class); + + $this->deleteJoinTableRecords($identifier, $types); + + return (bool) $this->conn->delete($tableName, $id, $types); + } + + /** + * Prepares the changeset of an entity for database insertion (UPDATE). + * + * The changeset is obtained from the currently running UnitOfWork. + * + * During this preparation the array that is passed as the second parameter is filled with + * => pairs, grouped by table name. + * + * Example: + * + * array( + * 'foo_table' => array('column1' => 'value1', 'column2' => 'value2', ...), + * 'bar_table' => array('columnX' => 'valueX', 'columnY' => 'valueY', ...), + * ... + * ) + * + * + * @param object $entity The entity for which to prepare the data. + * @param bool $isInsert Whether the data to be prepared refers to an insert statement. + * + * @return mixed[][] The prepared data. + * @psalm-return array> + */ + protected function prepareUpdateData(object $entity, bool $isInsert = false): array + { + $versionField = null; + $result = []; + $uow = $this->em->getUnitOfWork(); + + $versioned = $this->class->isVersioned; + if ($versioned !== false) { + $versionField = $this->class->versionField; + } + + foreach ($uow->getEntityChangeSet($entity) as $field => $change) { + if (isset($versionField) && $versionField === $field) { + continue; + } + + if (isset($this->class->embeddedClasses[$field])) { + continue; + } + + $newVal = $change[1]; + + if (! isset($this->class->associationMappings[$field])) { + $fieldMapping = $this->class->fieldMappings[$field]; + $columnName = $fieldMapping->columnName; + + if (! $isInsert && isset($fieldMapping->notUpdatable)) { + continue; + } + + if ($isInsert && isset($fieldMapping->notInsertable)) { + continue; + } + + $this->columnTypes[$columnName] = $fieldMapping->type; + + $result[$this->getOwningTable($field)][$columnName] = $newVal; + + continue; + } + + $assoc = $this->class->associationMappings[$field]; + + // Only owning side of x-1 associations can have a FK column. + if (! $assoc->isToOneOwningSide()) { + continue; + } + + if ($newVal !== null) { + $oid = spl_object_id($newVal); + + // If the associated entity $newVal is not yet persisted and/or does not yet have + // an ID assigned, we must set $newVal = null. This will insert a null value and + // schedule an extra update on the UnitOfWork. + // + // This gives us extra time to a) possibly obtain a database-generated identifier + // value for $newVal, and b) insert $newVal into the database before the foreign + // key reference is being made. + // + // When looking at $this->queuedInserts and $uow->isScheduledForInsert, be aware + // of the implementation details that our own executeInserts() method will remove + // entities from the former as soon as the insert statement has been executed and + // a post-insert ID has been assigned (if necessary), and that the UnitOfWork has + // already removed entities from its own list at the time they were passed to our + // addInsert() method. + // + // Then, there is one extra exception we can make: An entity that references back to itself + // _and_ uses an application-provided ID (the "NONE" generator strategy) also does not + // need the extra update, although it is still in the list of insertions itself. + // This looks like a minor optimization at first, but is the capstone for being able to + // use non-NULLable, self-referencing associations in applications that provide IDs (like UUIDs). + if ( + (isset($this->queuedInserts[$oid]) || $uow->isScheduledForInsert($newVal)) + && ! ($newVal === $entity && $this->class->isIdentifierNatural()) + ) { + $uow->scheduleExtraUpdate($entity, [$field => [null, $newVal]]); + + $newVal = null; + } + } + + $newValId = null; + + if ($newVal !== null) { + $newValId = $uow->getEntityIdentifier($newVal); + } + + $targetClass = $this->em->getClassMetadata($assoc->targetEntity); + $owningTable = $this->getOwningTable($field); + + foreach ($assoc->joinColumns as $joinColumn) { + $sourceColumn = $joinColumn->name; + $targetColumn = $joinColumn->referencedColumnName; + $quotedColumn = $this->quoteStrategy->getJoinColumnName($joinColumn, $this->class, $this->platform); + + $this->quotedColumns[$sourceColumn] = $quotedColumn; + $this->columnTypes[$sourceColumn] = PersisterHelper::getTypeOfColumn($targetColumn, $targetClass, $this->em); + $result[$owningTable][$sourceColumn] = $newValId + ? $newValId[$targetClass->getFieldForColumn($targetColumn)] + : null; + } + } + + return $result; + } + + /** + * Prepares the data changeset of a managed entity for database insertion (initial INSERT). + * The changeset of the entity is obtained from the currently running UnitOfWork. + * + * The default insert data preparation is the same as for updates. + * + * @see prepareUpdateData + * + * @param object $entity The entity for which to prepare the data. + * + * @return mixed[][] The prepared data for the tables to update. + * @psalm-return array + */ + protected function prepareInsertData(object $entity): array + { + return $this->prepareUpdateData($entity, true); + } + + public function getOwningTable(string $fieldName): string + { + return $this->class->getTableName(); + } + + /** + * {@inheritDoc} + */ + public function load( + array $criteria, + object|null $entity = null, + AssociationMapping|null $assoc = null, + array $hints = [], + LockMode|int|null $lockMode = null, + int|null $limit = null, + array|null $orderBy = null, + ): object|null { + $this->switchPersisterContext(null, $limit); + + $sql = $this->getSelectSQL($criteria, $assoc, $lockMode, $limit, null, $orderBy); + [$params, $types] = $this->expandParameters($criteria); + $stmt = $this->conn->executeQuery($sql, $params, $types); + + if ($entity !== null) { + $hints[Query::HINT_REFRESH] = true; + $hints[Query::HINT_REFRESH_ENTITY] = $entity; + } + + $hydrator = $this->em->newHydrator($this->currentPersisterContext->selectJoinSql ? Query::HYDRATE_OBJECT : Query::HYDRATE_SIMPLEOBJECT); + $entities = $hydrator->hydrateAll($stmt, $this->currentPersisterContext->rsm, $hints); + + return $entities ? $entities[0] : null; + } + + /** + * {@inheritDoc} + */ + public function loadById(array $identifier, object|null $entity = null): object|null + { + return $this->load($identifier, $entity); + } + + /** + * {@inheritDoc} + */ + public function loadOneToOneEntity(AssociationMapping $assoc, object $sourceEntity, array $identifier = []): object|null + { + $foundEntity = $this->em->getUnitOfWork()->tryGetById($identifier, $assoc->targetEntity); + if ($foundEntity !== false) { + return $foundEntity; + } + + $targetClass = $this->em->getClassMetadata($assoc->targetEntity); + + if ($assoc->isOwningSide()) { + $isInverseSingleValued = $assoc->inversedBy !== null && ! $targetClass->isCollectionValuedAssociation($assoc->inversedBy); + + // Mark inverse side as fetched in the hints, otherwise the UoW would + // try to load it in a separate query (remember: to-one inverse sides can not be lazy). + $hints = []; + + if ($isInverseSingleValued) { + $hints['fetched']['r'][$assoc->inversedBy] = true; + } + + $targetEntity = $this->load($identifier, null, $assoc, $hints); + + // Complete bidirectional association, if necessary + if ($targetEntity !== null && $isInverseSingleValued) { + $targetClass->reflFields[$assoc->inversedBy]->setValue($targetEntity, $sourceEntity); + } + + return $targetEntity; + } + + assert(isset($assoc->mappedBy)); + $sourceClass = $this->em->getClassMetadata($assoc->sourceEntity); + $owningAssoc = $targetClass->getAssociationMapping($assoc->mappedBy); + assert($owningAssoc->isOneToOneOwningSide()); + + $computedIdentifier = []; + + // TRICKY: since the association is specular source and target are flipped + foreach ($owningAssoc->targetToSourceKeyColumns as $sourceKeyColumn => $targetKeyColumn) { + if (! isset($sourceClass->fieldNames[$sourceKeyColumn])) { + throw MappingException::joinColumnMustPointToMappedField( + $sourceClass->name, + $sourceKeyColumn, + ); + } + + $computedIdentifier[$targetClass->getFieldForColumn($targetKeyColumn)] = + $sourceClass->reflFields[$sourceClass->fieldNames[$sourceKeyColumn]]->getValue($sourceEntity); + } + + $targetEntity = $this->load($computedIdentifier, null, $assoc); + + if ($targetEntity !== null) { + $targetClass->setFieldValue($targetEntity, $assoc->mappedBy, $sourceEntity); + } + + return $targetEntity; + } + + /** + * {@inheritDoc} + */ + public function refresh(array $id, object $entity, LockMode|int|null $lockMode = null): void + { + $sql = $this->getSelectSQL($id, null, $lockMode); + [$params, $types] = $this->expandParameters($id); + $stmt = $this->conn->executeQuery($sql, $params, $types); + + $hydrator = $this->em->newHydrator(Query::HYDRATE_OBJECT); + $hydrator->hydrateAll($stmt, $this->currentPersisterContext->rsm, [Query::HINT_REFRESH => true]); + } + + public function count(array|Criteria $criteria = []): int + { + $sql = $this->getCountSQL($criteria); + + [$params, $types] = $criteria instanceof Criteria + ? $this->expandCriteriaParameters($criteria) + : $this->expandParameters($criteria); + + return (int) $this->conn->executeQuery($sql, $params, $types)->fetchOne(); + } + + /** + * {@inheritDoc} + */ + public function loadCriteria(Criteria $criteria): array + { + $orderBy = array_map( + static fn (Order $order): string => $order->value, + $criteria->orderings(), + ); + $limit = $criteria->getMaxResults(); + $offset = $criteria->getFirstResult(); + $query = $this->getSelectSQL($criteria, null, null, $limit, $offset, $orderBy); + + [$params, $types] = $this->expandCriteriaParameters($criteria); + + $stmt = $this->conn->executeQuery($query, $params, $types); + $hydrator = $this->em->newHydrator($this->currentPersisterContext->selectJoinSql ? Query::HYDRATE_OBJECT : Query::HYDRATE_SIMPLEOBJECT); + + return $hydrator->hydrateAll($stmt, $this->currentPersisterContext->rsm, [UnitOfWork::HINT_DEFEREAGERLOAD => true]); + } + + /** + * {@inheritDoc} + */ + public function expandCriteriaParameters(Criteria $criteria): array + { + $expression = $criteria->getWhereExpression(); + $sqlParams = []; + $sqlTypes = []; + + if ($expression === null) { + return [$sqlParams, $sqlTypes]; + } + + $valueVisitor = new SqlValueVisitor(); + + $valueVisitor->dispatch($expression); + + [, $types] = $valueVisitor->getParamsAndTypes(); + + foreach ($types as $type) { + [$field, $value, $operator] = $type; + + if ($value === null && ($operator === Comparison::EQ || $operator === Comparison::NEQ)) { + continue; + } + + $sqlParams = [...$sqlParams, ...$this->getValues($value)]; + $sqlTypes = [...$sqlTypes, ...$this->getTypes($field, $value, $this->class)]; + } + + return [$sqlParams, $sqlTypes]; + } + + /** + * {@inheritDoc} + */ + public function loadAll( + array $criteria = [], + array|null $orderBy = null, + int|null $limit = null, + int|null $offset = null, + ): array { + $this->switchPersisterContext($offset, $limit); + + $sql = $this->getSelectSQL($criteria, null, null, $limit, $offset, $orderBy); + [$params, $types] = $this->expandParameters($criteria); + $stmt = $this->conn->executeQuery($sql, $params, $types); + + $hydrator = $this->em->newHydrator($this->currentPersisterContext->selectJoinSql ? Query::HYDRATE_OBJECT : Query::HYDRATE_SIMPLEOBJECT); + + return $hydrator->hydrateAll($stmt, $this->currentPersisterContext->rsm, [UnitOfWork::HINT_DEFEREAGERLOAD => true]); + } + + /** + * {@inheritDoc} + */ + public function getManyToManyCollection( + AssociationMapping $assoc, + object $sourceEntity, + int|null $offset = null, + int|null $limit = null, + ): array { + assert($assoc->isManyToMany()); + $this->switchPersisterContext($offset, $limit); + + $stmt = $this->getManyToManyStatement($assoc, $sourceEntity, $offset, $limit); + + return $this->loadArrayFromResult($assoc, $stmt); + } + + /** + * Loads an array of entities from a given DBAL statement. + * + * @return mixed[] + */ + private function loadArrayFromResult(AssociationMapping $assoc, Result $stmt): array + { + $rsm = $this->currentPersisterContext->rsm; + $hints = [UnitOfWork::HINT_DEFEREAGERLOAD => true]; + + if ($assoc->isIndexed()) { + $rsm = clone $this->currentPersisterContext->rsm; // this is necessary because the "default rsm" should be changed. + $rsm->addIndexBy('r', $assoc->indexBy()); + } + + return $this->em->newHydrator(Query::HYDRATE_OBJECT)->hydrateAll($stmt, $rsm, $hints); + } + + /** + * Hydrates a collection from a given DBAL statement. + * + * @return mixed[] + */ + private function loadCollectionFromStatement( + AssociationMapping $assoc, + Result $stmt, + PersistentCollection $coll, + ): array { + $rsm = $this->currentPersisterContext->rsm; + $hints = [ + UnitOfWork::HINT_DEFEREAGERLOAD => true, + 'collection' => $coll, + ]; + + if ($assoc->isIndexed()) { + $rsm = clone $this->currentPersisterContext->rsm; // this is necessary because the "default rsm" should be changed. + $rsm->addIndexBy('r', $assoc->indexBy()); + } + + return $this->em->newHydrator(Query::HYDRATE_OBJECT)->hydrateAll($stmt, $rsm, $hints); + } + + /** + * {@inheritDoc} + */ + public function loadManyToManyCollection(AssociationMapping $assoc, object $sourceEntity, PersistentCollection $collection): array + { + assert($assoc->isManyToMany()); + $stmt = $this->getManyToManyStatement($assoc, $sourceEntity); + + return $this->loadCollectionFromStatement($assoc, $stmt, $collection); + } + + /** @throws MappingException */ + private function getManyToManyStatement( + AssociationMapping&ManyToManyAssociationMapping $assoc, + object $sourceEntity, + int|null $offset = null, + int|null $limit = null, + ): Result { + $this->switchPersisterContext($offset, $limit); + + $sourceClass = $this->em->getClassMetadata($assoc->sourceEntity); + $class = $sourceClass; + $association = $assoc; + $criteria = []; + $parameters = []; + + if (! $assoc->isOwningSide()) { + $class = $this->em->getClassMetadata($assoc->targetEntity); + } + + $association = $this->em->getMetadataFactory()->getOwningSide($assoc); + $joinColumns = $assoc->isOwningSide() + ? $association->joinTable->joinColumns + : $association->joinTable->inverseJoinColumns; + + $quotedJoinTable = $this->quoteStrategy->getJoinTableName($association, $class, $this->platform); + + foreach ($joinColumns as $joinColumn) { + $sourceKeyColumn = $joinColumn->referencedColumnName; + $quotedKeyColumn = $this->quoteStrategy->getJoinColumnName($joinColumn, $class, $this->platform); + + switch (true) { + case $sourceClass->containsForeignIdentifier: + $field = $sourceClass->getFieldForColumn($sourceKeyColumn); + $value = $sourceClass->reflFields[$field]->getValue($sourceEntity); + + if (isset($sourceClass->associationMappings[$field])) { + $value = $this->em->getUnitOfWork()->getEntityIdentifier($value); + $value = $value[$this->em->getClassMetadata($sourceClass->associationMappings[$field]->targetEntity)->identifier[0]]; + } + + break; + + case isset($sourceClass->fieldNames[$sourceKeyColumn]): + $field = $sourceClass->fieldNames[$sourceKeyColumn]; + $value = $sourceClass->reflFields[$field]->getValue($sourceEntity); + + break; + + default: + throw MappingException::joinColumnMustPointToMappedField( + $sourceClass->name, + $sourceKeyColumn, + ); + } + + $criteria[$quotedJoinTable . '.' . $quotedKeyColumn] = $value; + $parameters[] = [ + 'value' => $value, + 'field' => $field, + 'class' => $sourceClass, + ]; + } + + $sql = $this->getSelectSQL($criteria, $assoc, null, $limit, $offset); + [$params, $types] = $this->expandToManyParameters($parameters); + + return $this->conn->executeQuery($sql, $params, $types); + } + + public function getSelectSQL( + array|Criteria $criteria, + AssociationMapping|null $assoc = null, + LockMode|int|null $lockMode = null, + int|null $limit = null, + int|null $offset = null, + array|null $orderBy = null, + ): string { + $this->switchPersisterContext($offset, $limit); + + $joinSql = ''; + $orderBySql = ''; + + if ($assoc !== null && $assoc->isManyToMany()) { + $joinSql = $this->getSelectManyToManyJoinSQL($assoc); + } + + if ($assoc !== null && $assoc->isOrdered()) { + $orderBy = $assoc->orderBy(); + } + + if ($orderBy) { + $orderBySql = $this->getOrderBySQL($orderBy, $this->getSQLTableAlias($this->class->name)); + } + + $conditionSql = $criteria instanceof Criteria + ? $this->getSelectConditionCriteriaSQL($criteria) + : $this->getSelectConditionSQL($criteria, $assoc); + + $lockSql = match ($lockMode) { + LockMode::PESSIMISTIC_READ => ' ' . $this->getReadLockSQL($this->platform), + LockMode::PESSIMISTIC_WRITE => ' ' . $this->getWriteLockSQL($this->platform), + default => '', + }; + + $columnList = $this->getSelectColumnsSQL(); + $tableAlias = $this->getSQLTableAlias($this->class->name); + $filterSql = $this->generateFilterConditionSQL($this->class, $tableAlias); + $tableName = $this->quoteStrategy->getTableName($this->class, $this->platform); + + if ($filterSql !== '') { + $conditionSql = $conditionSql + ? $conditionSql . ' AND ' . $filterSql + : $filterSql; + } + + $select = 'SELECT ' . $columnList; + $from = ' FROM ' . $tableName . ' ' . $tableAlias; + $join = $this->currentPersisterContext->selectJoinSql . $joinSql; + $where = ($conditionSql ? ' WHERE ' . $conditionSql : ''); + $lock = $this->platform->appendLockHint($from, $lockMode ?? LockMode::NONE); + $query = $select + . $lock + . $join + . $where + . $orderBySql; + + return $this->platform->modifyLimitQuery($query, $limit, $offset ?? 0) . $lockSql; + } + + public function getCountSQL(array|Criteria $criteria = []): string + { + $tableName = $this->quoteStrategy->getTableName($this->class, $this->platform); + $tableAlias = $this->getSQLTableAlias($this->class->name); + + $conditionSql = $criteria instanceof Criteria + ? $this->getSelectConditionCriteriaSQL($criteria) + : $this->getSelectConditionSQL($criteria); + + $filterSql = $this->generateFilterConditionSQL($this->class, $tableAlias); + + if ($filterSql !== '') { + $conditionSql = $conditionSql + ? $conditionSql . ' AND ' . $filterSql + : $filterSql; + } + + return 'SELECT COUNT(*) ' + . 'FROM ' . $tableName . ' ' . $tableAlias + . (empty($conditionSql) ? '' : ' WHERE ' . $conditionSql); + } + + /** + * Gets the ORDER BY SQL snippet for ordered collections. + * + * @psalm-param array $orderBy + * + * @throws InvalidOrientation + * @throws InvalidFindByCall + * @throws UnrecognizedField + */ + final protected function getOrderBySQL(array $orderBy, string $baseTableAlias): string + { + $orderByList = []; + + foreach ($orderBy as $fieldName => $orientation) { + $orientation = strtoupper(trim($orientation)); + + if ($orientation !== 'ASC' && $orientation !== 'DESC') { + throw InvalidOrientation::fromClassNameAndField($this->class->name, $fieldName); + } + + if (isset($this->class->fieldMappings[$fieldName])) { + $tableAlias = isset($this->class->fieldMappings[$fieldName]->inherited) + ? $this->getSQLTableAlias($this->class->fieldMappings[$fieldName]->inherited) + : $baseTableAlias; + + $columnName = $this->quoteStrategy->getColumnName($fieldName, $this->class, $this->platform); + $orderByList[] = $tableAlias . '.' . $columnName . ' ' . $orientation; + + continue; + } + + if (isset($this->class->associationMappings[$fieldName])) { + $association = $this->class->associationMappings[$fieldName]; + if (! $association->isOwningSide()) { + throw InvalidFindByCall::fromInverseSideUsage($this->class->name, $fieldName); + } + + assert($association->isToOneOwningSide()); + + $tableAlias = isset($association->inherited) + ? $this->getSQLTableAlias($association->inherited) + : $baseTableAlias; + + foreach ($association->joinColumns as $joinColumn) { + $columnName = $this->quoteStrategy->getJoinColumnName($joinColumn, $this->class, $this->platform); + $orderByList[] = $tableAlias . '.' . $columnName . ' ' . $orientation; + } + + continue; + } + + throw UnrecognizedField::byFullyQualifiedName($this->class->name, $fieldName); + } + + return ' ORDER BY ' . implode(', ', $orderByList); + } + + /** + * Gets the SQL fragment with the list of columns to select when querying for + * an entity in this persister. + * + * Subclasses should override this method to alter or change the select column + * list SQL fragment. Note that in the implementation of BasicEntityPersister + * the resulting SQL fragment is generated only once and cached in {@link selectColumnListSql}. + * Subclasses may or may not do the same. + */ + protected function getSelectColumnsSQL(): string + { + if ($this->currentPersisterContext->selectColumnListSql !== null) { + return $this->currentPersisterContext->selectColumnListSql; + } + + $columnList = []; + $this->currentPersisterContext->rsm->addEntityResult($this->class->name, 'r'); // r for root + + // Add regular columns to select list + foreach ($this->class->fieldNames as $field) { + $columnList[] = $this->getSelectColumnSQL($field, $this->class); + } + + $this->currentPersisterContext->selectJoinSql = ''; + $eagerAliasCounter = 0; + + foreach ($this->class->associationMappings as $assocField => $assoc) { + $assocColumnSQL = $this->getSelectColumnAssociationSQL($assocField, $assoc, $this->class); + + if ($assocColumnSQL) { + $columnList[] = $assocColumnSQL; + } + + $isAssocToOneInverseSide = $assoc->isToOne() && ! $assoc->isOwningSide(); + $isAssocFromOneEager = $assoc->isToOne() && $assoc->fetch === ClassMetadata::FETCH_EAGER; + + if (! ($isAssocFromOneEager || $isAssocToOneInverseSide)) { + continue; + } + + if ($assoc->isToMany() && $this->currentPersisterContext->handlesLimits) { + continue; + } + + $eagerEntity = $this->em->getClassMetadata($assoc->targetEntity); + + if ($eagerEntity->inheritanceType !== ClassMetadata::INHERITANCE_TYPE_NONE) { + continue; // now this is why you shouldn't use inheritance + } + + $assocAlias = 'e' . ($eagerAliasCounter++); + $this->currentPersisterContext->rsm->addJoinedEntityResult($assoc->targetEntity, $assocAlias, 'r', $assocField); + + foreach ($eagerEntity->fieldNames as $field) { + $columnList[] = $this->getSelectColumnSQL($field, $eagerEntity, $assocAlias); + } + + foreach ($eagerEntity->associationMappings as $eagerAssocField => $eagerAssoc) { + $eagerAssocColumnSQL = $this->getSelectColumnAssociationSQL( + $eagerAssocField, + $eagerAssoc, + $eagerEntity, + $assocAlias, + ); + + if ($eagerAssocColumnSQL) { + $columnList[] = $eagerAssocColumnSQL; + } + } + + $association = $assoc; + $joinCondition = []; + + if ($assoc->isIndexed()) { + assert($assoc->isToMany()); + $this->currentPersisterContext->rsm->addIndexBy($assocAlias, $assoc->indexBy()); + } + + if (! $assoc->isOwningSide()) { + $eagerEntity = $this->em->getClassMetadata($assoc->targetEntity); + $association = $eagerEntity->getAssociationMapping($assoc->mappedBy); + } + + assert($association->isToOneOwningSide()); + + $joinTableAlias = $this->getSQLTableAlias($eagerEntity->name, $assocAlias); + $joinTableName = $this->quoteStrategy->getTableName($eagerEntity, $this->platform); + + if ($assoc->isOwningSide()) { + $tableAlias = $this->getSQLTableAlias($association->targetEntity, $assocAlias); + $this->currentPersisterContext->selectJoinSql .= ' ' . $this->getJoinSQLForJoinColumns($association->joinColumns); + + foreach ($association->joinColumns as $joinColumn) { + $sourceCol = $this->quoteStrategy->getJoinColumnName($joinColumn, $this->class, $this->platform); + $targetCol = $this->quoteStrategy->getReferencedJoinColumnName($joinColumn, $this->class, $this->platform); + $joinCondition[] = $this->getSQLTableAlias($association->sourceEntity) + . '.' . $sourceCol . ' = ' . $tableAlias . '.' . $targetCol; + } + + // Add filter SQL + $filterSql = $this->generateFilterConditionSQL($eagerEntity, $tableAlias); + if ($filterSql) { + $joinCondition[] = $filterSql; + } + } else { + $this->currentPersisterContext->selectJoinSql .= ' LEFT JOIN'; + + foreach ($association->joinColumns as $joinColumn) { + $sourceCol = $this->quoteStrategy->getJoinColumnName($joinColumn, $this->class, $this->platform); + $targetCol = $this->quoteStrategy->getReferencedJoinColumnName($joinColumn, $this->class, $this->platform); + + $joinCondition[] = $this->getSQLTableAlias($association->sourceEntity, $assocAlias) . '.' . $sourceCol . ' = ' + . $this->getSQLTableAlias($association->targetEntity) . '.' . $targetCol; + } + } + + $this->currentPersisterContext->selectJoinSql .= ' ' . $joinTableName . ' ' . $joinTableAlias . ' ON '; + $this->currentPersisterContext->selectJoinSql .= implode(' AND ', $joinCondition); + } + + $this->currentPersisterContext->selectColumnListSql = implode(', ', $columnList); + + return $this->currentPersisterContext->selectColumnListSql; + } + + /** Gets the SQL join fragment used when selecting entities from an association. */ + protected function getSelectColumnAssociationSQL( + string $field, + AssociationMapping $assoc, + ClassMetadata $class, + string $alias = 'r', + ): string { + if (! $assoc->isToOneOwningSide()) { + return ''; + } + + $columnList = []; + $targetClass = $this->em->getClassMetadata($assoc->targetEntity); + $isIdentifier = isset($assoc->id) && $assoc->id === true; + $sqlTableAlias = $this->getSQLTableAlias($class->name, ($alias === 'r' ? '' : $alias)); + + foreach ($assoc->joinColumns as $joinColumn) { + $quotedColumn = $this->quoteStrategy->getJoinColumnName($joinColumn, $this->class, $this->platform); + $resultColumnName = $this->getSQLColumnAlias($joinColumn->name); + $type = PersisterHelper::getTypeOfColumn($joinColumn->referencedColumnName, $targetClass, $this->em); + + $this->currentPersisterContext->rsm->addMetaResult($alias, $resultColumnName, $joinColumn->name, $isIdentifier, $type); + + $columnList[] = sprintf('%s.%s AS %s', $sqlTableAlias, $quotedColumn, $resultColumnName); + } + + return implode(', ', $columnList); + } + + /** + * Gets the SQL join fragment used when selecting entities from a + * many-to-many association. + */ + protected function getSelectManyToManyJoinSQL(AssociationMapping&ManyToManyAssociationMapping $manyToMany): string + { + $conditions = []; + $association = $manyToMany; + $sourceTableAlias = $this->getSQLTableAlias($this->class->name); + + $association = $this->em->getMetadataFactory()->getOwningSide($manyToMany); + $joinTableName = $this->quoteStrategy->getJoinTableName($association, $this->class, $this->platform); + $joinColumns = $manyToMany->isOwningSide() + ? $association->joinTable->inverseJoinColumns + : $association->joinTable->joinColumns; + + foreach ($joinColumns as $joinColumn) { + $quotedSourceColumn = $this->quoteStrategy->getJoinColumnName($joinColumn, $this->class, $this->platform); + $quotedTargetColumn = $this->quoteStrategy->getReferencedJoinColumnName($joinColumn, $this->class, $this->platform); + $conditions[] = $sourceTableAlias . '.' . $quotedTargetColumn . ' = ' . $joinTableName . '.' . $quotedSourceColumn; + } + + return ' INNER JOIN ' . $joinTableName . ' ON ' . implode(' AND ', $conditions); + } + + public function getInsertSQL(): string + { + if ($this->insertSql !== null) { + return $this->insertSql; + } + + $columns = $this->getInsertColumnList(); + $tableName = $this->quoteStrategy->getTableName($this->class, $this->platform); + + if (empty($columns)) { + $identityColumn = $this->quoteStrategy->getColumnName($this->class->identifier[0], $this->class, $this->platform); + $this->insertSql = $this->platform->getEmptyIdentityInsertSQL($tableName, $identityColumn); + + return $this->insertSql; + } + + $values = []; + $columns = array_unique($columns); + + foreach ($columns as $column) { + $placeholder = '?'; + + if ( + isset($this->class->fieldNames[$column]) + && isset($this->columnTypes[$this->class->fieldNames[$column]]) + && isset($this->class->fieldMappings[$this->class->fieldNames[$column]]) + ) { + $type = Type::getType($this->columnTypes[$this->class->fieldNames[$column]]); + $placeholder = $type->convertToDatabaseValueSQL('?', $this->platform); + } + + $values[] = $placeholder; + } + + $columns = implode(', ', $columns); + $values = implode(', ', $values); + + $this->insertSql = sprintf('INSERT INTO %s (%s) VALUES (%s)', $tableName, $columns, $values); + + return $this->insertSql; + } + + /** + * Gets the list of columns to put in the INSERT SQL statement. + * + * Subclasses should override this method to alter or change the list of + * columns placed in the INSERT statements used by the persister. + * + * @psalm-return list + */ + protected function getInsertColumnList(): array + { + $columns = []; + + foreach ($this->class->reflFields as $name => $field) { + if ($this->class->isVersioned && $this->class->versionField === $name) { + continue; + } + + if (isset($this->class->embeddedClasses[$name])) { + continue; + } + + if (isset($this->class->associationMappings[$name])) { + $assoc = $this->class->associationMappings[$name]; + + if ($assoc->isToOneOwningSide()) { + foreach ($assoc->joinColumns as $joinColumn) { + $columns[] = $this->quoteStrategy->getJoinColumnName($joinColumn, $this->class, $this->platform); + } + } + + continue; + } + + if (! $this->class->isIdGeneratorIdentity() || $this->class->identifier[0] !== $name) { + if (isset($this->class->fieldMappings[$name]->notInsertable)) { + continue; + } + + $columns[] = $this->quoteStrategy->getColumnName($name, $this->class, $this->platform); + $this->columnTypes[$name] = $this->class->fieldMappings[$name]->type; + } + } + + return $columns; + } + + /** + * Gets the SQL snippet of a qualified column name for the given field name. + * + * @param ClassMetadata $class The class that declares this field. The table this class is + * mapped to must own the column for the given field. + */ + protected function getSelectColumnSQL(string $field, ClassMetadata $class, string $alias = 'r'): string + { + $root = $alias === 'r' ? '' : $alias; + $tableAlias = $this->getSQLTableAlias($class->name, $root); + $fieldMapping = $class->fieldMappings[$field]; + $sql = sprintf('%s.%s', $tableAlias, $this->quoteStrategy->getColumnName($field, $class, $this->platform)); + $columnAlias = $this->getSQLColumnAlias($fieldMapping->columnName); + + $this->currentPersisterContext->rsm->addFieldResult($alias, $columnAlias, $field); + if (! empty($fieldMapping->enumType)) { + $this->currentPersisterContext->rsm->addEnumResult($columnAlias, $fieldMapping->enumType); + } + + $type = Type::getType($fieldMapping->type); + $sql = $type->convertToPHPValueSQL($sql, $this->platform); + + return $sql . ' AS ' . $columnAlias; + } + + /** + * Gets the SQL table alias for the given class name. + * + * @todo Reconsider. Binding table aliases to class names is not such a good idea. + */ + protected function getSQLTableAlias(string $className, string $assocName = ''): string + { + if ($assocName) { + $className .= '#' . $assocName; + } + + if (isset($this->currentPersisterContext->sqlTableAliases[$className])) { + return $this->currentPersisterContext->sqlTableAliases[$className]; + } + + $tableAlias = 't' . $this->currentPersisterContext->sqlAliasCounter++; + + $this->currentPersisterContext->sqlTableAliases[$className] = $tableAlias; + + return $tableAlias; + } + + /** + * {@inheritDoc} + */ + public function lock(array $criteria, LockMode|int $lockMode): void + { + $conditionSql = $this->getSelectConditionSQL($criteria); + + $lockSql = match ($lockMode) { + LockMode::PESSIMISTIC_READ => $this->getReadLockSQL($this->platform), + LockMode::PESSIMISTIC_WRITE => $this->getWriteLockSQL($this->platform), + default => '', + }; + + $lock = $this->getLockTablesSql($lockMode); + $where = ($conditionSql ? ' WHERE ' . $conditionSql : '') . ' '; + $sql = 'SELECT 1 ' + . $lock + . $where + . $lockSql; + + [$params, $types] = $this->expandParameters($criteria); + + $this->conn->executeQuery($sql, $params, $types); + } + + /** + * Gets the FROM and optionally JOIN conditions to lock the entity managed by this persister. + * + * @psalm-param LockMode::* $lockMode + */ + protected function getLockTablesSql(LockMode|int $lockMode): string + { + return $this->platform->appendLockHint( + 'FROM ' + . $this->quoteStrategy->getTableName($this->class, $this->platform) . ' ' + . $this->getSQLTableAlias($this->class->name), + $lockMode, + ); + } + + /** + * Gets the Select Where Condition from a Criteria object. + */ + protected function getSelectConditionCriteriaSQL(Criteria $criteria): string + { + $expression = $criteria->getWhereExpression(); + + if ($expression === null) { + return ''; + } + + $visitor = new SqlExpressionVisitor($this, $this->class); + + return $visitor->dispatch($expression); + } + + public function getSelectConditionStatementSQL( + string $field, + mixed $value, + AssociationMapping|null $assoc = null, + string|null $comparison = null, + ): string { + $selectedColumns = []; + $columns = $this->getSelectConditionStatementColumnSQL($field, $assoc); + + if (count($columns) > 1 && $comparison === Comparison::IN) { + /* + * @todo try to support multi-column IN expressions. + * Example: (col1, col2) IN (('val1A', 'val2A'), ('val1B', 'val2B')) + */ + throw CantUseInOperatorOnCompositeKeys::create(); + } + + foreach ($columns as $column) { + $placeholder = '?'; + + if (isset($this->class->fieldMappings[$field])) { + $type = Type::getType($this->class->fieldMappings[$field]->type); + $placeholder = $type->convertToDatabaseValueSQL($placeholder, $this->platform); + } + + if ($comparison !== null) { + // special case null value handling + if (($comparison === Comparison::EQ || $comparison === Comparison::IS) && $value === null) { + $selectedColumns[] = $column . ' IS NULL'; + + continue; + } + + if ($comparison === Comparison::NEQ && $value === null) { + $selectedColumns[] = $column . ' IS NOT NULL'; + + continue; + } + + $selectedColumns[] = $column . ' ' . sprintf(self::$comparisonMap[$comparison], $placeholder); + + continue; + } + + if (is_array($value)) { + $in = sprintf('%s IN (%s)', $column, $placeholder); + + if (array_search(null, $value, true) !== false) { + $selectedColumns[] = sprintf('(%s OR %s IS NULL)', $in, $column); + + continue; + } + + $selectedColumns[] = $in; + + continue; + } + + if ($value === null) { + $selectedColumns[] = sprintf('%s IS NULL', $column); + + continue; + } + + $selectedColumns[] = sprintf('%s = %s', $column, $placeholder); + } + + return implode(' AND ', $selectedColumns); + } + + /** + * Builds the left-hand-side of a where condition statement. + * + * @return string[] + * @psalm-return list + * + * @throws InvalidFindByCall + * @throws UnrecognizedField + */ + private function getSelectConditionStatementColumnSQL( + string $field, + AssociationMapping|null $assoc = null, + ): array { + if (isset($this->class->fieldMappings[$field])) { + $className = $this->class->fieldMappings[$field]->inherited ?? $this->class->name; + + return [$this->getSQLTableAlias($className) . '.' . $this->quoteStrategy->getColumnName($field, $this->class, $this->platform)]; + } + + if (isset($this->class->associationMappings[$field])) { + $association = $this->class->associationMappings[$field]; + // Many-To-Many requires join table check for joinColumn + $columns = []; + $class = $this->class; + + if ($association->isManyToMany()) { + assert($assoc !== null); + if (! $association->isOwningSide()) { + $association = $assoc; + } + + assert($association->isManyToManyOwningSide()); + + $joinTableName = $this->quoteStrategy->getJoinTableName($association, $class, $this->platform); + $joinColumns = $assoc->isOwningSide() + ? $association->joinTable->joinColumns + : $association->joinTable->inverseJoinColumns; + + foreach ($joinColumns as $joinColumn) { + $columns[] = $joinTableName . '.' . $this->quoteStrategy->getJoinColumnName($joinColumn, $class, $this->platform); + } + } else { + if (! $association->isOwningSide()) { + throw InvalidFindByCall::fromInverseSideUsage( + $this->class->name, + $field, + ); + } + + assert($association->isToOneOwningSide()); + + $className = $association->inherited ?? $this->class->name; + + foreach ($association->joinColumns as $joinColumn) { + $columns[] = $this->getSQLTableAlias($className) . '.' . $this->quoteStrategy->getJoinColumnName($joinColumn, $this->class, $this->platform); + } + } + + return $columns; + } + + if ($assoc !== null && ! str_contains($field, ' ') && ! str_contains($field, '(')) { + // very careless developers could potentially open up this normally hidden api for userland attacks, + // therefore checking for spaces and function calls which are not allowed. + + // found a join column condition, not really a "field" + return [$field]; + } + + throw UnrecognizedField::byFullyQualifiedName($this->class->name, $field); + } + + /** + * Gets the conditional SQL fragment used in the WHERE clause when selecting + * entities in this persister. + * + * Subclasses are supposed to override this method if they intend to change + * or alter the criteria by which entities are selected. + * + * @psalm-param array $criteria + */ + protected function getSelectConditionSQL(array $criteria, AssociationMapping|null $assoc = null): string + { + $conditions = []; + + foreach ($criteria as $field => $value) { + $conditions[] = $this->getSelectConditionStatementSQL($field, $value, $assoc); + } + + return implode(' AND ', $conditions); + } + + /** + * {@inheritDoc} + */ + public function getOneToManyCollection( + AssociationMapping $assoc, + object $sourceEntity, + int|null $offset = null, + int|null $limit = null, + ): array { + assert($assoc instanceof OneToManyAssociationMapping); + $this->switchPersisterContext($offset, $limit); + + $stmt = $this->getOneToManyStatement($assoc, $sourceEntity, $offset, $limit); + + return $this->loadArrayFromResult($assoc, $stmt); + } + + public function loadOneToManyCollection( + AssociationMapping $assoc, + object $sourceEntity, + PersistentCollection $collection, + ): mixed { + assert($assoc instanceof OneToManyAssociationMapping); + $stmt = $this->getOneToManyStatement($assoc, $sourceEntity); + + return $this->loadCollectionFromStatement($assoc, $stmt, $collection); + } + + /** Builds criteria and execute SQL statement to fetch the one to many entities from. */ + private function getOneToManyStatement( + OneToManyAssociationMapping $assoc, + object $sourceEntity, + int|null $offset = null, + int|null $limit = null, + ): Result { + $this->switchPersisterContext($offset, $limit); + + $criteria = []; + $parameters = []; + $owningAssoc = $this->class->associationMappings[$assoc->mappedBy]; + $sourceClass = $this->em->getClassMetadata($assoc->sourceEntity); + $tableAlias = $this->getSQLTableAlias($owningAssoc->inherited ?? $this->class->name); + assert($owningAssoc->isManyToOne()); + + foreach ($owningAssoc->targetToSourceKeyColumns as $sourceKeyColumn => $targetKeyColumn) { + if ($sourceClass->containsForeignIdentifier) { + $field = $sourceClass->getFieldForColumn($sourceKeyColumn); + $value = $sourceClass->reflFields[$field]->getValue($sourceEntity); + + if (isset($sourceClass->associationMappings[$field])) { + $value = $this->em->getUnitOfWork()->getEntityIdentifier($value); + $value = $value[$this->em->getClassMetadata($sourceClass->associationMappings[$field]->targetEntity)->identifier[0]]; + } + + $criteria[$tableAlias . '.' . $targetKeyColumn] = $value; + $parameters[] = [ + 'value' => $value, + 'field' => $field, + 'class' => $sourceClass, + ]; + + continue; + } + + $field = $sourceClass->fieldNames[$sourceKeyColumn]; + $value = $sourceClass->reflFields[$field]->getValue($sourceEntity); + + $criteria[$tableAlias . '.' . $targetKeyColumn] = $value; + $parameters[] = [ + 'value' => $value, + 'field' => $field, + 'class' => $sourceClass, + ]; + } + + $sql = $this->getSelectSQL($criteria, $assoc, null, $limit, $offset); + [$params, $types] = $this->expandToManyParameters($parameters); + + return $this->conn->executeQuery($sql, $params, $types); + } + + /** + * {@inheritDoc} + */ + public function expandParameters(array $criteria): array + { + $params = []; + $types = []; + + foreach ($criteria as $field => $value) { + if ($value === null) { + continue; // skip null values. + } + + $types = [...$types, ...$this->getTypes($field, $value, $this->class)]; + $params = array_merge($params, $this->getValues($value)); + } + + return [$params, $types]; + } + + /** + * Expands the parameters from the given criteria and use the correct binding types if found, + * specialized for OneToMany or ManyToMany associations. + * + * @param mixed[][] $criteria an array of arrays containing following: + * - field to which each criterion will be bound + * - value to be bound + * - class to which the field belongs to + * + * @return mixed[][] + * @psalm-return array{0: array, 1: list} + */ + private function expandToManyParameters(array $criteria): array + { + $params = []; + $types = []; + + foreach ($criteria as $criterion) { + if ($criterion['value'] === null) { + continue; // skip null values. + } + + $types = [...$types, ...$this->getTypes($criterion['field'], $criterion['value'], $criterion['class'])]; + $params = array_merge($params, $this->getValues($criterion['value'])); + } + + return [$params, $types]; + } + + /** + * Infers field types to be used by parameter type casting. + * + * @return list + * @psalm-return list + * + * @throws QueryException + */ + private function getTypes(string $field, mixed $value, ClassMetadata $class): array + { + $types = []; + + switch (true) { + case isset($class->fieldMappings[$field]): + $types = array_merge($types, [$class->fieldMappings[$field]->type]); + break; + + case isset($class->associationMappings[$field]): + $assoc = $this->em->getMetadataFactory()->getOwningSide($class->associationMappings[$field]); + $class = $this->em->getClassMetadata($assoc->targetEntity); + + if ($assoc->isManyToManyOwningSide()) { + $columns = $assoc->relationToTargetKeyColumns; + } else { + assert($assoc->isToOneOwningSide()); + $columns = $assoc->sourceToTargetKeyColumns; + } + + foreach ($columns as $column) { + $types[] = PersisterHelper::getTypeOfColumn($column, $class, $this->em); + } + + break; + + default: + $types[] = ParameterType::STRING; + break; + } + + if (is_array($value)) { + return array_map($this->getArrayBindingType(...), $types); + } + + return $types; + } + + /** @psalm-return ArrayParameterType::* */ + private function getArrayBindingType(ParameterType|int|string $type): ArrayParameterType|int + { + if (! $type instanceof ParameterType) { + $type = Type::getType((string) $type)->getBindingType(); + } + + return match ($type) { + ParameterType::STRING => ArrayParameterType::STRING, + ParameterType::INTEGER => ArrayParameterType::INTEGER, + ParameterType::ASCII => ArrayParameterType::ASCII, + }; + } + + /** + * Retrieves the parameters that identifies a value. + * + * @return mixed[] + */ + private function getValues(mixed $value): array + { + if (is_array($value)) { + $newValue = []; + + foreach ($value as $itemValue) { + $newValue = array_merge($newValue, $this->getValues($itemValue)); + } + + return [$newValue]; + } + + return $this->getIndividualValue($value); + } + + /** + * Retrieves an individual parameter value. + * + * @psalm-return list + */ + private function getIndividualValue(mixed $value): array + { + if (! is_object($value)) { + return [$value]; + } + + if ($value instanceof BackedEnum) { + return [$value->value]; + } + + $valueClass = DefaultProxyClassNameResolver::getClass($value); + + if ($this->em->getMetadataFactory()->isTransient($valueClass)) { + return [$value]; + } + + $class = $this->em->getClassMetadata($valueClass); + + if ($class->isIdentifierComposite) { + $newValue = []; + + foreach ($class->getIdentifierValues($value) as $innerValue) { + $newValue = array_merge($newValue, $this->getValues($innerValue)); + } + + return $newValue; + } + + return [$this->em->getUnitOfWork()->getSingleIdentifierValue($value)]; + } + + public function exists(object $entity, Criteria|null $extraConditions = null): bool + { + $criteria = $this->class->getIdentifierValues($entity); + + if (! $criteria) { + return false; + } + + $alias = $this->getSQLTableAlias($this->class->name); + + $sql = 'SELECT 1 ' + . $this->getLockTablesSql(LockMode::NONE) + . ' WHERE ' . $this->getSelectConditionSQL($criteria); + + [$params, $types] = $this->expandParameters($criteria); + + if ($extraConditions !== null) { + $sql .= ' AND ' . $this->getSelectConditionCriteriaSQL($extraConditions); + [$criteriaParams, $criteriaTypes] = $this->expandCriteriaParameters($extraConditions); + + $params = [...$params, ...$criteriaParams]; + $types = [...$types, ...$criteriaTypes]; + } + + $filterSql = $this->generateFilterConditionSQL($this->class, $alias); + if ($filterSql) { + $sql .= ' AND ' . $filterSql; + } + + return (bool) $this->conn->fetchOne($sql, $params, $types); + } + + /** + * Generates the appropriate join SQL for the given join column. + * + * @param list $joinColumns The join columns definition of an association. + * + * @return string LEFT JOIN if one of the columns is nullable, INNER JOIN otherwise. + */ + protected function getJoinSQLForJoinColumns(array $joinColumns): string + { + // if one of the join columns is nullable, return left join + foreach ($joinColumns as $joinColumn) { + if (! isset($joinColumn->nullable) || $joinColumn->nullable) { + return 'LEFT JOIN'; + } + } + + return 'INNER JOIN'; + } + + public function getSQLColumnAlias(string $columnName): string + { + return $this->quoteStrategy->getColumnAlias($columnName, $this->currentPersisterContext->sqlAliasCounter++, $this->platform); + } + + /** + * Generates the filter SQL for a given entity and table alias. + * + * @param ClassMetadata $targetEntity Metadata of the target entity. + * @param string $targetTableAlias The table alias of the joined/selected table. + * + * @return string The SQL query part to add to a query. + */ + protected function generateFilterConditionSQL(ClassMetadata $targetEntity, string $targetTableAlias): string + { + $filterClauses = []; + + foreach ($this->em->getFilters()->getEnabledFilters() as $filter) { + $filterExpr = $filter->addFilterConstraint($targetEntity, $targetTableAlias); + if ($filterExpr !== '') { + $filterClauses[] = '(' . $filterExpr . ')'; + } + } + + $sql = implode(' AND ', $filterClauses); + + return $sql ? '(' . $sql . ')' : ''; // Wrap again to avoid "X or Y and FilterConditionSQL" + } + + /** + * Switches persister context according to current query offset/limits + * + * This is due to the fact that to-many associations cannot be fetch-joined when a limit is involved + */ + protected function switchPersisterContext(int|null $offset, int|null $limit): void + { + if ($offset === null && $limit === null) { + $this->currentPersisterContext = $this->noLimitsContext; + + return; + } + + $this->currentPersisterContext = $this->limitsHandlingContext; + } + + /** + * @return string[] + * @psalm-return list + */ + protected function getClassIdentifiersTypes(ClassMetadata $class): array + { + $entityManager = $this->em; + + return array_map( + static function ($fieldName) use ($class, $entityManager): string { + $types = PersisterHelper::getTypeOfField($fieldName, $class, $entityManager); + assert(isset($types[0])); + + return $types[0]; + }, + $class->identifier, + ); + } +} 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 @@ + + */ + public array $sqlTableAliases = []; + + public function __construct( + /** + * Metadata object that describes the mapping of the mapped entity class. + */ + public ClassMetadata $class, + /** + * ResultSetMapping that is used for all queries. Is generated lazily once per request. + */ + public ResultSetMapping $rsm, + /** + * Whether this persistent context is considering limit operations applied to the selection queries + */ + public bool $handlesLimits, + ) { + } +} 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 @@ +, list} + */ + public function expandParameters(array $criteria): array; + + /** + * Expands Criteria Parameters by walking the expressions and grabbing all parameters and types from it. + * + * @psalm-return array{list, list} + */ + public function expandCriteriaParameters(Criteria $criteria): array; + + /** Gets the SQL WHERE condition for matching a field with a given value. */ + public function getSelectConditionStatementSQL( + string $field, + mixed $value, + AssociationMapping|null $assoc = null, + string|null $comparison = null, + ): string; + + /** + * Adds an entity to the queued insertions. + * The entity remains queued until {@link executeInserts} is invoked. + */ + public function addInsert(object $entity): void; + + /** + * Executes all queued entity insertions. + * + * If no inserts are queued, invoking this method is a NOOP. + */ + public function executeInserts(): void; + + /** + * Updates a managed entity. The entity is updated according to its current changeset + * in the running UnitOfWork. If there is no changeset, nothing is updated. + */ + public function update(object $entity): void; + + /** + * Deletes a managed entity. + * + * The entity to delete must be managed and have a persistent identifier. + * The deletion happens instantaneously. + * + * Subclasses may override this method to customize the semantics of entity deletion. + * + * @return bool TRUE if the entity got deleted in the database, FALSE otherwise. + */ + public function delete(object $entity): bool; + + /** + * Count entities (optionally filtered by a criteria) + * + * @param mixed[]|Criteria $criteria + */ + public function count(array|Criteria $criteria = []): int; + + /** + * Gets the name of the table that owns the column the given field is mapped to. + * + * The default implementation in BasicEntityPersister always returns the name + * of the table the entity type of this persister is mapped to, since an entity + * is always persisted to a single table with a BasicEntityPersister. + */ + public function getOwningTable(string $fieldName): string; + + /** + * Loads an entity by a list of field criteria. + * + * @param mixed[] $criteria The criteria by which to load the entity. + * @param object|null $entity The entity to load the data into. If not specified, + * a new entity is created. + * @param AssociationMapping|null $assoc The association that connects the entity + * to load to another entity, if any. + * @param mixed[] $hints Hints for entity creation. + * @param LockMode|int|null $lockMode One of the \Doctrine\DBAL\LockMode::* constants + * or NULL if no specific lock mode should be used + * for loading the entity. + * @param int|null $limit Limit number of results. + * @param string[]|null $orderBy Criteria to order by. + * @psalm-param array $criteria + * @psalm-param array $hints + * @psalm-param LockMode::*|null $lockMode + * @psalm-param array|null $orderBy + * + * @return object|null The loaded and managed entity instance or NULL if the entity can not be found. + * + * @todo Check identity map? loadById method? Try to guess whether $criteria is the id? + */ + public function load( + array $criteria, + object|null $entity = null, + AssociationMapping|null $assoc = null, + array $hints = [], + LockMode|int|null $lockMode = null, + int|null $limit = null, + array|null $orderBy = null, + ): object|null; + + /** + * Loads an entity by identifier. + * + * @param object|null $entity The entity to load the data into. If not specified, a new entity is created. + * @psalm-param array $identifier The entity identifier. + * + * @return object|null The loaded and managed entity instance or NULL if the entity can not be found. + * + * @todo Check parameters + */ + public function loadById(array $identifier, object|null $entity = null): object|null; + + /** + * Loads an entity of this persister's mapped class as part of a single-valued + * association from another entity. + * + * @param AssociationMapping $assoc The association to load. + * @param object $sourceEntity The entity that owns the association (not necessarily the "owning side"). + * @psalm-param array $identifier The identifier of the entity to load. Must be provided if + * the association to load represents the owning side, otherwise + * the identifier is derived from the $sourceEntity. + * + * @return object|null The loaded and managed entity instance or NULL if the entity can not be found. + * + * @throws MappingException + */ + public function loadOneToOneEntity(AssociationMapping $assoc, object $sourceEntity, array $identifier = []): object|null; + + /** + * Refreshes a managed entity. + * + * @param LockMode|int|null $lockMode One of the \Doctrine\DBAL\LockMode::* constants + * or NULL if no specific lock mode should be used + * for refreshing the managed entity. + * @psalm-param array $id The identifier of the entity as an + * associative array from column or + * field names to values. + * @psalm-param LockMode::*|null $lockMode + */ + public function refresh(array $id, object $entity, LockMode|int|null $lockMode = null): void; + + /** + * Loads Entities matching the given Criteria object. + * + * @return mixed[] + */ + public function loadCriteria(Criteria $criteria): array; + + /** + * Loads a list of entities by a list of field criteria. + * + * @psalm-param array|null $orderBy + * @psalm-param array $criteria + * + * @return mixed[] + */ + public function loadAll( + array $criteria = [], + array|null $orderBy = null, + int|null $limit = null, + int|null $offset = null, + ): array; + + /** + * Gets (sliced or full) elements of the given collection. + * + * @return mixed[] + */ + public function getManyToManyCollection( + AssociationMapping $assoc, + object $sourceEntity, + int|null $offset = null, + int|null $limit = null, + ): array; + + /** + * Loads a collection of entities of a many-to-many association. + * + * @param AssociationMapping $assoc The association mapping of the association being loaded. + * @param object $sourceEntity The entity that owns the collection. + * @param PersistentCollection $collection The collection to fill. + * + * @return mixed[] + */ + public function loadManyToManyCollection( + AssociationMapping $assoc, + object $sourceEntity, + PersistentCollection $collection, + ): array; + + /** + * Loads a collection of entities in a one-to-many association. + * + * @param PersistentCollection $collection The collection to load/fill. + */ + public function loadOneToManyCollection( + AssociationMapping $assoc, + object $sourceEntity, + PersistentCollection $collection, + ): mixed; + + /** + * Locks all rows of this entity matching the given criteria with the specified pessimistic lock mode. + * + * @psalm-param array $criteria + * @psalm-param LockMode::* $lockMode + */ + public function lock(array $criteria, LockMode|int $lockMode): void; + + /** + * Returns an array with (sliced or full list) of elements in the specified collection. + * + * @return mixed[] + */ + public function getOneToManyCollection( + AssociationMapping $assoc, + object $sourceEntity, + int|null $offset = null, + int|null $limit = null, + ): array; + + /** + * Checks whether the given managed entity exists in the database. + */ + public function exists(object $entity, Criteria|null $extraConditions = null): bool; +} 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 @@ +Class Table Inheritance strategy. + * + * @see https://martinfowler.com/eaaCatalog/classTableInheritance.html + */ +class JoinedSubclassPersister extends AbstractEntityInheritancePersister +{ + use LockSqlHelper; + use SQLResultCasing; + + /** + * Map that maps column names to the table names that own them. + * This is mainly a temporary cache, used during a single request. + * + * @psalm-var array + */ + private array $owningTableMap = []; + + /** + * Map of table to quoted table names. + * + * @psalm-var array + */ + private array $quotedTableMap = []; + + protected function getDiscriminatorColumnTableName(): string + { + $class = $this->class->name !== $this->class->rootEntityName + ? $this->em->getClassMetadata($this->class->rootEntityName) + : $this->class; + + return $class->getTableName(); + } + + /** + * This function finds the ClassMetadata instance in an inheritance hierarchy + * that is responsible for enabling versioning. + */ + private function getVersionedClassMetadata(): ClassMetadata + { + if (isset($this->class->fieldMappings[$this->class->versionField]->inherited)) { + $definingClassName = $this->class->fieldMappings[$this->class->versionField]->inherited; + + return $this->em->getClassMetadata($definingClassName); + } + + return $this->class; + } + + /** + * Gets the name of the table that owns the column the given field is mapped to. + */ + public function getOwningTable(string $fieldName): string + { + if (isset($this->owningTableMap[$fieldName])) { + return $this->owningTableMap[$fieldName]; + } + + $cm = match (true) { + isset($this->class->associationMappings[$fieldName]->inherited) + => $this->em->getClassMetadata($this->class->associationMappings[$fieldName]->inherited), + isset($this->class->fieldMappings[$fieldName]->inherited) + => $this->em->getClassMetadata($this->class->fieldMappings[$fieldName]->inherited), + default => $this->class, + }; + + $tableName = $cm->getTableName(); + $quotedTableName = $this->quoteStrategy->getTableName($cm, $this->platform); + + $this->owningTableMap[$fieldName] = $tableName; + $this->quotedTableMap[$tableName] = $quotedTableName; + + return $tableName; + } + + public function executeInserts(): void + { + if (! $this->queuedInserts) { + return; + } + + $uow = $this->em->getUnitOfWork(); + $idGenerator = $this->class->idGenerator; + $isPostInsertId = $idGenerator->isPostInsertGenerator(); + $rootClass = $this->class->name !== $this->class->rootEntityName + ? $this->em->getClassMetadata($this->class->rootEntityName) + : $this->class; + + // Prepare statement for the root table + $rootPersister = $this->em->getUnitOfWork()->getEntityPersister($rootClass->name); + $rootTableName = $rootClass->getTableName(); + $rootTableStmt = $this->conn->prepare($rootPersister->getInsertSQL()); + + // Prepare statements for sub tables. + $subTableStmts = []; + + if ($rootClass !== $this->class) { + $subTableStmts[$this->class->getTableName()] = $this->conn->prepare($this->getInsertSQL()); + } + + foreach ($this->class->parentClasses as $parentClassName) { + $parentClass = $this->em->getClassMetadata($parentClassName); + $parentTableName = $parentClass->getTableName(); + + if ($parentClass !== $rootClass) { + $parentPersister = $this->em->getUnitOfWork()->getEntityPersister($parentClassName); + $subTableStmts[$parentTableName] = $this->conn->prepare($parentPersister->getInsertSQL()); + } + } + + // Execute all inserts. For each entity: + // 1) Insert on root table + // 2) Insert on sub tables + foreach ($this->queuedInserts as $entity) { + $insertData = $this->prepareInsertData($entity); + + // Execute insert on root table + $paramIndex = 1; + + foreach ($insertData[$rootTableName] as $columnName => $value) { + $rootTableStmt->bindValue($paramIndex++, $value, $this->columnTypes[$columnName]); + } + + $rootTableStmt->executeStatement(); + + if ($isPostInsertId) { + $generatedId = $idGenerator->generateId($this->em, $entity); + $id = [$this->class->identifier[0] => $generatedId]; + + $uow->assignPostInsertId($entity, $generatedId); + } else { + $id = $this->em->getUnitOfWork()->getEntityIdentifier($entity); + } + + // Execute inserts on subtables. + // The order doesn't matter because all child tables link to the root table via FK. + foreach ($subTableStmts as $tableName => $stmt) { + $paramIndex = 1; + $data = $insertData[$tableName] ?? []; + + foreach ($id as $idName => $idVal) { + $type = $this->columnTypes[$idName] ?? Types::STRING; + + $stmt->bindValue($paramIndex++, $idVal, $type); + } + + foreach ($data as $columnName => $value) { + if (! isset($id[$columnName])) { + $stmt->bindValue($paramIndex++, $value, $this->columnTypes[$columnName]); + } + } + + $stmt->executeStatement(); + } + + if ($this->class->requiresFetchAfterChange) { + $this->assignDefaultVersionAndUpsertableValues($entity, $id); + } + } + + $this->queuedInserts = []; + } + + public function update(object $entity): void + { + $updateData = $this->prepareUpdateData($entity); + + if (! $updateData) { + return; + } + + $isVersioned = $this->class->isVersioned; + + $versionedClass = $this->getVersionedClassMetadata(); + $versionedTable = $versionedClass->getTableName(); + + foreach ($updateData as $tableName => $data) { + $tableName = $this->quotedTableMap[$tableName]; + $versioned = $isVersioned && $versionedTable === $tableName; + + $this->updateTable($entity, $tableName, $data, $versioned); + } + + if ($this->class->requiresFetchAfterChange) { + // Make sure the table with the version column is updated even if no columns on that + // table were affected. + if ($isVersioned && ! isset($updateData[$versionedTable])) { + $tableName = $this->quoteStrategy->getTableName($versionedClass, $this->platform); + + $this->updateTable($entity, $tableName, [], true); + } + + $identifiers = $this->em->getUnitOfWork()->getEntityIdentifier($entity); + + $this->assignDefaultVersionAndUpsertableValues($entity, $identifiers); + } + } + + public function delete(object $entity): bool + { + $identifier = $this->em->getUnitOfWork()->getEntityIdentifier($entity); + $id = array_combine($this->class->getIdentifierColumnNames(), $identifier); + $types = $this->getClassIdentifiersTypes($this->class); + + $this->deleteJoinTableRecords($identifier, $types); + + // Delete the row from the root table. Cascades do the rest. + $rootClass = $this->em->getClassMetadata($this->class->rootEntityName); + $rootTable = $this->quoteStrategy->getTableName($rootClass, $this->platform); + $rootTypes = $this->getClassIdentifiersTypes($rootClass); + + return (bool) $this->conn->delete($rootTable, $id, $rootTypes); + } + + public function getSelectSQL( + array|Criteria $criteria, + AssociationMapping|null $assoc = null, + LockMode|int|null $lockMode = null, + int|null $limit = null, + int|null $offset = null, + array|null $orderBy = null, + ): string { + $this->switchPersisterContext($offset, $limit); + + $baseTableAlias = $this->getSQLTableAlias($this->class->name); + $joinSql = $this->getJoinSql($baseTableAlias); + + if ($assoc !== null && $assoc->isManyToMany()) { + $joinSql .= $this->getSelectManyToManyJoinSQL($assoc); + } + + $conditionSql = $criteria instanceof Criteria + ? $this->getSelectConditionCriteriaSQL($criteria) + : $this->getSelectConditionSQL($criteria, $assoc); + + $filterSql = $this->generateFilterConditionSQL( + $this->em->getClassMetadata($this->class->rootEntityName), + $this->getSQLTableAlias($this->class->rootEntityName), + ); + // If the current class in the root entity, add the filters + if ($filterSql) { + $conditionSql .= $conditionSql + ? ' AND ' . $filterSql + : $filterSql; + } + + $orderBySql = ''; + + if ($assoc !== null && $assoc->isOrdered()) { + $orderBy = $assoc->orderBy(); + } + + if ($orderBy) { + $orderBySql = $this->getOrderBySQL($orderBy, $baseTableAlias); + } + + $lockSql = ''; + + switch ($lockMode) { + case LockMode::PESSIMISTIC_READ: + $lockSql = ' ' . $this->getReadLockSQL($this->platform); + + break; + + case LockMode::PESSIMISTIC_WRITE: + $lockSql = ' ' . $this->getWriteLockSQL($this->platform); + + break; + } + + $tableName = $this->quoteStrategy->getTableName($this->class, $this->platform); + $from = ' FROM ' . $tableName . ' ' . $baseTableAlias; + $where = $conditionSql !== '' ? ' WHERE ' . $conditionSql : ''; + $lock = $this->platform->appendLockHint($from, $lockMode ?? LockMode::NONE); + $columnList = $this->getSelectColumnsSQL(); + $query = 'SELECT ' . $columnList + . $lock + . $joinSql + . $where + . $orderBySql; + + return $this->platform->modifyLimitQuery($query, $limit, $offset ?? 0) . $lockSql; + } + + public function getCountSQL(array|Criteria $criteria = []): string + { + $tableName = $this->quoteStrategy->getTableName($this->class, $this->platform); + $baseTableAlias = $this->getSQLTableAlias($this->class->name); + $joinSql = $this->getJoinSql($baseTableAlias); + + $conditionSql = $criteria instanceof Criteria + ? $this->getSelectConditionCriteriaSQL($criteria) + : $this->getSelectConditionSQL($criteria); + + $filterSql = $this->generateFilterConditionSQL($this->em->getClassMetadata($this->class->rootEntityName), $this->getSQLTableAlias($this->class->rootEntityName)); + + if ($filterSql !== '') { + $conditionSql = $conditionSql + ? $conditionSql . ' AND ' . $filterSql + : $filterSql; + } + + return 'SELECT COUNT(*) ' + . 'FROM ' . $tableName . ' ' . $baseTableAlias + . $joinSql + . (empty($conditionSql) ? '' : ' WHERE ' . $conditionSql); + } + + protected function getLockTablesSql(LockMode|int $lockMode): string + { + $joinSql = ''; + $identifierColumns = $this->class->getIdentifierColumnNames(); + $baseTableAlias = $this->getSQLTableAlias($this->class->name); + + // INNER JOIN parent tables + foreach ($this->class->parentClasses as $parentClassName) { + $conditions = []; + $tableAlias = $this->getSQLTableAlias($parentClassName); + $parentClass = $this->em->getClassMetadata($parentClassName); + $joinSql .= ' INNER JOIN ' . $this->quoteStrategy->getTableName($parentClass, $this->platform) . ' ' . $tableAlias . ' ON '; + + foreach ($identifierColumns as $idColumn) { + $conditions[] = $baseTableAlias . '.' . $idColumn . ' = ' . $tableAlias . '.' . $idColumn; + } + + $joinSql .= implode(' AND ', $conditions); + } + + return parent::getLockTablesSql($lockMode) . $joinSql; + } + + /** + * Ensure this method is never called. This persister overrides getSelectEntitiesSQL directly. + */ + protected function getSelectColumnsSQL(): string + { + // Create the column list fragment only once + if ($this->currentPersisterContext->selectColumnListSql !== null) { + return $this->currentPersisterContext->selectColumnListSql; + } + + $columnList = []; + $discrColumn = $this->class->getDiscriminatorColumn(); + $discrColumnName = $discrColumn->name; + $discrColumnType = $discrColumn->type; + $baseTableAlias = $this->getSQLTableAlias($this->class->name); + $resultColumnName = $this->getSQLResultCasing($this->platform, $discrColumnName); + + $this->currentPersisterContext->rsm->addEntityResult($this->class->name, 'r'); + $this->currentPersisterContext->rsm->setDiscriminatorColumn('r', $resultColumnName); + $this->currentPersisterContext->rsm->addMetaResult('r', $resultColumnName, $discrColumnName, false, $discrColumnType); + + // Add regular columns + foreach ($this->class->fieldMappings as $fieldName => $mapping) { + $class = isset($mapping->inherited) + ? $this->em->getClassMetadata($mapping->inherited) + : $this->class; + + $columnList[] = $this->getSelectColumnSQL($fieldName, $class); + } + + // Add foreign key columns + foreach ($this->class->associationMappings as $mapping) { + if (! $mapping->isToOneOwningSide()) { + continue; + } + + $tableAlias = isset($mapping->inherited) + ? $this->getSQLTableAlias($mapping->inherited) + : $baseTableAlias; + + $targetClass = $this->em->getClassMetadata($mapping->targetEntity); + + foreach ($mapping->joinColumns as $joinColumn) { + $columnList[] = $this->getSelectJoinColumnSQL( + $tableAlias, + $joinColumn->name, + $this->quoteStrategy->getJoinColumnName($joinColumn, $this->class, $this->platform), + PersisterHelper::getTypeOfColumn($joinColumn->referencedColumnName, $targetClass, $this->em), + ); + } + } + + // Add discriminator column (DO NOT ALIAS, see AbstractEntityInheritancePersister#processSQLResult). + $tableAlias = $this->class->rootEntityName === $this->class->name + ? $baseTableAlias + : $this->getSQLTableAlias($this->class->rootEntityName); + + $columnList[] = $tableAlias . '.' . $discrColumnName; + + // sub tables + foreach ($this->class->subClasses as $subClassName) { + $subClass = $this->em->getClassMetadata($subClassName); + $tableAlias = $this->getSQLTableAlias($subClassName); + + // Add subclass columns + foreach ($subClass->fieldMappings as $fieldName => $mapping) { + if (isset($mapping->inherited)) { + continue; + } + + $columnList[] = $this->getSelectColumnSQL($fieldName, $subClass); + } + + // Add join columns (foreign keys) + foreach ($subClass->associationMappings as $mapping) { + if (! $mapping->isToOneOwningSide() || isset($mapping->inherited)) { + continue; + } + + $targetClass = $this->em->getClassMetadata($mapping->targetEntity); + + foreach ($mapping->joinColumns as $joinColumn) { + $columnList[] = $this->getSelectJoinColumnSQL( + $tableAlias, + $joinColumn->name, + $this->quoteStrategy->getJoinColumnName($joinColumn, $subClass, $this->platform), + PersisterHelper::getTypeOfColumn($joinColumn->referencedColumnName, $targetClass, $this->em), + ); + } + } + } + + $this->currentPersisterContext->selectColumnListSql = implode(', ', $columnList); + + return $this->currentPersisterContext->selectColumnListSql; + } + + /** + * {@inheritDoc} + */ + protected function getInsertColumnList(): array + { + // Identifier columns must always come first in the column list of subclasses. + $columns = $this->class->parentClasses + ? $this->class->getIdentifierColumnNames() + : []; + + foreach ($this->class->reflFields as $name => $field) { + if ( + isset($this->class->fieldMappings[$name]->inherited) + && ! isset($this->class->fieldMappings[$name]->id) + || isset($this->class->associationMappings[$name]->inherited) + || ($this->class->isVersioned && $this->class->versionField === $name) + || isset($this->class->embeddedClasses[$name]) + || isset($this->class->fieldMappings[$name]->notInsertable) + ) { + continue; + } + + if (isset($this->class->associationMappings[$name])) { + $assoc = $this->class->associationMappings[$name]; + if ($assoc->isToOneOwningSide()) { + foreach ($assoc->targetToSourceKeyColumns as $sourceCol) { + $columns[] = $sourceCol; + } + } + } elseif ( + $this->class->name !== $this->class->rootEntityName || + ! $this->class->isIdGeneratorIdentity() || $this->class->identifier[0] !== $name + ) { + $columns[] = $this->quoteStrategy->getColumnName($name, $this->class, $this->platform); + $this->columnTypes[$name] = $this->class->fieldMappings[$name]->type; + } + } + + // Add discriminator column if it is the topmost class. + if ($this->class->name === $this->class->rootEntityName) { + $columns[] = $this->class->getDiscriminatorColumn()->name; + } + + return $columns; + } + + /** + * {@inheritDoc} + */ + protected function assignDefaultVersionAndUpsertableValues(object $entity, array $id): void + { + $values = $this->fetchVersionAndNotUpsertableValues($this->getVersionedClassMetadata(), $id); + + foreach ($values as $field => $value) { + $value = Type::getType($this->class->fieldMappings[$field]->type)->convertToPHPValue($value, $this->platform); + + $this->class->setFieldValue($entity, $field, $value); + } + } + + /** + * {@inheritDoc} + */ + protected function fetchVersionAndNotUpsertableValues(ClassMetadata $versionedClass, array $id): mixed + { + $columnNames = []; + foreach ($this->class->fieldMappings as $key => $column) { + $class = null; + if ($this->class->isVersioned && $key === $versionedClass->versionField) { + $class = $versionedClass; + } elseif (isset($column->generated)) { + $class = isset($column->inherited) + ? $this->em->getClassMetadata($column->inherited) + : $this->class; + } else { + continue; + } + + $columnNames[$key] = $this->getSelectColumnSQL($key, $class); + } + + $tableName = $this->quoteStrategy->getTableName($versionedClass, $this->platform); + $baseTableAlias = $this->getSQLTableAlias($this->class->name); + $joinSql = $this->getJoinSql($baseTableAlias); + $identifier = $this->quoteStrategy->getIdentifierColumnNames($versionedClass, $this->platform); + foreach ($identifier as $i => $idValue) { + $identifier[$i] = $baseTableAlias . '.' . $idValue; + } + + $sql = 'SELECT ' . implode(', ', $columnNames) + . ' FROM ' . $tableName . ' ' . $baseTableAlias + . $joinSql + . ' WHERE ' . implode(' = ? AND ', $identifier) . ' = ?'; + + $flatId = $this->identifierFlattener->flattenIdentifier($versionedClass, $id); + $values = $this->conn->fetchNumeric( + $sql, + array_values($flatId), + $this->extractIdentifierTypes($id, $versionedClass), + ); + + if ($values === false) { + throw new LengthException('Unexpected empty result for database query.'); + } + + $values = array_combine(array_keys($columnNames), $values); + + if (! $values) { + throw new LengthException('Unexpected number of database columns.'); + } + + return $values; + } + + private function getJoinSql(string $baseTableAlias): string + { + $joinSql = ''; + $identifierColumn = $this->class->getIdentifierColumnNames(); + + // INNER JOIN parent tables + foreach ($this->class->parentClasses as $parentClassName) { + $conditions = []; + $parentClass = $this->em->getClassMetadata($parentClassName); + $tableAlias = $this->getSQLTableAlias($parentClassName); + $joinSql .= ' INNER JOIN ' . $this->quoteStrategy->getTableName($parentClass, $this->platform) . ' ' . $tableAlias . ' ON '; + + foreach ($identifierColumn as $idColumn) { + $conditions[] = $baseTableAlias . '.' . $idColumn . ' = ' . $tableAlias . '.' . $idColumn; + } + + $joinSql .= implode(' AND ', $conditions); + } + + // OUTER JOIN sub tables + foreach ($this->class->subClasses as $subClassName) { + $conditions = []; + $subClass = $this->em->getClassMetadata($subClassName); + $tableAlias = $this->getSQLTableAlias($subClassName); + $joinSql .= ' LEFT JOIN ' . $this->quoteStrategy->getTableName($subClass, $this->platform) . ' ' . $tableAlias . ' ON '; + + foreach ($identifierColumn as $idColumn) { + $conditions[] = $baseTableAlias . '.' . $idColumn . ' = ' . $tableAlias . '.' . $idColumn; + } + + $joinSql .= implode(' AND ', $conditions); + } + + return $joinSql; + } +} 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 @@ +class->getTableName(); + } + + protected function getSelectColumnsSQL(): string + { + $columnList = []; + if ($this->currentPersisterContext->selectColumnListSql !== null) { + return $this->currentPersisterContext->selectColumnListSql; + } + + $columnList[] = parent::getSelectColumnsSQL(); + + $rootClass = $this->em->getClassMetadata($this->class->rootEntityName); + $tableAlias = $this->getSQLTableAlias($rootClass->name); + + // Append discriminator column + $discrColumn = $this->class->getDiscriminatorColumn(); + $discrColumnName = $discrColumn->name; + $discrColumnType = $discrColumn->type; + + $columnList[] = $tableAlias . '.' . $discrColumnName; + + $resultColumnName = $this->getSQLResultCasing($this->platform, $discrColumnName); + + $this->currentPersisterContext->rsm->setDiscriminatorColumn('r', $resultColumnName); + $this->currentPersisterContext->rsm->addMetaResult('r', $resultColumnName, $discrColumnName, false, $discrColumnType); + + // Append subclass columns + foreach ($this->class->subClasses as $subClassName) { + $subClass = $this->em->getClassMetadata($subClassName); + + // Regular columns + foreach ($subClass->fieldMappings as $fieldName => $mapping) { + if (isset($mapping->inherited)) { + continue; + } + + $columnList[] = $this->getSelectColumnSQL($fieldName, $subClass); + } + + // Foreign key columns + foreach ($subClass->associationMappings as $assoc) { + if (! $assoc->isToOneOwningSide() || isset($assoc->inherited)) { + continue; + } + + $targetClass = $this->em->getClassMetadata($assoc->targetEntity); + + foreach ($assoc->joinColumns as $joinColumn) { + $columnList[] = $this->getSelectJoinColumnSQL( + $tableAlias, + $joinColumn->name, + $this->quoteStrategy->getJoinColumnName($joinColumn, $subClass, $this->platform), + PersisterHelper::getTypeOfColumn($joinColumn->referencedColumnName, $targetClass, $this->em), + ); + } + } + } + + $this->currentPersisterContext->selectColumnListSql = implode(', ', $columnList); + + return $this->currentPersisterContext->selectColumnListSql; + } + + /** + * {@inheritDoc} + */ + protected function getInsertColumnList(): array + { + $columns = parent::getInsertColumnList(); + + // Add discriminator column to the INSERT SQL + $columns[] = $this->class->getDiscriminatorColumn()->name; + + return $columns; + } + + protected function getSQLTableAlias(string $className, string $assocName = ''): string + { + return parent::getSQLTableAlias($this->class->rootEntityName, $assocName); + } + + /** + * {@inheritDoc} + */ + protected function getSelectConditionSQL(array $criteria, AssociationMapping|null $assoc = null): string + { + $conditionSql = parent::getSelectConditionSQL($criteria, $assoc); + + if ($conditionSql) { + $conditionSql .= ' AND '; + } + + return $conditionSql . $this->getSelectConditionDiscriminatorValueSQL(); + } + + protected function getSelectConditionCriteriaSQL(Criteria $criteria): string + { + $conditionSql = parent::getSelectConditionCriteriaSQL($criteria); + + if ($conditionSql) { + $conditionSql .= ' AND '; + } + + return $conditionSql . $this->getSelectConditionDiscriminatorValueSQL(); + } + + protected function getSelectConditionDiscriminatorValueSQL(): string + { + $values = array_map($this->conn->quote(...), array_map( + strval(...), + array_flip(array_intersect($this->class->discriminatorMap, $this->class->subClasses)), + )); + + if ($this->class->discriminatorValue !== null) { // discriminators can be 0 + array_unshift($values, $this->conn->quote((string) $this->class->discriminatorValue)); + } + + $discColumnName = $this->class->getDiscriminatorColumn()->name; + + $values = implode(', ', $values); + $tableAlias = $this->getSQLTableAlias($this->class->name); + + return $tableAlias . '.' . $discColumnName . ' IN (' . $values . ')'; + } + + protected function generateFilterConditionSQL(ClassMetadata $targetEntity, string $targetTableAlias): string + { + // Ensure that the filters are applied to the root entity of the inheritance tree + $targetEntity = $this->em->getClassMetadata($targetEntity->rootEntityName); + // we don't care about the $targetTableAlias, in a STI there is only one table. + + return parent::generateFilterConditionSQL($targetEntity, $targetTableAlias); + } +} 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 @@ +getField(); + $value = $comparison->getValue()->getValue(); // shortcut for walkValue() + + if ( + isset($this->classMetadata->associationMappings[$field]) && + $value !== null && + ! is_object($value) && + ! in_array($comparison->getOperator(), [Comparison::IN, Comparison::NIN], true) + ) { + throw MatchingAssociationFieldRequiresObject::fromClassAndAssociation( + $this->classMetadata->name, + $field, + ); + } + + return $this->persister->getSelectConditionStatementSQL($field, $value, null, $comparison->getOperator()); + } + + /** + * Converts a composite expression into the target query language output. + * + * @throws RuntimeException + */ + public function walkCompositeExpression(CompositeExpression $expr): string + { + $expressionList = []; + + foreach ($expr->getExpressionList() as $child) { + $expressionList[] = $this->dispatch($child); + } + + return match ($expr->getType()) { + CompositeExpression::TYPE_AND => '(' . implode(' AND ', $expressionList) . ')', + CompositeExpression::TYPE_OR => '(' . implode(' OR ', $expressionList) . ')', + CompositeExpression::TYPE_NOT => 'NOT (' . $expressionList[0] . ')', + default => throw new RuntimeException('Unknown composite ' . $expr->getType()), + }; + } + + /** + * Converts a value expression into the target query language part. + */ + public function walkValue(Value $value): string + { + return '?'; + } +} 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 @@ +getValueFromComparison($comparison); + + $this->values[] = $value; + $this->types[] = [$comparison->getField(), $value, $comparison->getOperator()]; + + return null; + } + + /** + * Converts a composite expression into the target query language output. + * + * {@inheritDoc} + */ + public function walkCompositeExpression(CompositeExpression $expr) + { + foreach ($expr->getExpressionList() as $child) { + $this->dispatch($child); + } + + return null; + } + + /** + * Converts a value expression into the target query language part. + * + * {@inheritDoc} + */ + public function walkValue(Value $value) + { + return null; + } + + /** + * Returns the Parameters and Types necessary for matching the last visited expression. + * + * @return mixed[][] + * @psalm-return array{0: array, 1: array>} + */ + public function getParamsAndTypes(): array + { + return [$this->values, $this->types]; + } + + /** + * Returns the value from a Comparison. In case of a CONTAINS comparison, + * the value is wrapped in %-signs, because it will be used in a LIKE clause. + */ + protected function getValueFromComparison(Comparison $comparison): mixed + { + $value = $comparison->getValue()->getValue(); + + return match ($comparison->getOperator()) { + Comparison::CONTAINS => '%' . $value . '%', + Comparison::STARTS_WITH => $value . '%', + Comparison::ENDS_WITH => '%' . $value, + default => $value, + }; + } +} 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 @@ +resolveClassName($object::class); + } +} 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 @@ + + */ +interface InternalProxy extends Proxy +{ + public function __setInitialized(bool $initialized): void; +} 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 @@ +; + +/** + * DO NOT EDIT THIS FILE - IT WAS CREATED BY DOCTRINE'S PROXY GENERATOR + */ +class extends \ implements \ +{ + + + public function __isInitialized(): bool + { + return isset($this->lazyObjectState) && $this->isLazyObjectInitialized(); + } + + public function __serialize(): array + { + + } +} + +EOPHP; + + /** The UnitOfWork this factory uses to retrieve persisters */ + private readonly UnitOfWork $uow; + + /** @var self::AUTOGENERATE_* */ + private $autoGenerate; + + /** The IdentifierFlattener used for manipulating identifiers */ + private readonly IdentifierFlattener $identifierFlattener; + + /** @var array */ + private array $proxyFactories = []; + + /** + * Initializes a new instance of the ProxyFactory class that is + * connected to the given EntityManager. + * + * @param EntityManagerInterface $em The EntityManager the new factory works for. + * @param string $proxyDir The directory to use for the proxy classes. It must exist. + * @param string $proxyNs The namespace to use for the proxy classes. + * @param bool|self::AUTOGENERATE_* $autoGenerate The strategy for automatically generating proxy classes. + */ + public function __construct( + private readonly EntityManagerInterface $em, + private readonly string $proxyDir, + private readonly string $proxyNs, + bool|int $autoGenerate = self::AUTOGENERATE_NEVER, + ) { + if (! $proxyDir) { + throw ORMInvalidArgumentException::proxyDirectoryRequired(); + } + + if (! $proxyNs) { + throw ORMInvalidArgumentException::proxyNamespaceRequired(); + } + + if (is_int($autoGenerate) ? $autoGenerate < 0 || $autoGenerate > 4 : ! is_bool($autoGenerate)) { + throw ORMInvalidArgumentException::invalidAutoGenerateMode($autoGenerate); + } + + $this->uow = $em->getUnitOfWork(); + $this->autoGenerate = (int) $autoGenerate; + $this->identifierFlattener = new IdentifierFlattener($this->uow, $em->getMetadataFactory()); + } + + /** + * @param class-string $className + * @param array $identifier + */ + public function getProxy(string $className, array $identifier): InternalProxy + { + $proxyFactory = $this->proxyFactories[$className] ?? $this->getProxyFactory($className); + + return $proxyFactory($identifier); + } + + /** + * Generates proxy classes for all given classes. + * + * @param ClassMetadata[] $classes The classes (ClassMetadata instances) for which to generate proxies. + * @param string|null $proxyDir The target directory of the proxy classes. If not specified, the + * directory configured on the Configuration of the EntityManager used + * by this factory is used. + * + * @return int Number of generated proxies. + */ + public function generateProxyClasses(array $classes, string|null $proxyDir = null): int + { + $generated = 0; + + foreach ($classes as $class) { + if ($this->skipClass($class)) { + continue; + } + + $proxyFileName = $this->getProxyFileName($class->getName(), $proxyDir ?: $this->proxyDir); + $proxyClassName = self::generateProxyClassName($class->getName(), $this->proxyNs); + + $this->generateProxyClass($class, $proxyFileName, $proxyClassName); + + ++$generated; + } + + return $generated; + } + + protected function skipClass(ClassMetadata $metadata): bool + { + return $metadata->isMappedSuperclass + || $metadata->isEmbeddedClass + || $metadata->getReflectionClass()->isAbstract(); + } + + /** + * Creates a closure capable of initializing a proxy + * + * @return Closure(InternalProxy, array):void + * + * @throws EntityNotFoundException + */ + private function createLazyInitializer(ClassMetadata $classMetadata, EntityPersister $entityPersister, IdentifierFlattener $identifierFlattener): Closure + { + return static function (InternalProxy $proxy, array $identifier) use ($entityPersister, $classMetadata, $identifierFlattener): void { + $original = $entityPersister->loadById($identifier); + + if ($original === null) { + throw EntityNotFoundException::fromClassNameAndIdentifier( + $classMetadata->getName(), + $identifierFlattener->flattenIdentifier($classMetadata, $identifier), + ); + } + + if ($proxy === $original) { + return; + } + + $class = $entityPersister->getClassMetadata(); + + foreach ($class->getReflectionProperties() as $property) { + if (! $property || isset($identifier[$property->getName()]) || ! $class->hasField($property->getName()) && ! $class->hasAssociation($property->getName())) { + continue; + } + + $property->setValue($proxy, $property->getValue($original)); + } + }; + } + + private function getProxyFileName(string $className, string $baseDirectory): string + { + $baseDirectory = $baseDirectory ?: $this->proxyDir; + + return rtrim($baseDirectory, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . InternalProxy::MARKER + . str_replace('\\', '', $className) . '.php'; + } + + private function getProxyFactory(string $className): Closure + { + $skippedProperties = []; + $class = $this->em->getClassMetadata($className); + $identifiers = array_flip($class->getIdentifierFieldNames()); + $filter = ReflectionProperty::IS_PUBLIC | ReflectionProperty::IS_PROTECTED | ReflectionProperty::IS_PRIVATE; + $reflector = $class->getReflectionClass(); + + while ($reflector) { + foreach ($reflector->getProperties($filter) as $property) { + $name = $property->name; + + if ($property->isStatic() || (($class->hasField($name) || $class->hasAssociation($name)) && ! isset($identifiers[$name]))) { + continue; + } + + $prefix = $property->isPrivate() ? "\0" . $property->class . "\0" : ($property->isProtected() ? "\0*\0" : ''); + + $skippedProperties[$prefix . $name] = true; + } + + $filter = ReflectionProperty::IS_PRIVATE; + $reflector = $reflector->getParentClass(); + } + + $className = $class->getName(); // aliases and case sensitivity + $entityPersister = $this->uow->getEntityPersister($className); + $initializer = $this->createLazyInitializer($class, $entityPersister, $this->identifierFlattener); + $proxyClassName = $this->loadProxyClass($class); + $identifierFields = array_intersect_key($class->getReflectionProperties(), $identifiers); + + $proxyFactory = Closure::bind(static function (array $identifier) use ($initializer, $skippedProperties, $identifierFields, $className): InternalProxy { + $proxy = self::createLazyGhost(static function (InternalProxy $object) use ($initializer, $identifier): void { + $initializer($object, $identifier); + }, $skippedProperties); + + foreach ($identifierFields as $idField => $reflector) { + if (! isset($identifier[$idField])) { + throw ORMInvalidArgumentException::missingPrimaryKeyValue($className, $idField); + } + + assert($reflector !== null); + $reflector->setValue($proxy, $identifier[$idField]); + } + + return $proxy; + }, null, $proxyClassName); + + return $this->proxyFactories[$className] = $proxyFactory; + } + + private function loadProxyClass(ClassMetadata $class): string + { + $proxyClassName = self::generateProxyClassName($class->getName(), $this->proxyNs); + + if (class_exists($proxyClassName, false)) { + return $proxyClassName; + } + + if ($this->autoGenerate === self::AUTOGENERATE_EVAL) { + $this->generateProxyClass($class, null, $proxyClassName); + + return $proxyClassName; + } + + $fileName = $this->getProxyFileName($class->getName(), $this->proxyDir); + + switch ($this->autoGenerate) { + case self::AUTOGENERATE_FILE_NOT_EXISTS_OR_CHANGED: + if (file_exists($fileName) && filemtime($fileName) >= filemtime($class->getReflectionClass()->getFileName())) { + break; + } + // no break + case self::AUTOGENERATE_FILE_NOT_EXISTS: + if (file_exists($fileName)) { + break; + } + // no break + case self::AUTOGENERATE_ALWAYS: + $this->generateProxyClass($class, $fileName, $proxyClassName); + break; + } + + require $fileName; + + return $proxyClassName; + } + + private function generateProxyClass(ClassMetadata $class, string|null $fileName, string $proxyClassName): void + { + $i = strrpos($proxyClassName, '\\'); + $placeholders = [ + '' => $class->getName(), + '' => substr($proxyClassName, 0, $i), + '' => substr($proxyClassName, 1 + $i), + '' => InternalProxy::class, + ]; + + preg_match_all('(<([a-zA-Z]+)>)', self::PROXY_CLASS_TEMPLATE, $placeholderMatches); + + foreach (array_combine($placeholderMatches[0], $placeholderMatches[1]) as $placeholder => $name) { + $placeholders[$placeholder] ?? $placeholders[$placeholder] = $this->{'generate' . ucfirst($name)}($class); + } + + $proxyCode = strtr(self::PROXY_CLASS_TEMPLATE, $placeholders); + + if (! $fileName) { + if (! class_exists($proxyClassName)) { + eval(substr($proxyCode, 5)); + } + + return; + } + + $parentDirectory = dirname($fileName); + + if (! is_dir($parentDirectory) && ! @mkdir($parentDirectory, 0775, true)) { + throw ORMInvalidArgumentException::proxyDirectoryNotWritable($this->proxyDir); + } + + if (! is_writable($parentDirectory)) { + throw ORMInvalidArgumentException::proxyDirectoryNotWritable($this->proxyDir); + } + + $tmpFileName = $fileName . '.' . bin2hex(random_bytes(12)); + + file_put_contents($tmpFileName, $proxyCode); + @chmod($tmpFileName, 0664); + rename($tmpFileName, $fileName); + } + + private function generateUseLazyGhostTrait(ClassMetadata $class): string + { + $code = ProxyHelper::generateLazyGhost($class->getReflectionClass()); + $code = substr($code, 7 + (int) strpos($code, "\n{")); + $code = substr($code, 0, (int) strpos($code, "\n}")); + $code = str_replace('LazyGhostTrait;', str_replace("\n ", "\n", 'LazyGhostTrait { + initializeLazyObject as private; + setLazyObjectAsInitialized as public __setInitialized; + isLazyObjectInitialized as private; + createLazyGhost as private; + resetLazyObject as private; + } + + public function __load(): void + { + $this->initializeLazyObject(); + } + '), $code); + + return $code; + } + + private function generateSerializeImpl(ClassMetadata $class): string + { + $reflector = $class->getReflectionClass(); + $properties = $reflector->hasMethod('__serialize') ? 'parent::__serialize()' : '(array) $this'; + + $code = '$properties = ' . $properties . '; + unset($properties["\0" . self::class . "\0lazyObjectState"]); + + '; + + if ($reflector->hasMethod('__serialize') || ! $reflector->hasMethod('__sleep')) { + return $code . 'return $properties;'; + } + + return $code . '$data = []; + + foreach (parent::__sleep() as $name) { + $value = $properties[$k = $name] ?? $properties[$k = "\0*\0$name"] ?? $properties[$k = "\0' . $reflector->name . '\0$name"] ?? $k = null; + + if (null === $k) { + trigger_error(sprintf(\'serialize(): "%s" returned as member variable from __sleep() but does not exist\', $name), \E_USER_NOTICE); + } else { + $data[$k] = $value; + } + } + + return $data;'; + } + + private static function generateProxyClassName(string $className, string $proxyNamespace): string + { + return rtrim($proxyNamespace, '\\') . '\\' . Proxy::MARKER . '\\' . ltrim($className, '\\'); + } +} 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 @@ + + */ + private array $parsedTypes = []; + + /** + * Cached DQL query. + */ + private string|null $dql = null; + + /** + * The parser result that holds DQL => SQL information. + */ + private ParserResult $parserResult; + + /** + * The first result to return (the "offset"). + */ + private int $firstResult = 0; + + /** + * The maximum number of results to return (the "limit"). + */ + private int|null $maxResults = null; + + /** + * The cache driver used for caching queries. + */ + private CacheItemPoolInterface|null $queryCache = null; + + /** + * Whether or not expire the query cache. + */ + private bool $expireQueryCache = false; + + /** + * The query cache lifetime. + */ + private int|null $queryCacheTTL = null; + + /** + * Whether to use a query cache, if available. Defaults to TRUE. + */ + private bool $useQueryCache = true; + + /** + * Gets the SQL query/queries that correspond to this DQL query. + * + * @return list|string The built sql query or an array of all sql queries. + */ + public function getSQL(): string|array + { + return $this->parse()->getSqlExecutor()->getSqlStatements(); + } + + /** + * Returns the corresponding AST for this DQL query. + */ + public function getAST(): SelectStatement|UpdateStatement|DeleteStatement + { + $parser = new Parser($this); + + return $parser->getAST(); + } + + protected function getResultSetMapping(): ResultSetMapping + { + // parse query or load from cache + if ($this->resultSetMapping === null) { + $this->resultSetMapping = $this->parse()->getResultSetMapping(); + } + + return $this->resultSetMapping; + } + + /** + * Parses the DQL query, if necessary, and stores the parser result. + * + * Note: Populates $this->_parserResult as a side-effect. + */ + private function parse(): ParserResult + { + $types = []; + + foreach ($this->parameters as $parameter) { + /** @var Query\Parameter $parameter */ + $types[$parameter->getName()] = $parameter->getType(); + } + + // Return previous parser result if the query and the filter collection are both clean + if ($this->state === self::STATE_CLEAN && $this->parsedTypes === $types && $this->em->isFiltersStateClean()) { + return $this->parserResult; + } + + $this->state = self::STATE_CLEAN; + $this->parsedTypes = $types; + + $queryCache = $this->queryCache ?? $this->em->getConfiguration()->getQueryCache(); + // Check query cache. + if (! ($this->useQueryCache && $queryCache)) { + $parser = new Parser($this); + + $this->parserResult = $parser->parse(); + + return $this->parserResult; + } + + $cacheItem = $queryCache->getItem($this->getQueryCacheId()); + + if (! $this->expireQueryCache && $cacheItem->isHit()) { + $cached = $cacheItem->get(); + if ($cached instanceof ParserResult) { + // Cache hit. + $this->parserResult = $cached; + + return $this->parserResult; + } + } + + // Cache miss. + $parser = new Parser($this); + + $this->parserResult = $parser->parse(); + + $queryCache->save($cacheItem->set($this->parserResult)->expiresAfter($this->queryCacheTTL)); + + return $this->parserResult; + } + + protected function _doExecute(): Result|int + { + $executor = $this->parse()->getSqlExecutor(); + + if ($this->queryCacheProfile) { + $executor->setQueryCacheProfile($this->queryCacheProfile); + } else { + $executor->removeQueryCacheProfile(); + } + + if ($this->resultSetMapping === null) { + $this->resultSetMapping = $this->parserResult->getResultSetMapping(); + } + + // Prepare parameters + $paramMappings = $this->parserResult->getParameterMappings(); + $paramCount = count($this->parameters); + $mappingCount = count($paramMappings); + + if ($paramCount > $mappingCount) { + throw QueryException::tooManyParameters($mappingCount, $paramCount); + } + + if ($paramCount < $mappingCount) { + throw QueryException::tooFewParameters($mappingCount, $paramCount); + } + + // evict all cache for the entity region + if ($this->hasCache && isset($this->hints[self::HINT_CACHE_EVICT]) && $this->hints[self::HINT_CACHE_EVICT]) { + $this->evictEntityCacheRegion(); + } + + [$sqlParams, $types] = $this->processParameterMappings($paramMappings); + + $this->evictResultSetCache( + $executor, + $sqlParams, + $types, + $this->em->getConnection()->getParams(), + ); + + return $executor->execute($this->em->getConnection(), $sqlParams, $types); + } + + /** + * @param array $sqlParams + * @param array $types + * @param array $connectionParams + */ + private function evictResultSetCache( + AbstractSqlExecutor $executor, + array $sqlParams, + array $types, + array $connectionParams, + ): void { + if ($this->queryCacheProfile === null || ! $this->getExpireResultCache()) { + return; + } + + $cache = $this->queryCacheProfile->getResultCache(); + + assert($cache !== null); + + $statements = (array) $executor->getSqlStatements(); // Type casted since it can either be a string or an array + + foreach ($statements as $statement) { + $cacheKeys = $this->queryCacheProfile->generateCacheKeys($statement, $sqlParams, $types, $connectionParams); + $cache->deleteItem(reset($cacheKeys)); + } + } + + /** + * Evict entity cache region + */ + private function evictEntityCacheRegion(): void + { + $AST = $this->getAST(); + + if ($AST instanceof SelectStatement) { + throw new QueryException('The hint "HINT_CACHE_EVICT" is not valid for select statements.'); + } + + $className = $AST instanceof DeleteStatement + ? $AST->deleteClause->abstractSchemaName + : $AST->updateClause->abstractSchemaName; + + $this->em->getCache()->evictEntityRegion($className); + } + + /** + * Processes query parameter mappings. + * + * @param array> $paramMappings + * + * @return mixed[][] + * @psalm-return array{0: list, 1: array} + * + * @throws Query\QueryException + */ + private function processParameterMappings(array $paramMappings): array + { + $sqlParams = []; + $types = []; + + foreach ($this->parameters as $parameter) { + $key = $parameter->getName(); + + if (! isset($paramMappings[$key])) { + throw QueryException::unknownParameter($key); + } + + [$value, $type] = $this->resolveParameterValue($parameter); + + foreach ($paramMappings[$key] as $position) { + $types[$position] = $type; + } + + $sqlPositions = $paramMappings[$key]; + + // optimized multi value sql positions away for now, + // they are not allowed in DQL anyways. + $value = [$value]; + $countValue = count($value); + + for ($i = 0, $l = count($sqlPositions); $i < $l; $i++) { + $sqlParams[$sqlPositions[$i]] = $value[$i % $countValue]; + } + } + + if (count($sqlParams) !== count($types)) { + throw QueryException::parameterTypeMismatch(); + } + + if ($sqlParams) { + ksort($sqlParams); + $sqlParams = array_values($sqlParams); + + ksort($types); + $types = array_values($types); + } + + return [$sqlParams, $types]; + } + + /** + * @return mixed[] tuple of (value, type) + * @psalm-return array{0: mixed, 1: mixed} + */ + private function resolveParameterValue(Parameter $parameter): array + { + if ($parameter->typeWasSpecified()) { + return [$parameter->getValue(), $parameter->getType()]; + } + + $key = $parameter->getName(); + $originalValue = $parameter->getValue(); + $value = $originalValue; + $rsm = $this->getResultSetMapping(); + + if ($value instanceof ClassMetadata && isset($rsm->metadataParameterMapping[$key])) { + $value = $value->getMetadataValue($rsm->metadataParameterMapping[$key]); + } + + if ($value instanceof ClassMetadata && isset($rsm->discriminatorParameters[$key])) { + $value = array_keys(HierarchyDiscriminatorResolver::resolveDiscriminatorsForClass($value, $this->em)); + } + + $processedValue = $this->processParameterValue($value); + + return [ + $processedValue, + $originalValue === $processedValue + ? $parameter->getType() + : ParameterTypeInferer::inferType($processedValue), + ]; + } + + /** + * Defines a cache driver to be used for caching queries. + * + * @return $this + */ + public function setQueryCache(CacheItemPoolInterface|null $queryCache): self + { + $this->queryCache = $queryCache; + + return $this; + } + + /** + * Defines whether the query should make use of a query cache, if available. + * + * @return $this + */ + public function useQueryCache(bool $bool): self + { + $this->useQueryCache = $bool; + + return $this; + } + + /** + * Defines how long the query cache will be active before expire. + * + * @param int|null $timeToLive How long the cache entry is valid. + * + * @return $this + */ + public function setQueryCacheLifetime(int|null $timeToLive): self + { + $this->queryCacheTTL = $timeToLive; + + return $this; + } + + /** + * Retrieves the lifetime of resultset cache. + */ + public function getQueryCacheLifetime(): int|null + { + return $this->queryCacheTTL; + } + + /** + * Defines if the query cache is active or not. + * + * @return $this + */ + public function expireQueryCache(bool $expire = true): self + { + $this->expireQueryCache = $expire; + + return $this; + } + + /** + * Retrieves if the query cache is active or not. + */ + public function getExpireQueryCache(): bool + { + return $this->expireQueryCache; + } + + public function free(): void + { + parent::free(); + + $this->dql = null; + $this->state = self::STATE_CLEAN; + } + + /** + * Sets a DQL query string. + */ + public function setDQL(string $dqlQuery): self + { + $this->dql = $dqlQuery; + $this->state = self::STATE_DIRTY; + + return $this; + } + + /** + * Returns the DQL query that is represented by this query object. + */ + public function getDQL(): string|null + { + return $this->dql; + } + + /** + * Returns the state of this query object + * By default the type is Doctrine_ORM_Query_Abstract::STATE_CLEAN but if it appears any unprocessed DQL + * part, it is switched to Doctrine_ORM_Query_Abstract::STATE_DIRTY. + * + * @see AbstractQuery::STATE_CLEAN + * @see AbstractQuery::STATE_DIRTY + * + * @return int The query state. + * @psalm-return self::STATE_* The query state. + */ + public function getState(): int + { + return $this->state; + } + + /** + * Method to check if an arbitrary piece of DQL exists + * + * @param string $dql Arbitrary piece of DQL to check for. + */ + public function contains(string $dql): bool + { + return stripos($this->getDQL(), $dql) !== false; + } + + /** + * Sets the position of the first result to retrieve (the "offset"). + * + * @param int $firstResult The first result to return. + * + * @return $this + */ + public function setFirstResult(int $firstResult): self + { + $this->firstResult = $firstResult; + $this->state = self::STATE_DIRTY; + + return $this; + } + + /** + * Gets the position of the first result the query object was set to retrieve (the "offset"). + * Returns 0 if {@link setFirstResult} was not applied to this query. + * + * @return int The position of the first result. + */ + public function getFirstResult(): int + { + return $this->firstResult; + } + + /** + * Sets the maximum number of results to retrieve (the "limit"). + * + * @return $this + */ + public function setMaxResults(int|null $maxResults): self + { + $this->maxResults = $maxResults; + $this->state = self::STATE_DIRTY; + + return $this; + } + + /** + * Gets the maximum number of results the query object was set to retrieve (the "limit"). + * Returns NULL if {@link setMaxResults} was not applied to this query. + * + * @return int|null Maximum number of results. + */ + public function getMaxResults(): int|null + { + return $this->maxResults; + } + + /** {@inheritDoc} */ + public function toIterable(iterable $parameters = [], $hydrationMode = self::HYDRATE_OBJECT): iterable + { + $this->setHint(self::HINT_INTERNAL_ITERATION, true); + + return parent::toIterable($parameters, $hydrationMode); + } + + public function setHint(string $name, mixed $value): static + { + $this->state = self::STATE_DIRTY; + + return parent::setHint($name, $value); + } + + public function setHydrationMode(string|int $hydrationMode): static + { + $this->state = self::STATE_DIRTY; + + return parent::setHydrationMode($hydrationMode); + } + + /** + * Set the lock mode for this Query. + * + * @see \Doctrine\DBAL\LockMode + * + * @psalm-param LockMode::* $lockMode + * + * @return $this + * + * @throws TransactionRequiredException + */ + public function setLockMode(LockMode|int $lockMode): self + { + if (in_array($lockMode, [LockMode::NONE, LockMode::PESSIMISTIC_READ, LockMode::PESSIMISTIC_WRITE], true)) { + if (! $this->em->getConnection()->isTransactionActive()) { + throw TransactionRequiredException::transactionRequired(); + } + } + + $this->setHint(self::HINT_LOCK_MODE, $lockMode); + + return $this; + } + + /** + * Get the current lock mode for this query. + * + * @return LockMode|int|null The current lock mode of this query or NULL if no specific lock mode is set. + * @psalm-return LockMode::*|null + */ + public function getLockMode(): LockMode|int|null + { + $lockMode = $this->getHint(self::HINT_LOCK_MODE); + + if ($lockMode === false) { + return null; + } + + return $lockMode; + } + + /** + * Generate a cache id for the query cache - reusing the Result-Cache-Id generator. + */ + protected function getQueryCacheId(): string + { + ksort($this->hints); + + return md5( + $this->getDQL() . serialize($this->hints) . + '&platform=' . get_debug_type($this->getEntityManager()->getConnection()->getDatabasePlatform()) . + ($this->em->hasFilters() ? $this->em->getFilters()->getHash() : '') . + '&firstResult=' . $this->firstResult . '&maxResult=' . $this->maxResults . + '&hydrationMode=' . $this->hydrationMode . '&types=' . serialize($this->parsedTypes) . 'DOCTRINE_QUERY_CACHE_SALT', + ); + } + + protected function getHash(): string + { + return sha1(parent::getHash() . '-' . $this->firstResult . '-' . $this->maxResults); + } + + /** + * Cleanup Query resource when clone is called. + */ + public function __clone() + { + parent::__clone(); + + $this->state = self::STATE_DIRTY; + } +} 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 @@ +walkAggregateExpression($this); + } +} 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 @@ +simpleArithmeticExpression; + } + + public function isSubselect(): bool + { + return (bool) $this->subselect; + } + + public function dispatch(SqlWalker $walker): string + { + return $walker->walkArithmeticExpression($this); + } +} 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 @@ +sign === true; + } + + public function isNegativeSigned(): bool + { + return $this->sign === false; + } + + public function dispatch(SqlWalker $walker): string + { + return $walker->walkArithmeticFactor($this); + } +} 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 @@ +walkArithmeticTerm($this); + } +} 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 @@ +walkBetweenExpression($this); + } +} 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 @@ +walkCoalesceExpression($this); + } +} 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 @@ +walkCollectionMemberExpression($this); + } +} 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 @@ +" | "!=") (BooleanExpression | QuantifiedExpression) | + * EnumExpression ("=" | "<>" | "!=") (EnumExpression | QuantifiedExpression) | + * DatetimeExpression ComparisonOperator (DatetimeExpression | QuantifiedExpression) | + * EntityExpression ("=" | "<>") (EntityExpression | QuantifiedExpression) + * + * @link www.doctrine-project.org + */ +class ComparisonExpression extends Node +{ + public function __construct( + public Node|string $leftExpression, + public string $operator, + public Node|string $rightExpression, + ) { + } + + public function dispatch(SqlWalker $walker): string + { + return $walker->walkComparisonExpression($this); + } +} 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 @@ +walkConditionalExpression($this); + } +} 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 @@ +walkConditionalFactor($this); + } +} 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 @@ +simpleConditionalExpression; + } + + public function isConditionalExpression(): bool + { + return (bool) $this->conditionalExpression; + } + + public function dispatch(SqlWalker $walker): string + { + return $walker->walkConditionalPrimary($this); + } +} 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 @@ +walkConditionalTerm($this); + } +} 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 @@ +walkDeleteClause($this); + } +} 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 @@ +walkDeleteStatement($this); + } +} 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 @@ +walkEmptyCollectionComparisonExpression($this); + } +} 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 @@ +walkExistsExpression($this); + } +} 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 @@ +walkFromClause($this); + } +} 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 @@ +walkSimpleArithmeticExpression( + $this->simpleArithmeticExpression, + ) . ')'; + } + + public function parse(Parser $parser): void + { + $parser->match(TokenType::T_IDENTIFIER); + $parser->match(TokenType::T_OPEN_PARENTHESIS); + + $this->simpleArithmeticExpression = $parser->SimpleArithmeticExpression(); + + $parser->match(TokenType::T_CLOSE_PARENTHESIS); + } +} 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 @@ +aggregateExpression->dispatch($sqlWalker); + } + + public function parse(Parser $parser): void + { + $this->aggregateExpression = $parser->AggregateExpression(); + } +} 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 @@ +getConnection()->getDatabasePlatform(); + + return $platform->getBitAndComparisonExpression( + $this->firstArithmetic->dispatch($sqlWalker), + $this->secondArithmetic->dispatch($sqlWalker), + ); + } + + public function parse(Parser $parser): void + { + $parser->match(TokenType::T_IDENTIFIER); + $parser->match(TokenType::T_OPEN_PARENTHESIS); + + $this->firstArithmetic = $parser->ArithmeticPrimary(); + $parser->match(TokenType::T_COMMA); + $this->secondArithmetic = $parser->ArithmeticPrimary(); + + $parser->match(TokenType::T_CLOSE_PARENTHESIS); + } +} 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 @@ +getConnection()->getDatabasePlatform(); + + return $platform->getBitOrComparisonExpression( + $this->firstArithmetic->dispatch($sqlWalker), + $this->secondArithmetic->dispatch($sqlWalker), + ); + } + + public function parse(Parser $parser): void + { + $parser->match(TokenType::T_IDENTIFIER); + $parser->match(TokenType::T_OPEN_PARENTHESIS); + + $this->firstArithmetic = $parser->ArithmeticPrimary(); + $parser->match(TokenType::T_COMMA); + $this->secondArithmetic = $parser->ArithmeticPrimary(); + + $parser->match(TokenType::T_CLOSE_PARENTHESIS); + } +} 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 @@ + */ + public array $concatExpressions = []; + + public function getSql(SqlWalker $sqlWalker): string + { + $platform = $sqlWalker->getConnection()->getDatabasePlatform(); + + $args = []; + + foreach ($this->concatExpressions as $expression) { + $args[] = $sqlWalker->walkStringPrimary($expression); + } + + return $platform->getConcatExpression(...$args); + } + + public function parse(Parser $parser): void + { + $parser->match(TokenType::T_IDENTIFIER); + $parser->match(TokenType::T_OPEN_PARENTHESIS); + + $this->firstStringPrimary = $parser->StringPrimary(); + $this->concatExpressions[] = $this->firstStringPrimary; + + $parser->match(TokenType::T_COMMA); + + $this->secondStringPrimary = $parser->StringPrimary(); + $this->concatExpressions[] = $this->secondStringPrimary; + + while ($parser->getLexer()->isNextToken(TokenType::T_COMMA)) { + $parser->match(TokenType::T_COMMA); + $this->concatExpressions[] = $parser->StringPrimary(); + } + + $parser->match(TokenType::T_CLOSE_PARENTHESIS); + } +} 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 @@ +aggregateExpression->dispatch($sqlWalker); + } + + public function parse(Parser $parser): void + { + $this->aggregateExpression = $parser->AggregateExpression(); + } + + public function getReturnType(): Type + { + return Type::getType(Types::INTEGER); + } +} 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 @@ +getConnection()->getDatabasePlatform()->getCurrentDateSQL(); + } + + public function parse(Parser $parser): void + { + $parser->match(TokenType::T_IDENTIFIER); + $parser->match(TokenType::T_OPEN_PARENTHESIS); + $parser->match(TokenType::T_CLOSE_PARENTHESIS); + } +} 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 @@ +getConnection()->getDatabasePlatform()->getCurrentTimeSQL(); + } + + public function parse(Parser $parser): void + { + $parser->match(TokenType::T_IDENTIFIER); + $parser->match(TokenType::T_OPEN_PARENTHESIS); + $parser->match(TokenType::T_CLOSE_PARENTHESIS); + } +} 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 @@ +getConnection()->getDatabasePlatform()->getCurrentTimestampSQL(); + } + + public function parse(Parser $parser): void + { + $parser->match(TokenType::T_IDENTIFIER); + $parser->match(TokenType::T_OPEN_PARENTHESIS); + $parser->match(TokenType::T_CLOSE_PARENTHESIS); + } +} 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 @@ +unit->value)) { + 'second' => $sqlWalker->getConnection()->getDatabasePlatform()->getDateAddSecondsExpression( + $this->firstDateExpression->dispatch($sqlWalker), + $this->dispatchIntervalExpression($sqlWalker), + ), + 'minute' => $sqlWalker->getConnection()->getDatabasePlatform()->getDateAddMinutesExpression( + $this->firstDateExpression->dispatch($sqlWalker), + $this->dispatchIntervalExpression($sqlWalker), + ), + 'hour' => $sqlWalker->getConnection()->getDatabasePlatform()->getDateAddHourExpression( + $this->firstDateExpression->dispatch($sqlWalker), + $this->dispatchIntervalExpression($sqlWalker), + ), + 'day' => $sqlWalker->getConnection()->getDatabasePlatform()->getDateAddDaysExpression( + $this->firstDateExpression->dispatch($sqlWalker), + $this->dispatchIntervalExpression($sqlWalker), + ), + 'week' => $sqlWalker->getConnection()->getDatabasePlatform()->getDateAddWeeksExpression( + $this->firstDateExpression->dispatch($sqlWalker), + $this->dispatchIntervalExpression($sqlWalker), + ), + 'month' => $sqlWalker->getConnection()->getDatabasePlatform()->getDateAddMonthExpression( + $this->firstDateExpression->dispatch($sqlWalker), + $this->dispatchIntervalExpression($sqlWalker), + ), + 'year' => $sqlWalker->getConnection()->getDatabasePlatform()->getDateAddYearsExpression( + $this->firstDateExpression->dispatch($sqlWalker), + $this->dispatchIntervalExpression($sqlWalker), + ), + default => throw QueryException::semanticalError( + 'DATE_ADD() only supports units of type second, minute, hour, day, week, month and year.', + ), + }; + } + + /** @throws ASTException */ + private function dispatchIntervalExpression(SqlWalker $sqlWalker): string + { + return $this->intervalExpression->dispatch($sqlWalker); + } + + public function parse(Parser $parser): void + { + $parser->match(TokenType::T_IDENTIFIER); + $parser->match(TokenType::T_OPEN_PARENTHESIS); + + $this->firstDateExpression = $parser->ArithmeticPrimary(); + $parser->match(TokenType::T_COMMA); + $this->intervalExpression = $parser->ArithmeticPrimary(); + $parser->match(TokenType::T_COMMA); + $this->unit = $parser->StringPrimary(); + + $parser->match(TokenType::T_CLOSE_PARENTHESIS); + } +} 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 @@ +getConnection()->getDatabasePlatform()->getDateDiffExpression( + $this->date1->dispatch($sqlWalker), + $this->date2->dispatch($sqlWalker), + ); + } + + public function parse(Parser $parser): void + { + $parser->match(TokenType::T_IDENTIFIER); + $parser->match(TokenType::T_OPEN_PARENTHESIS); + + $this->date1 = $parser->ArithmeticPrimary(); + $parser->match(TokenType::T_COMMA); + $this->date2 = $parser->ArithmeticPrimary(); + + $parser->match(TokenType::T_CLOSE_PARENTHESIS); + } +} 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 @@ +unit->value)) { + 'second' => $sqlWalker->getConnection()->getDatabasePlatform()->getDateSubSecondsExpression( + $this->firstDateExpression->dispatch($sqlWalker), + $this->dispatchIntervalExpression($sqlWalker), + ), + 'minute' => $sqlWalker->getConnection()->getDatabasePlatform()->getDateSubMinutesExpression( + $this->firstDateExpression->dispatch($sqlWalker), + $this->dispatchIntervalExpression($sqlWalker), + ), + 'hour' => $sqlWalker->getConnection()->getDatabasePlatform()->getDateSubHourExpression( + $this->firstDateExpression->dispatch($sqlWalker), + $this->dispatchIntervalExpression($sqlWalker), + ), + 'day' => $sqlWalker->getConnection()->getDatabasePlatform()->getDateSubDaysExpression( + $this->firstDateExpression->dispatch($sqlWalker), + $this->dispatchIntervalExpression($sqlWalker), + ), + 'week' => $sqlWalker->getConnection()->getDatabasePlatform()->getDateSubWeeksExpression( + $this->firstDateExpression->dispatch($sqlWalker), + $this->dispatchIntervalExpression($sqlWalker), + ), + 'month' => $sqlWalker->getConnection()->getDatabasePlatform()->getDateSubMonthExpression( + $this->firstDateExpression->dispatch($sqlWalker), + $this->dispatchIntervalExpression($sqlWalker), + ), + 'year' => $sqlWalker->getConnection()->getDatabasePlatform()->getDateSubYearsExpression( + $this->firstDateExpression->dispatch($sqlWalker), + $this->dispatchIntervalExpression($sqlWalker), + ), + default => throw QueryException::semanticalError( + 'DATE_SUB() only supports units of type second, minute, hour, day, week, month and year.', + ), + }; + } + + /** @throws ASTException */ + private function dispatchIntervalExpression(SqlWalker $sqlWalker): string + { + return $this->intervalExpression->dispatch($sqlWalker); + } +} 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 @@ +walkFunction($this); + } + + abstract public function parse(Parser $parser): void; +} 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 @@ +pathExpression->field !== null); + $entityManager = $sqlWalker->getEntityManager(); + $platform = $entityManager->getConnection()->getDatabasePlatform(); + $quoteStrategy = $entityManager->getConfiguration()->getQuoteStrategy(); + $dqlAlias = $this->pathExpression->identificationVariable; + $assocField = $this->pathExpression->field; + $assoc = $sqlWalker->getMetadataForDqlAlias($dqlAlias)->associationMappings[$assocField]; + $targetEntity = $entityManager->getClassMetadata($assoc->targetEntity); + + assert($assoc->isToOneOwningSide()); + $joinColumn = reset($assoc->joinColumns); + + if ($this->fieldMapping !== null) { + if (! isset($targetEntity->fieldMappings[$this->fieldMapping])) { + throw new QueryException(sprintf('Undefined reference field mapping "%s"', $this->fieldMapping)); + } + + $field = $targetEntity->fieldMappings[$this->fieldMapping]; + $joinColumn = null; + + foreach ($assoc->joinColumns as $mapping) { + if ($mapping->referencedColumnName === $field->columnName) { + $joinColumn = $mapping; + + break; + } + } + + if ($joinColumn === null) { + throw new QueryException(sprintf('Unable to resolve the reference field mapping "%s"', $this->fieldMapping)); + } + } + + // The table with the relation may be a subclass, so get the table name from the association definition + $tableName = $entityManager->getClassMetadata($assoc->sourceEntity)->getTableName(); + + $tableAlias = $sqlWalker->getSQLTableAlias($tableName, $dqlAlias); + $columnName = $quoteStrategy->getJoinColumnName($joinColumn, $targetEntity, $platform); + + return $tableAlias . '.' . $columnName; + } + + public function parse(Parser $parser): void + { + $parser->match(TokenType::T_IDENTIFIER); + $parser->match(TokenType::T_OPEN_PARENTHESIS); + + $this->pathExpression = $parser->SingleValuedAssociationPathExpression(); + + if ($parser->getLexer()->isNextToken(TokenType::T_COMMA)) { + $parser->match(TokenType::T_COMMA); + $parser->match(TokenType::T_STRING); + + $token = $parser->getLexer()->token; + assert($token !== null); + $this->fieldMapping = $token->value; + } + + $parser->match(TokenType::T_CLOSE_PARENTHESIS); + } +} 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 @@ +getConnection()->getDatabasePlatform()->getLengthExpression( + $sqlWalker->walkSimpleArithmeticExpression($this->stringPrimary), + ); + } + + public function parse(Parser $parser): void + { + $parser->match(TokenType::T_IDENTIFIER); + $parser->match(TokenType::T_OPEN_PARENTHESIS); + + $this->stringPrimary = $parser->StringPrimary(); + + $parser->match(TokenType::T_CLOSE_PARENTHESIS); + } + + public function getReturnType(): Type + { + return Type::getType(Types::INTEGER); + } +} 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 @@ +getConnection()->getDatabasePlatform(); + + $firstString = $sqlWalker->walkStringPrimary($this->firstStringPrimary); + $secondString = $sqlWalker->walkStringPrimary($this->secondStringPrimary); + + if ($this->simpleArithmeticExpression) { + return $platform->getLocateExpression( + $secondString, + $firstString, + $sqlWalker->walkSimpleArithmeticExpression($this->simpleArithmeticExpression), + ); + } + + return $platform->getLocateExpression($secondString, $firstString); + } + + public function parse(Parser $parser): void + { + $parser->match(TokenType::T_IDENTIFIER); + $parser->match(TokenType::T_OPEN_PARENTHESIS); + + $this->firstStringPrimary = $parser->StringPrimary(); + + $parser->match(TokenType::T_COMMA); + + $this->secondStringPrimary = $parser->StringPrimary(); + + $lexer = $parser->getLexer(); + if ($lexer->isNextToken(TokenType::T_COMMA)) { + $parser->match(TokenType::T_COMMA); + + $this->simpleArithmeticExpression = $parser->SimpleArithmeticExpression(); + } + + $parser->match(TokenType::T_CLOSE_PARENTHESIS); + } +} 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 @@ +walkSimpleArithmeticExpression($this->stringPrimary), + ); + } + + public function parse(Parser $parser): void + { + $parser->match(TokenType::T_IDENTIFIER); + $parser->match(TokenType::T_OPEN_PARENTHESIS); + + $this->stringPrimary = $parser->StringPrimary(); + + $parser->match(TokenType::T_CLOSE_PARENTHESIS); + } +} 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 @@ +aggregateExpression->dispatch($sqlWalker); + } + + public function parse(Parser $parser): void + { + $this->aggregateExpression = $parser->AggregateExpression(); + } +} 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 @@ +aggregateExpression->dispatch($sqlWalker); + } + + public function parse(Parser $parser): void + { + $this->aggregateExpression = $parser->AggregateExpression(); + } +} 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 @@ +getConnection()->getDatabasePlatform()->getModExpression( + $sqlWalker->walkSimpleArithmeticExpression($this->firstSimpleArithmeticExpression), + $sqlWalker->walkSimpleArithmeticExpression($this->secondSimpleArithmeticExpression), + ); + } + + public function parse(Parser $parser): void + { + $parser->match(TokenType::T_IDENTIFIER); + $parser->match(TokenType::T_OPEN_PARENTHESIS); + + $this->firstSimpleArithmeticExpression = $parser->SimpleArithmeticExpression(); + + $parser->match(TokenType::T_COMMA); + + $this->secondSimpleArithmeticExpression = $parser->SimpleArithmeticExpression(); + + $parser->match(TokenType::T_CLOSE_PARENTHESIS); + } +} 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 @@ +collectionPathExpression->field !== null); + $entityManager = $sqlWalker->getEntityManager(); + $platform = $entityManager->getConnection()->getDatabasePlatform(); + $quoteStrategy = $entityManager->getConfiguration()->getQuoteStrategy(); + $dqlAlias = $this->collectionPathExpression->identificationVariable; + $assocField = $this->collectionPathExpression->field; + + $class = $sqlWalker->getMetadataForDqlAlias($dqlAlias); + $assoc = $class->associationMappings[$assocField]; + $sql = 'SELECT COUNT(*) FROM '; + + if ($assoc->isOneToMany()) { + $targetClass = $entityManager->getClassMetadata($assoc->targetEntity); + $targetTableAlias = $sqlWalker->getSQLTableAlias($targetClass->getTableName()); + $sourceTableAlias = $sqlWalker->getSQLTableAlias($class->getTableName(), $dqlAlias); + + $sql .= $quoteStrategy->getTableName($targetClass, $platform) . ' ' . $targetTableAlias . ' WHERE '; + + $owningAssoc = $targetClass->associationMappings[$assoc->mappedBy]; + assert($owningAssoc->isManyToOne()); + + $first = true; + + foreach ($owningAssoc->targetToSourceKeyColumns as $targetColumn => $sourceColumn) { + if ($first) { + $first = false; + } else { + $sql .= ' AND '; + } + + $sql .= $targetTableAlias . '.' . $sourceColumn + . ' = ' + . $sourceTableAlias . '.' . $quoteStrategy->getColumnName($class->fieldNames[$targetColumn], $class, $platform); + } + } else { // many-to-many + assert($assoc->isManyToMany()); + $owningAssoc = $entityManager->getMetadataFactory()->getOwningSide($assoc); + $joinTable = $owningAssoc->joinTable; + + // SQL table aliases + $joinTableAlias = $sqlWalker->getSQLTableAlias($joinTable->name); + $sourceTableAlias = $sqlWalker->getSQLTableAlias($class->getTableName(), $dqlAlias); + + // join to target table + $targetClass = $entityManager->getClassMetadata($assoc->targetEntity); + $sql .= $quoteStrategy->getJoinTableName($owningAssoc, $targetClass, $platform) . ' ' . $joinTableAlias . ' WHERE '; + + $joinColumns = $assoc->isOwningSide() + ? $joinTable->joinColumns + : $joinTable->inverseJoinColumns; + + $first = true; + + foreach ($joinColumns as $joinColumn) { + if ($first) { + $first = false; + } else { + $sql .= ' AND '; + } + + $sourceColumnName = $quoteStrategy->getColumnName( + $class->fieldNames[$joinColumn->referencedColumnName], + $class, + $platform, + ); + + $sql .= $joinTableAlias . '.' . $joinColumn->name + . ' = ' + . $sourceTableAlias . '.' . $sourceColumnName; + } + } + + return '(' . $sql . ')'; + } + + public function parse(Parser $parser): void + { + $parser->match(TokenType::T_IDENTIFIER); + $parser->match(TokenType::T_OPEN_PARENTHESIS); + + $this->collectionPathExpression = $parser->CollectionValuedPathExpression(); + + $parser->match(TokenType::T_CLOSE_PARENTHESIS); + } +} 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 @@ +walkSimpleArithmeticExpression($this->simpleArithmeticExpression), + ); + } + + public function parse(Parser $parser): void + { + $parser->match(TokenType::T_IDENTIFIER); + $parser->match(TokenType::T_OPEN_PARENTHESIS); + + $this->simpleArithmeticExpression = $parser->SimpleArithmeticExpression(); + + $parser->match(TokenType::T_CLOSE_PARENTHESIS); + } +} 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 @@ +secondSimpleArithmeticExpression !== null) { + $optionalSecondSimpleArithmeticExpression = $sqlWalker->walkSimpleArithmeticExpression($this->secondSimpleArithmeticExpression); + } + + return $sqlWalker->getConnection()->getDatabasePlatform()->getSubstringExpression( + $sqlWalker->walkStringPrimary($this->stringPrimary), + $sqlWalker->walkSimpleArithmeticExpression($this->firstSimpleArithmeticExpression), + $optionalSecondSimpleArithmeticExpression, + ); + } + + public function parse(Parser $parser): void + { + $parser->match(TokenType::T_IDENTIFIER); + $parser->match(TokenType::T_OPEN_PARENTHESIS); + + $this->stringPrimary = $parser->StringPrimary(); + + $parser->match(TokenType::T_COMMA); + + $this->firstSimpleArithmeticExpression = $parser->SimpleArithmeticExpression(); + + $lexer = $parser->getLexer(); + if ($lexer->isNextToken(TokenType::T_COMMA)) { + $parser->match(TokenType::T_COMMA); + + $this->secondSimpleArithmeticExpression = $parser->SimpleArithmeticExpression(); + } + + $parser->match(TokenType::T_CLOSE_PARENTHESIS); + } +} 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 @@ +aggregateExpression->dispatch($sqlWalker); + } + + public function parse(Parser $parser): void + { + $this->aggregateExpression = $parser->AggregateExpression(); + } +} 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 @@ +walkStringPrimary($this->stringPrimary); + $platform = $sqlWalker->getConnection()->getDatabasePlatform(); + $trimMode = $this->getTrimMode(); + + if ($this->trimChar !== false) { + return $platform->getTrimExpression( + $stringPrimary, + $trimMode, + $platform->quoteStringLiteral($this->trimChar), + ); + } + + return $platform->getTrimExpression($stringPrimary, $trimMode); + } + + public function parse(Parser $parser): void + { + $lexer = $parser->getLexer(); + + $parser->match(TokenType::T_IDENTIFIER); + $parser->match(TokenType::T_OPEN_PARENTHESIS); + + $this->parseTrimMode($parser); + + if ($lexer->isNextToken(TokenType::T_STRING)) { + $parser->match(TokenType::T_STRING); + + assert($lexer->token !== null); + $this->trimChar = $lexer->token->value; + } + + if ($this->leading || $this->trailing || $this->both || ($this->trimChar !== false)) { + $parser->match(TokenType::T_FROM); + } + + $this->stringPrimary = $parser->StringPrimary(); + + $parser->match(TokenType::T_CLOSE_PARENTHESIS); + } + + /** @psalm-return TrimMode::* */ + private function getTrimMode(): TrimMode|int + { + if ($this->leading) { + return TrimMode::LEADING; + } + + if ($this->trailing) { + return TrimMode::TRAILING; + } + + if ($this->both) { + return TrimMode::BOTH; + } + + return TrimMode::UNSPECIFIED; + } + + private function parseTrimMode(Parser $parser): void + { + $lexer = $parser->getLexer(); + assert($lexer->lookahead !== null); + $value = $lexer->lookahead->value; + + if (strcasecmp('leading', $value) === 0) { + $parser->match(TokenType::T_LEADING); + + $this->leading = true; + + return; + } + + if (strcasecmp('trailing', $value) === 0) { + $parser->match(TokenType::T_TRAILING); + + $this->trailing = true; + + return; + } + + if (strcasecmp('both', $value) === 0) { + $parser->match(TokenType::T_BOTH); + + $this->both = true; + + return; + } + } +} 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 @@ +walkSimpleArithmeticExpression($this->stringPrimary), + ); + } + + public function parse(Parser $parser): void + { + $parser->match(TokenType::T_IDENTIFIER); + $parser->match(TokenType::T_OPEN_PARENTHESIS); + + $this->stringPrimary = $parser->StringPrimary(); + + $parser->match(TokenType::T_CLOSE_PARENTHESIS); + } +} 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 @@ +walkGeneralCaseExpression($this); + } +} 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 @@ +walkGroupByClause($this); + } +} 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 @@ +walkHavingClause($this); + } +} 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 @@ +walkIdentificationVariableDeclaration($this); + } +} 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 @@ + $literals */ + public function __construct( + public ArithmeticExpression $expression, + public array $literals, + public bool $not = false, + ) { + } + + public function dispatch(SqlWalker $walker): string + { + return $walker->walkInListExpression($this); + } +} 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 @@ +walkInSubselectExpression($this); + } +} 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 @@ +walkIndexBy($this); + + return ''; + } +} 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 @@ +isNamed = ! is_numeric($param); + $this->name = $param; + } + + public function dispatch(SqlWalker $walker): string + { + return $walker->walkInputParameter($this); + } +} 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 @@ + $value */ + public function __construct( + public string $identificationVariable, + public array $value, + public bool $not = false, + ) { + } + + public function dispatch(SqlWalker $walker): string + { + return $walker->walkInstanceOfExpression($this); + } +} 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 @@ +walkJoin($this); + } +} 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 @@ +walkJoinAssociationDeclaration($this); + } +} 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 @@ +walkJoinPathExpression($this); + } +} 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 @@ +walkJoinVariableDeclaration($this); + } +} 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 @@ +walkLikeExpression($this); + } +} 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 @@ +walkLiteral($this); + } +} 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 @@ +walkNewObject($this); + } +} 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 @@ +dump($this); + } + + public function dump(mixed $value): string + { + static $ident = 0; + + $str = ''; + + if ($value instanceof Node) { + $str .= get_debug_type($value) . '(' . PHP_EOL; + $props = get_object_vars($value); + + foreach ($props as $name => $prop) { + $ident += 4; + $str .= str_repeat(' ', $ident) . '"' . $name . '": ' + . $this->dump($prop) . ',' . PHP_EOL; + $ident -= 4; + } + + $str .= str_repeat(' ', $ident) . ')'; + } elseif (is_array($value)) { + $ident += 4; + $str .= 'array('; + $some = false; + + foreach ($value as $k => $v) { + $str .= PHP_EOL . str_repeat(' ', $ident) . '"' + . $k . '" => ' . $this->dump($v) . ','; + $some = true; + } + + $ident -= 4; + $str .= ($some ? PHP_EOL . str_repeat(' ', $ident) : '') . ')'; + } elseif (is_object($value)) { + $str .= 'instanceof(' . get_debug_type($value) . ')'; + } else { + $str .= var_export($value, true); + } + + return $str; + } +} 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 @@ +walkNullComparisonExpression($this); + } +} 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 @@ +walkNullIfExpression($this); + } +} 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 @@ +walkOrderByClause($this); + } +} 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 @@ +type) === 'ASC'; + } + + public function isDesc(): bool + { + return strtoupper($this->type) === 'DESC'; + } + + public function dispatch(SqlWalker $walker): string + { + return $walker->walkOrderByItem($this); + } +} 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 @@ +walkParenthesisExpression($this); + } +} 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 @@ + $expectedType */ + public function __construct( + public int $expectedType, + public string $identificationVariable, + public string|null $field = null, + ) { + } + + public function dispatch(SqlWalker $walker): string + { + return $walker->walkPathExpression($this); + } +} 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 @@ +type) === 'ALL'; + } + + public function isAny(): bool + { + return strtoupper($this->type) === 'ANY'; + } + + public function isSome(): bool + { + return strtoupper($this->type) === 'SOME'; + } + + public function dispatch(SqlWalker $walker): string + { + return $walker->walkQuantifiedExpression($this); + } +} 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 @@ +walkRangeVariableDeclaration($this); + } +} 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 @@ +walkSelectClause($this); + } +} 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 @@ +walkSelectExpression($this); + } +} 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 @@ +walkSelectStatement($this); + } +} 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 @@ +walkSimpleArithmeticExpression($this); + } +} 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 @@ +walkSimpleCaseExpression($this); + } +} 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 @@ +walkSimpleSelectClause($this); + } +} 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 @@ +walkSimpleSelectExpression($this); + } +} 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 @@ +walkWhenClauseExpression($this); + } +} 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 @@ +walkSubselect($this); + } +} 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 @@ +walkSubselectFromClause($this); + } +} 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 @@ +walkUpdateClause($this); + } +} 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 @@ +walkUpdateItem($this); + } +} 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 @@ +walkUpdateStatement($this); + } +} 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 @@ +walkWhenClauseExpression($this); + } +} 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 @@ +walkWhereClause($this); + } +} 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 @@ +, WrapperParameterType>|array + */ +abstract class AbstractSqlExecutor +{ + /** @var list|string */ + protected array|string $sqlStatements; + + protected QueryCacheProfile|null $queryCacheProfile = null; + + /** + * Gets the SQL statements that are executed by the executor. + * + * @return list|string All the SQL update statements. + */ + public function getSqlStatements(): array|string + { + return $this->sqlStatements; + } + + public function setQueryCacheProfile(QueryCacheProfile $qcp): void + { + $this->queryCacheProfile = $qcp; + } + + /** + * Do not use query cache + */ + public function removeQueryCacheProfile(): void + { + $this->queryCacheProfile = null; + } + + /** + * Executes all sql statements. + * + * @param Connection $conn The database connection that is used to execute the queries. + * @param list|array $params The parameters. + * @psalm-param WrapperParameterTypeArray $types The parameter types. + */ + abstract public function execute(Connection $conn, array $params, array $types): Result|int; +} 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 @@ +MultiTableDeleteExecutor. + * + * Internal note: Any SQL construction and preparation takes place in the constructor for + * best performance. With a query cache the executor will be cached. + * + * @param DeleteStatement $AST The root AST node of the DQL query. + * @param SqlWalker $sqlWalker The walker used for SQL generation from the AST. + */ + public function __construct(AST\Node $AST, SqlWalker $sqlWalker) + { + $em = $sqlWalker->getEntityManager(); + $conn = $em->getConnection(); + $platform = $conn->getDatabasePlatform(); + $quoteStrategy = $em->getConfiguration()->getQuoteStrategy(); + + if ($conn instanceof PrimaryReadReplicaConnection) { + $conn->ensureConnectedToPrimary(); + } + + $primaryClass = $em->getClassMetadata($AST->deleteClause->abstractSchemaName); + $primaryDqlAlias = $AST->deleteClause->aliasIdentificationVariable; + $rootClass = $em->getClassMetadata($primaryClass->rootEntityName); + + $tempTable = $platform->getTemporaryTableName($rootClass->getTemporaryIdTableName()); + $idColumnNames = $rootClass->getIdentifierColumnNames(); + $idColumnList = implode(', ', $idColumnNames); + + // 1. Create an INSERT INTO temptable ... SELECT identifiers WHERE $AST->getWhereClause() + $sqlWalker->setSQLTableAlias($primaryClass->getTableName(), 't0', $primaryDqlAlias); + + $insertSql = 'INSERT INTO ' . $tempTable . ' (' . $idColumnList . ')' + . ' SELECT t0.' . implode(', t0.', $idColumnNames); + + $rangeDecl = new AST\RangeVariableDeclaration($primaryClass->name, $primaryDqlAlias); + $fromClause = new AST\FromClause([new AST\IdentificationVariableDeclaration($rangeDecl, null, [])]); + $insertSql .= $sqlWalker->walkFromClause($fromClause); + + // Append WHERE clause, if there is one. + if ($AST->whereClause) { + $insertSql .= $sqlWalker->walkWhereClause($AST->whereClause); + } + + $this->insertSql = $insertSql; + + // 2. Create ID subselect statement used in DELETE ... WHERE ... IN (subselect) + $idSubselect = 'SELECT ' . $idColumnList . ' FROM ' . $tempTable; + + // 3. Create and store DELETE statements + $classNames = [...$primaryClass->parentClasses, ...[$primaryClass->name], ...$primaryClass->subClasses]; + foreach (array_reverse($classNames) as $className) { + $tableName = $quoteStrategy->getTableName($em->getClassMetadata($className), $platform); + $this->sqlStatements[] = 'DELETE FROM ' . $tableName + . ' WHERE (' . $idColumnList . ') IN (' . $idSubselect . ')'; + } + + // 4. Store DDL for temporary identifier table. + $columnDefinitions = []; + foreach ($idColumnNames as $idColumnName) { + $columnDefinitions[$idColumnName] = [ + 'name' => $idColumnName, + 'notnull' => true, + 'type' => Type::getType(PersisterHelper::getTypeOfColumn($idColumnName, $rootClass, $em)), + ]; + } + + $this->createTempTableSql = $platform->getCreateTemporaryTableSnippetSQL() . ' ' . $tempTable . ' (' + . $platform->getColumnDeclarationListSQL($columnDefinitions) . ', PRIMARY KEY(' . implode(',', $idColumnNames) . '))'; + $this->dropTempTableSql = $platform->getDropTemporaryTableSQL($tempTable); + } + + /** + * {@inheritDoc} + */ + public function execute(Connection $conn, array $params, array $types): int + { + // Create temporary id table + $conn->executeStatement($this->createTempTableSql); + + try { + // Insert identifiers + $numDeleted = $conn->executeStatement($this->insertSql, $params, $types); + + // Execute DELETE statements + foreach ($this->sqlStatements as $sql) { + $conn->executeStatement($sql); + } + } catch (Throwable $exception) { + // FAILURE! Drop temporary table to avoid possible collisions + $conn->executeStatement($this->dropTempTableSql); + + // Re-throw exception + throw $exception; + } + + // Drop temporary table + $conn->executeStatement($this->dropTempTableSql); + + return $numDeleted; + } +} 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 @@ +MultiTableUpdateExecutor. + * + * Internal note: Any SQL construction and preparation takes place in the constructor for + * best performance. With a query cache the executor will be cached. + * + * @param UpdateStatement $AST The root AST node of the DQL query. + * @param SqlWalker $sqlWalker The walker used for SQL generation from the AST. + */ + public function __construct(AST\Node $AST, SqlWalker $sqlWalker) + { + $em = $sqlWalker->getEntityManager(); + $conn = $em->getConnection(); + $platform = $conn->getDatabasePlatform(); + $quoteStrategy = $em->getConfiguration()->getQuoteStrategy(); + $this->sqlStatements = []; + + if ($conn instanceof PrimaryReadReplicaConnection) { + $conn->ensureConnectedToPrimary(); + } + + $updateClause = $AST->updateClause; + $primaryClass = $sqlWalker->getEntityManager()->getClassMetadata($updateClause->abstractSchemaName); + $rootClass = $em->getClassMetadata($primaryClass->rootEntityName); + + $updateItems = $updateClause->updateItems; + + $tempTable = $platform->getTemporaryTableName($rootClass->getTemporaryIdTableName()); + $idColumnNames = $rootClass->getIdentifierColumnNames(); + $idColumnList = implode(', ', $idColumnNames); + + // 1. Create an INSERT INTO temptable ... SELECT identifiers WHERE $AST->getWhereClause() + $sqlWalker->setSQLTableAlias($primaryClass->getTableName(), 't0', $updateClause->aliasIdentificationVariable); + + $insertSql = 'INSERT INTO ' . $tempTable . ' (' . $idColumnList . ')' + . ' SELECT t0.' . implode(', t0.', $idColumnNames); + + $rangeDecl = new AST\RangeVariableDeclaration($primaryClass->name, $updateClause->aliasIdentificationVariable); + $fromClause = new AST\FromClause([new AST\IdentificationVariableDeclaration($rangeDecl, null, [])]); + + $insertSql .= $sqlWalker->walkFromClause($fromClause); + + // 2. Create ID subselect statement used in UPDATE ... WHERE ... IN (subselect) + $idSubselect = 'SELECT ' . $idColumnList . ' FROM ' . $tempTable; + + // 3. Create and store UPDATE statements + $classNames = [...$primaryClass->parentClasses, ...[$primaryClass->name], ...$primaryClass->subClasses]; + + foreach (array_reverse($classNames) as $className) { + $affected = false; + $class = $em->getClassMetadata($className); + $updateSql = 'UPDATE ' . $quoteStrategy->getTableName($class, $platform) . ' SET '; + + $sqlParameters = []; + foreach ($updateItems as $updateItem) { + $field = $updateItem->pathExpression->field; + + if ( + (isset($class->fieldMappings[$field]) && ! isset($class->fieldMappings[$field]->inherited)) || + (isset($class->associationMappings[$field]) && ! isset($class->associationMappings[$field]->inherited)) + ) { + $newValue = $updateItem->newValue; + + if (! $affected) { + $affected = true; + } else { + $updateSql .= ', '; + } + + $updateSql .= $sqlWalker->walkUpdateItem($updateItem); + + if ($newValue instanceof AST\InputParameter) { + $sqlParameters[] = $newValue->name; + + ++$this->numParametersInUpdateClause; + } + } + } + + if ($affected) { + $this->sqlParameters[] = $sqlParameters; + $this->sqlStatements[] = $updateSql . ' WHERE (' . $idColumnList . ') IN (' . $idSubselect . ')'; + } + } + + // Append WHERE clause to insertSql, if there is one. + if ($AST->whereClause) { + $insertSql .= $sqlWalker->walkWhereClause($AST->whereClause); + } + + $this->insertSql = $insertSql; + + // 4. Store DDL for temporary identifier table. + $columnDefinitions = []; + + foreach ($idColumnNames as $idColumnName) { + $columnDefinitions[$idColumnName] = [ + 'name' => $idColumnName, + 'notnull' => true, + 'type' => Type::getType(PersisterHelper::getTypeOfColumn($idColumnName, $rootClass, $em)), + ]; + } + + $this->createTempTableSql = $platform->getCreateTemporaryTableSnippetSQL() . ' ' . $tempTable . ' (' + . $platform->getColumnDeclarationListSQL($columnDefinitions) . ', PRIMARY KEY(' . implode(',', $idColumnNames) . '))'; + + $this->dropTempTableSql = $platform->getDropTemporaryTableSQL($tempTable); + } + + /** + * {@inheritDoc} + */ + public function execute(Connection $conn, array $params, array $types): int + { + // Create temporary id table + $conn->executeStatement($this->createTempTableSql); + + try { + // Insert identifiers. Parameters from the update clause are cut off. + $numUpdated = $conn->executeStatement( + $this->insertSql, + array_slice($params, $this->numParametersInUpdateClause), + array_slice($types, $this->numParametersInUpdateClause), + ); + + // Execute UPDATE statements + foreach ($this->sqlStatements as $key => $statement) { + $paramValues = []; + $paramTypes = []; + + if (isset($this->sqlParameters[$key])) { + foreach ($this->sqlParameters[$key] as $parameterKey => $parameterName) { + $paramValues[] = $params[$parameterKey]; + $paramTypes[] = $types[$parameterKey] ?? ParameterTypeInferer::inferType($params[$parameterKey]); + } + } + + $conn->executeStatement($statement, $paramValues, $paramTypes); + } + } finally { + // Drop temporary table + $conn->executeStatement($this->dropTempTableSql); + } + + return $numUpdated; + } +} 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 @@ +sqlStatements = $sqlWalker->walkSelectStatement($AST); + } + + /** + * {@inheritDoc} + */ + public function execute(Connection $conn, array $params, array $types): Result + { + return $conn->executeQuery($this->sqlStatements, $params, $types, $this->queryCacheProfile); + } +} 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 @@ +sqlStatements = $sqlWalker->walkUpdateStatement($AST); + } elseif ($AST instanceof AST\DeleteStatement) { + $this->sqlStatements = $sqlWalker->walkDeleteStatement($AST); + } + } + + /** + * {@inheritDoc} + */ + public function execute(Connection $conn, array $params, array $types): int + { + if ($conn instanceof PrimaryReadReplicaConnection) { + $conn->ensureConnectedToPrimary(); + } + + return $conn->executeStatement($this->sqlStatements, $params, $types); + } +} 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 @@ +andX($expr->eq('u.type', ':1'), $expr->eq('u.role', ':2')); + * + * @param Expr\Comparison|Expr\Func|Expr\Andx|Expr\Orx|string ...$x Optional clause. Defaults to null, + * but requires at least one defined + * when converting to string. + */ + public function andX(Expr\Comparison|Expr\Func|Expr\Andx|Expr\Orx|string ...$x): Expr\Andx + { + self::validateVariadicParameter($x); + + return new Expr\Andx($x); + } + + /** + * Creates a disjunction of the given boolean expressions. + * + * Example: + * + * [php] + * // (u.type = ?1) OR (u.role = ?2) + * $q->where($q->expr()->orX('u.type = ?1', 'u.role = ?2')); + * + * @param Expr\Comparison|Expr\Func|Expr\Andx|Expr\Orx|string ...$x Optional clause. Defaults to null, + * but requires at least one defined + * when converting to string. + */ + public function orX(Expr\Comparison|Expr\Func|Expr\Andx|Expr\Orx|string ...$x): Expr\Orx + { + self::validateVariadicParameter($x); + + return new Expr\Orx($x); + } + + /** + * Creates an ASCending order expression. + */ + public function asc(mixed $expr): Expr\OrderBy + { + return new Expr\OrderBy($expr, 'ASC'); + } + + /** + * Creates a DESCending order expression. + */ + public function desc(mixed $expr): Expr\OrderBy + { + return new Expr\OrderBy($expr, 'DESC'); + } + + /** + * Creates an equality comparison expression with the given arguments. + * + * First argument is considered the left expression and the second is the right expression. + * When converted to string, it will generated a = . Example: + * + * [php] + * // u.id = ?1 + * $expr->eq('u.id', '?1'); + * + * @param mixed $x Left expression. + * @param mixed $y Right expression. + */ + public function eq(mixed $x, mixed $y): Expr\Comparison + { + return new Expr\Comparison($x, Expr\Comparison::EQ, $y); + } + + /** + * Creates an instance of Expr\Comparison, with the given arguments. + * First argument is considered the left expression and the second is the right expression. + * When converted to string, it will generated a <> . Example: + * + * [php] + * // u.id <> ?1 + * $q->where($q->expr()->neq('u.id', '?1')); + * + * @param mixed $x Left expression. + * @param mixed $y Right expression. + */ + public function neq(mixed $x, mixed $y): Expr\Comparison + { + return new Expr\Comparison($x, Expr\Comparison::NEQ, $y); + } + + /** + * Creates an instance of Expr\Comparison, with the given arguments. + * First argument is considered the left expression and the second is the right expression. + * When converted to string, it will generated a < . Example: + * + * [php] + * // u.id < ?1 + * $q->where($q->expr()->lt('u.id', '?1')); + * + * @param mixed $x Left expression. + * @param mixed $y Right expression. + */ + public function lt(mixed $x, mixed $y): Expr\Comparison + { + return new Expr\Comparison($x, Expr\Comparison::LT, $y); + } + + /** + * Creates an instance of Expr\Comparison, with the given arguments. + * First argument is considered the left expression and the second is the right expression. + * When converted to string, it will generated a <= . Example: + * + * [php] + * // u.id <= ?1 + * $q->where($q->expr()->lte('u.id', '?1')); + * + * @param mixed $x Left expression. + * @param mixed $y Right expression. + */ + public function lte(mixed $x, mixed $y): Expr\Comparison + { + return new Expr\Comparison($x, Expr\Comparison::LTE, $y); + } + + /** + * Creates an instance of Expr\Comparison, with the given arguments. + * First argument is considered the left expression and the second is the right expression. + * When converted to string, it will generated a > . Example: + * + * [php] + * // u.id > ?1 + * $q->where($q->expr()->gt('u.id', '?1')); + * + * @param mixed $x Left expression. + * @param mixed $y Right expression. + */ + public function gt(mixed $x, mixed $y): Expr\Comparison + { + return new Expr\Comparison($x, Expr\Comparison::GT, $y); + } + + /** + * Creates an instance of Expr\Comparison, with the given arguments. + * First argument is considered the left expression and the second is the right expression. + * When converted to string, it will generated a >= . Example: + * + * [php] + * // u.id >= ?1 + * $q->where($q->expr()->gte('u.id', '?1')); + * + * @param mixed $x Left expression. + * @param mixed $y Right expression. + */ + public function gte(mixed $x, mixed $y): Expr\Comparison + { + return new Expr\Comparison($x, Expr\Comparison::GTE, $y); + } + + /** + * Creates an instance of AVG() function, with the given argument. + * + * @param mixed $x Argument to be used in AVG() function. + */ + public function avg(mixed $x): Expr\Func + { + return new Expr\Func('AVG', [$x]); + } + + /** + * Creates an instance of MAX() function, with the given argument. + * + * @param mixed $x Argument to be used in MAX() function. + */ + public function max(mixed $x): Expr\Func + { + return new Expr\Func('MAX', [$x]); + } + + /** + * Creates an instance of MIN() function, with the given argument. + * + * @param mixed $x Argument to be used in MIN() function. + */ + public function min(mixed $x): Expr\Func + { + return new Expr\Func('MIN', [$x]); + } + + /** + * Creates an instance of COUNT() function, with the given argument. + * + * @param mixed $x Argument to be used in COUNT() function. + */ + public function count(mixed $x): Expr\Func + { + return new Expr\Func('COUNT', [$x]); + } + + /** + * Creates an instance of COUNT(DISTINCT) function, with the given argument. + * + * @param mixed ...$x Argument to be used in COUNT(DISTINCT) function. + */ + public function countDistinct(mixed ...$x): string + { + self::validateVariadicParameter($x); + + return 'COUNT(DISTINCT ' . implode(', ', $x) . ')'; + } + + /** + * Creates an instance of EXISTS() function, with the given DQL Subquery. + * + * @param mixed $subquery DQL Subquery to be used in EXISTS() function. + */ + public function exists(mixed $subquery): Expr\Func + { + return new Expr\Func('EXISTS', [$subquery]); + } + + /** + * Creates an instance of ALL() function, with the given DQL Subquery. + * + * @param mixed $subquery DQL Subquery to be used in ALL() function. + */ + public function all(mixed $subquery): Expr\Func + { + return new Expr\Func('ALL', [$subquery]); + } + + /** + * Creates a SOME() function expression with the given DQL subquery. + * + * @param mixed $subquery DQL Subquery to be used in SOME() function. + */ + public function some(mixed $subquery): Expr\Func + { + return new Expr\Func('SOME', [$subquery]); + } + + /** + * Creates an ANY() function expression with the given DQL subquery. + * + * @param mixed $subquery DQL Subquery to be used in ANY() function. + */ + public function any(mixed $subquery): Expr\Func + { + return new Expr\Func('ANY', [$subquery]); + } + + /** + * Creates a negation expression of the given restriction. + * + * @param mixed $restriction Restriction to be used in NOT() function. + */ + public function not(mixed $restriction): Expr\Func + { + return new Expr\Func('NOT', [$restriction]); + } + + /** + * Creates an ABS() function expression with the given argument. + * + * @param mixed $x Argument to be used in ABS() function. + */ + public function abs(mixed $x): Expr\Func + { + return new Expr\Func('ABS', [$x]); + } + + /** + * Creates a MOD($x, $y) function expression to return the remainder of $x divided by $y. + */ + public function mod(mixed $x, mixed $y): Expr\Func + { + return new Expr\Func('MOD', [$x, $y]); + } + + /** + * Creates a product mathematical expression with the given arguments. + * + * First argument is considered the left expression and the second is the right expression. + * When converted to string, it will generated a * . Example: + * + * [php] + * // u.salary * u.percentAnnualSalaryIncrease + * $q->expr()->prod('u.salary', 'u.percentAnnualSalaryIncrease') + * + * @param mixed $x Left expression. + * @param mixed $y Right expression. + */ + public function prod(mixed $x, mixed $y): Expr\Math + { + return new Expr\Math($x, '*', $y); + } + + /** + * Creates a difference mathematical expression with the given arguments. + * First argument is considered the left expression and the second is the right expression. + * When converted to string, it will generated a - . Example: + * + * [php] + * // u.monthlySubscriptionCount - 1 + * $q->expr()->diff('u.monthlySubscriptionCount', '1') + * + * @param mixed $x Left expression. + * @param mixed $y Right expression. + */ + public function diff(mixed $x, mixed $y): Expr\Math + { + return new Expr\Math($x, '-', $y); + } + + /** + * Creates a sum mathematical expression with the given arguments. + * First argument is considered the left expression and the second is the right expression. + * When converted to string, it will generated a + . Example: + * + * [php] + * // u.numChildren + 1 + * $q->expr()->sum('u.numChildren', '1') + * + * @param mixed $x Left expression. + * @param mixed $y Right expression. + */ + public function sum(mixed $x, mixed $y): Expr\Math + { + return new Expr\Math($x, '+', $y); + } + + /** + * Creates a quotient mathematical expression with the given arguments. + * First argument is considered the left expression and the second is the right expression. + * When converted to string, it will generated a / . Example: + * + * [php] + * // u.total / u.period + * $expr->quot('u.total', 'u.period') + * + * @param mixed $x Left expression. + * @param mixed $y Right expression. + */ + public function quot(mixed $x, mixed $y): Expr\Math + { + return new Expr\Math($x, '/', $y); + } + + /** + * Creates a SQRT() function expression with the given argument. + * + * @param mixed $x Argument to be used in SQRT() function. + */ + public function sqrt(mixed $x): Expr\Func + { + return new Expr\Func('SQRT', [$x]); + } + + /** + * Creates an IN() expression with the given arguments. + * + * @param string $x Field in string format to be restricted by IN() function. + * @param mixed $y Argument to be used in IN() function. + */ + public function in(string $x, mixed $y): Expr\Func + { + if (is_iterable($y)) { + if ($y instanceof Traversable) { + $y = iterator_to_array($y); + } + + foreach ($y as &$literal) { + if (! ($literal instanceof Expr\Literal)) { + $literal = $this->quoteLiteral($literal); + } + } + } + + return new Expr\Func($x . ' IN', (array) $y); + } + + /** + * Creates a NOT IN() expression with the given arguments. + * + * @param string $x Field in string format to be restricted by NOT IN() function. + * @param mixed $y Argument to be used in NOT IN() function. + */ + public function notIn(string $x, mixed $y): Expr\Func + { + if (is_iterable($y)) { + if ($y instanceof Traversable) { + $y = iterator_to_array($y); + } + + foreach ($y as &$literal) { + if (! ($literal instanceof Expr\Literal)) { + $literal = $this->quoteLiteral($literal); + } + } + } + + return new Expr\Func($x . ' NOT IN', (array) $y); + } + + /** + * Creates an IS NULL expression with the given arguments. + * + * @param string $x Field in string format to be restricted by IS NULL. + */ + public function isNull(string $x): string + { + return $x . ' IS NULL'; + } + + /** + * Creates an IS NOT NULL expression with the given arguments. + * + * @param string $x Field in string format to be restricted by IS NOT NULL. + */ + public function isNotNull(string $x): string + { + return $x . ' IS NOT NULL'; + } + + /** + * Creates a LIKE() comparison expression with the given arguments. + * + * @param string $x Field in string format to be inspected by LIKE() comparison. + * @param mixed $y Argument to be used in LIKE() comparison. + */ + public function like(string $x, mixed $y): Expr\Comparison + { + return new Expr\Comparison($x, 'LIKE', $y); + } + + /** + * Creates a NOT LIKE() comparison expression with the given arguments. + * + * @param string $x Field in string format to be inspected by LIKE() comparison. + * @param mixed $y Argument to be used in LIKE() comparison. + */ + public function notLike(string $x, mixed $y): Expr\Comparison + { + return new Expr\Comparison($x, 'NOT LIKE', $y); + } + + /** + * Creates a CONCAT() function expression with the given arguments. + * + * @param mixed ...$x Arguments to be used in CONCAT() function. + */ + public function concat(mixed ...$x): Expr\Func + { + self::validateVariadicParameter($x); + + return new Expr\Func('CONCAT', $x); + } + + /** + * Creates a SUBSTRING() function expression with the given arguments. + * + * @param mixed $x Argument to be used as string to be cropped by SUBSTRING() function. + * @param int $from Initial offset to start cropping string. May accept negative values. + * @param int|null $len Length of crop. May accept negative values. + */ + public function substring(mixed $x, int $from, int|null $len = null): Expr\Func + { + $args = [$x, $from]; + if ($len !== null) { + $args[] = $len; + } + + return new Expr\Func('SUBSTRING', $args); + } + + /** + * Creates a LOWER() function expression with the given argument. + * + * @param mixed $x Argument to be used in LOWER() function. + * + * @return Expr\Func A LOWER function expression. + */ + public function lower(mixed $x): Expr\Func + { + return new Expr\Func('LOWER', [$x]); + } + + /** + * Creates an UPPER() function expression with the given argument. + * + * @param mixed $x Argument to be used in UPPER() function. + * + * @return Expr\Func An UPPER function expression. + */ + public function upper(mixed $x): Expr\Func + { + return new Expr\Func('UPPER', [$x]); + } + + /** + * Creates a LENGTH() function expression with the given argument. + * + * @param mixed $x Argument to be used as argument of LENGTH() function. + * + * @return Expr\Func A LENGTH function expression. + */ + public function length(mixed $x): Expr\Func + { + return new Expr\Func('LENGTH', [$x]); + } + + /** + * Creates a literal expression of the given argument. + * + * @param scalar $literal Argument to be converted to literal. + */ + public function literal(bool|string|int|float $literal): Expr\Literal + { + return new Expr\Literal($this->quoteLiteral($literal)); + } + + /** + * Quotes a literal value, if necessary, according to the DQL syntax. + * + * @param scalar $literal The literal value. + */ + private function quoteLiteral(bool|string|int|float $literal): string + { + if (is_int($literal) || is_float($literal)) { + return (string) $literal; + } + + if (is_bool($literal)) { + return $literal ? 'true' : 'false'; + } + + return "'" . str_replace("'", "''", $literal) . "'"; + } + + /** + * Creates an instance of BETWEEN() function, with the given argument. + * + * @param mixed $val Valued to be inspected by range values. + * @param int|string $x Starting range value to be used in BETWEEN() function. + * @param int|string $y End point value to be used in BETWEEN() function. + * + * @return string A BETWEEN expression. + */ + public function between(mixed $val, int|string $x, int|string $y): string + { + return $val . ' BETWEEN ' . $x . ' AND ' . $y; + } + + /** + * Creates an instance of TRIM() function, with the given argument. + * + * @param mixed $x Argument to be used as argument of TRIM() function. + * + * @return Expr\Func a TRIM expression. + */ + public function trim(mixed $x): Expr\Func + { + return new Expr\Func('TRIM', $x); + } + + /** + * Creates an instance of MEMBER OF function, with the given arguments. + * + * @param string $x Value to be checked + * @param string $y Value to be checked against + */ + public function isMemberOf(string $x, string $y): Expr\Comparison + { + return new Expr\Comparison($x, 'MEMBER OF', $y); + } + + /** + * Creates an instance of INSTANCE OF function, with the given arguments. + * + * @param string $x Value to be checked + * @param string $y Value to be checked against + */ + public function isInstanceOf(string $x, string $y): Expr\Comparison + { + return new Expr\Comparison($x, 'INSTANCE OF', $y); + } +} 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 @@ + */ + protected array $parts = []; + + /** @psalm-return list */ + public function getParts(): array + { + return $this->parts; + } +} 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 @@ + */ + protected array $allowedClasses = []; + + /** @var list */ + protected array $parts = []; + + public function __construct(mixed $args = []) + { + if (is_array($args) && array_key_exists(0, $args) && is_array($args[0])) { + $args = $args[0]; + } + + $this->addMultiple($args); + } + + /** + * @param string[]|object[]|string|object $args + * @psalm-param list|string|object $args + * + * @return $this + */ + public function addMultiple(array|string|object $args = []): static + { + foreach ((array) $args as $arg) { + $this->add($arg); + } + + return $this; + } + + /** + * @return $this + * + * @throws InvalidArgumentException + */ + public function add(mixed $arg): static + { + if ($arg !== null && (! $arg instanceof self || $arg->count() > 0)) { + // If we decide to keep Expr\Base instances, we can use this check + if (! is_string($arg) && ! in_array($arg::class, $this->allowedClasses, true)) { + throw new InvalidArgumentException(sprintf( + "Expression of type '%s' not allowed in this context.", + get_debug_type($arg), + )); + } + + $this->parts[] = $arg; + } + + return $this; + } + + /** @psalm-return 0|positive-int */ + public function count(): int + { + return count($this->parts); + } + + public function __toString(): string + { + if ($this->count() === 1) { + return (string) $this->parts[0]; + } + + return $this->preSeparator . implode($this->separator, $this->parts) . $this->postSeparator; + } +} 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 @@ +'; + final public const LT = '<'; + final public const LTE = '<='; + final public const GT = '>'; + final public const GTE = '>='; + + /** Creates a comparison expression with the given arguments. */ + public function __construct(protected mixed $leftExpr, protected string $operator, protected mixed $rightExpr) + { + } + + public function getLeftExpr(): mixed + { + return $this->leftExpr; + } + + public function getOperator(): string + { + return $this->operator; + } + + public function getRightExpr(): mixed + { + return $this->rightExpr; + } + + public function __toString(): string + { + return $this->leftExpr . ' ' . $this->operator . ' ' . $this->rightExpr; + } +} 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 @@ +count() === 1) { + return (string) $this->parts[0]; + } + + $components = []; + + foreach ($this->parts as $part) { + $components[] = $this->processQueryPart($part); + } + + return implode($this->separator, $components); + } + + private function processQueryPart(string|Stringable $part): string + { + $queryPart = (string) $part; + + if (is_object($part) && $part instanceof self && $part->count() > 1) { + return $this->preSeparator . $queryPart . $this->postSeparator; + } + + // Fixes DDC-1237: User may have added a where item containing nested expression (with "OR" or "AND") + if (preg_match('/\s(OR|AND)\s/i', $queryPart)) { + return $this->preSeparator . $queryPart . $this->postSeparator; + } + + return $queryPart; + } +} 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 @@ +from; + } + + public function getAlias(): string + { + return $this->alias; + } + + public function getIndexBy(): string|null + { + return $this->indexBy; + } + + public function __toString(): string + { + return $this->from . ' ' . $this->alias . + ($this->indexBy ? ' INDEX BY ' . $this->indexBy : ''); + } +} 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 @@ +|mixed $arguments + */ + public function __construct( + protected string $name, + mixed $arguments, + ) { + $this->arguments = (array) $arguments; + } + + public function getName(): string + { + return $this->name; + } + + /** @psalm-return list */ + public function getArguments(): array + { + return $this->arguments; + } + + public function __toString(): string + { + return $this->name . '(' . implode(', ', $this->arguments) . ')'; + } +} 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 @@ + */ + protected array $parts = []; + + /** @psalm-return list */ + public function getParts(): array + { + return $this->parts; + } +} 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 @@ +joinType; + } + + public function getJoin(): string + { + return $this->join; + } + + public function getAlias(): string|null + { + return $this->alias; + } + + /** @psalm-return self::ON|self::WITH|null */ + public function getConditionType(): string|null + { + return $this->conditionType; + } + + public function getCondition(): string|Comparison|Composite|Func|null + { + return $this->condition; + } + + public function getIndexBy(): string|null + { + return $this->indexBy; + } + + public function __toString(): string + { + return strtoupper($this->joinType) . ' JOIN ' . $this->join + . ($this->alias ? ' ' . $this->alias : '') + . ($this->indexBy ? ' INDEX BY ' . $this->indexBy : '') + . ($this->condition ? ' ' . strtoupper($this->conditionType) . ' ' . $this->condition : ''); + } +} 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 @@ + */ + protected array $parts = []; + + /** @psalm-return list */ + public function getParts(): array + { + return $this->parts; + } +} 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 @@ +leftExpr; + } + + public function getOperator(): string + { + return $this->operator; + } + + public function getRightExpr(): mixed + { + return $this->rightExpr; + } + + public function __toString(): string + { + // Adjusting Left Expression + $leftExpr = (string) $this->leftExpr; + + if ($this->leftExpr instanceof Math) { + $leftExpr = '(' . $leftExpr . ')'; + } + + // Adjusting Right Expression + $rightExpr = (string) $this->rightExpr; + + if ($this->rightExpr instanceof Math) { + $rightExpr = '(' . $rightExpr . ')'; + } + + return $leftExpr . ' ' . $this->operator . ' ' . $rightExpr; + } +} 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 @@ + */ + protected array $parts = []; + + public function __construct( + string|null $sort = null, + string|null $order = null, + ) { + if ($sort) { + $this->add($sort, $order); + } + } + + public function add(string $sort, string|null $order = null): void + { + $order = ! $order ? 'ASC' : $order; + $this->parts[] = $sort . ' ' . $order; + } + + /** @psalm-return 0|positive-int */ + public function count(): int + { + return count($this->parts); + } + + /** @psalm-return list */ + public function getParts(): array + { + return $this->parts; + } + + public function __toString(): string + { + return $this->preSeparator . implode($this->separator, $this->parts) . $this->postSeparator; + } +} 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 @@ + */ + protected array $parts = []; + + /** @psalm-return list */ + public function getParts(): array + { + return $this->parts; + } +} 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 @@ + */ + protected array $parts = []; + + /** @psalm-return list */ + public function getParts(): array + { + return $this->parts; + } +} 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 @@ + + */ + private array $parameters = []; + + final public function __construct( + private readonly EntityManagerInterface $em, + ) { + } + + /** + * Sets a parameter list that can be used by the filter. + * + * @param array $values List of parameter values. + * @param string $type The parameter type. If specified, the given value will be run through + * the type conversion of this type. + * + * @return $this + */ + final public function setParameterList(string $name, array $values, string $type = Types::STRING): static + { + $this->parameters[$name] = ['value' => $values, 'type' => $type, 'is_list' => true]; + + // Keep the parameters sorted for the hash + ksort($this->parameters); + + // The filter collection of the EM is now dirty + $this->em->getFilters()->setFiltersStateDirty(); + + return $this; + } + + /** + * Sets a parameter that can be used by the filter. + * + * @param string|null $type The parameter type. If specified, the given value will be run through + * the type conversion of this type. This is usually not needed for + * strings and numeric types. + * + * @return $this + */ + final public function setParameter(string $name, mixed $value, string|null $type = null): static + { + if ($type === null) { + $type = ParameterTypeInferer::inferType($value); + } + + $this->parameters[$name] = ['value' => $value, 'type' => $type, 'is_list' => false]; + + // Keep the parameters sorted for the hash + ksort($this->parameters); + + // The filter collection of the EM is now dirty + $this->em->getFilters()->setFiltersStateDirty(); + + return $this; + } + + /** + * Gets a parameter to use in a query. + * + * The function is responsible for the right output escaping to use the + * value in a query. + * + * @return string The SQL escaped parameter to use in a query. + * + * @throws InvalidArgumentException + */ + final public function getParameter(string $name): string + { + if (! isset($this->parameters[$name])) { + throw new InvalidArgumentException("Parameter '" . $name . "' does not exist."); + } + + if ($this->parameters[$name]['is_list']) { + throw FilterException::cannotConvertListParameterIntoSingleValue($name); + } + + return $this->em->getConnection()->quote((string) $this->parameters[$name]['value']); + } + + /** + * Gets a parameter to use in a query assuming it's a list of entries. + * + * The function is responsible for the right output escaping to use the + * value in a query, separating each entry by comma to inline it into + * an IN() query part. + * + * @throws InvalidArgumentException + */ + final public function getParameterList(string $name): string + { + if (! isset($this->parameters[$name])) { + throw new InvalidArgumentException("Parameter '" . $name . "' does not exist."); + } + + if ($this->parameters[$name]['is_list'] === false) { + throw FilterException::cannotConvertSingleParameterIntoListValue($name); + } + + $param = $this->parameters[$name]; + $connection = $this->em->getConnection(); + + $quoted = array_map( + static fn (mixed $value): string => $connection->quote((string) $value), + $param['value'], + ); + + return implode(',', $quoted); + } + + /** + * Checks if a parameter was set for the filter. + */ + final public function hasParameter(string $name): bool + { + return isset($this->parameters[$name]); + } + + /** + * Returns as string representation of the SQLFilter parameters (the state). + */ + final public function __toString(): string + { + return serialize($this->parameters); + } + + /** + * Returns the database connection used by the entity manager + */ + final protected function getConnection(): Connection + { + return $this->em->getConnection(); + } + + /** + * Gets the SQL query part to add to a query. + * + * @psalm-param ClassMetadata $targetEntity + * + * @return string The constraint SQL if there is available, empty string otherwise. + */ + abstract public function addFilterConstraint(ClassMetadata $targetEntity, string $targetTableAlias): string; +} 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 @@ + + */ + private array $enabledFilters = []; + + /** The filter hash from the last time the query was parsed. */ + private string $filterHash = ''; + + /** + * Instances of suspended filters. + * + * @var SQLFilter[] + * @psalm-var array + */ + private array $suspendedFilters = []; + + /** + * The current state of this filter. + * + * @psalm-var self::FILTERS_STATE_* + */ + private int $filtersState = self::FILTERS_STATE_CLEAN; + + public function __construct( + private readonly EntityManagerInterface $em, + ) { + $this->config = $em->getConfiguration(); + } + + /** + * Gets all the enabled filters. + * + * @return array The enabled filters. + */ + public function getEnabledFilters(): array + { + return $this->enabledFilters; + } + + /** + * Gets all the suspended filters. + * + * @return SQLFilter[] The suspended filters. + * @psalm-return array + */ + public function getSuspendedFilters(): array + { + return $this->suspendedFilters; + } + + /** + * Enables a filter from the collection. + * + * @throws InvalidArgumentException If the filter does not exist. + */ + public function enable(string $name): SQLFilter + { + if (! $this->has($name)) { + throw new InvalidArgumentException("Filter '" . $name . "' does not exist."); + } + + if (! $this->isEnabled($name)) { + $filterClass = $this->config->getFilterClassName($name); + + assert($filterClass !== null); + + $this->enabledFilters[$name] = new $filterClass($this->em); + + // In case a suspended filter with the same name was forgotten + unset($this->suspendedFilters[$name]); + + // Keep the enabled filters sorted for the hash + ksort($this->enabledFilters); + + $this->setFiltersStateDirty(); + } + + return $this->enabledFilters[$name]; + } + + /** + * Disables a filter. + * + * @throws InvalidArgumentException If the filter does not exist. + */ + public function disable(string $name): SQLFilter + { + // Get the filter to return it + $filter = $this->getFilter($name); + + unset($this->enabledFilters[$name]); + + $this->setFiltersStateDirty(); + + return $filter; + } + + /** + * Suspend a filter. + * + * @param string $name Name of the filter. + * + * @return SQLFilter The suspended filter. + * + * @throws InvalidArgumentException If the filter does not exist. + */ + public function suspend(string $name): SQLFilter + { + // Get the filter to return it + $filter = $this->getFilter($name); + + $this->suspendedFilters[$name] = $filter; + unset($this->enabledFilters[$name]); + + $this->setFiltersStateDirty(); + + return $filter; + } + + /** + * Restore a disabled filter from the collection. + * + * @param string $name Name of the filter. + * + * @return SQLFilter The restored filter. + * + * @throws InvalidArgumentException If the filter does not exist. + */ + public function restore(string $name): SQLFilter + { + if (! $this->isSuspended($name)) { + throw new InvalidArgumentException("Filter '" . $name . "' is not suspended."); + } + + $this->enabledFilters[$name] = $this->suspendedFilters[$name]; + unset($this->suspendedFilters[$name]); + + // Keep the enabled filters sorted for the hash + ksort($this->enabledFilters); + + $this->setFiltersStateDirty(); + + return $this->enabledFilters[$name]; + } + + /** + * Gets an enabled filter from the collection. + * + * @throws InvalidArgumentException If the filter is not enabled. + */ + public function getFilter(string $name): SQLFilter + { + if (! $this->isEnabled($name)) { + throw new InvalidArgumentException("Filter '" . $name . "' is not enabled."); + } + + return $this->enabledFilters[$name]; + } + + /** + * Checks whether filter with given name is defined. + */ + public function has(string $name): bool + { + return $this->config->getFilterClassName($name) !== null; + } + + /** + * Checks if a filter is enabled. + */ + public function isEnabled(string $name): bool + { + return isset($this->enabledFilters[$name]); + } + + /** + * Checks if a filter is suspended. + * + * @param string $name Name of the filter. + * + * @return bool True if the filter is suspended, false otherwise. + */ + public function isSuspended(string $name): bool + { + return isset($this->suspendedFilters[$name]); + } + + /** + * Checks if the filter collection is clean. + */ + public function isClean(): bool + { + return $this->filtersState === self::FILTERS_STATE_CLEAN; + } + + /** + * Generates a string of currently enabled filters to use for the cache id. + */ + public function getHash(): string + { + // If there are only clean filters, the previous hash can be returned + if ($this->filtersState === self::FILTERS_STATE_CLEAN) { + return $this->filterHash; + } + + $filterHash = ''; + + foreach ($this->enabledFilters as $name => $filter) { + $filterHash .= $name . $filter; + } + + $this->filterHash = $filterHash; + $this->filtersState = self::FILTERS_STATE_CLEAN; + + return $filterHash; + } + + /** + * Sets the filter state to dirty. + */ + public function setFiltersStateDirty(): void + { + $this->filtersState = self::FILTERS_STATE_DIRTY; + } +} 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 @@ + + */ +class Lexer extends AbstractLexer +{ + /** + * Creates a new query scanner object. + * + * @param string $input A query string. + */ + public function __construct(string $input) + { + $this->setInput($input); + } + + /** + * {@inheritDoc} + */ + protected function getCatchablePatterns(): array + { + return [ + '[a-z_][a-z0-9_]*\:[a-z_][a-z0-9_]*(?:\\\[a-z_][a-z0-9_]*)*', // aliased name + '[a-z_\\\][a-z0-9_]*(?:\\\[a-z_][a-z0-9_]*)*', // identifier or qualified name + '(?:[0-9]+(?:[\.][0-9]+)*)(?:e[+-]?[0-9]+)?', // numbers + "'(?:[^']|'')*'", // quoted strings + '\?[0-9]*|:[a-z_][a-z0-9_]*', // parameters + ]; + } + + /** + * {@inheritDoc} + */ + protected function getNonCatchablePatterns(): array + { + return ['\s+', '--.*', '(.)']; + } + + protected function getType(string &$value): TokenType + { + $type = TokenType::T_NONE; + + switch (true) { + // Recognize numeric values + case is_numeric($value): + if (str_contains($value, '.') || stripos($value, 'e') !== false) { + return TokenType::T_FLOAT; + } + + return TokenType::T_INTEGER; + + // Recognize quoted strings + case $value[0] === "'": + $value = str_replace("''", "'", substr($value, 1, strlen($value) - 2)); + + return TokenType::T_STRING; + + // Recognize identifiers, aliased or qualified names + case ctype_alpha($value[0]) || $value[0] === '_' || $value[0] === '\\': + $name = 'Doctrine\ORM\Query\TokenType::T_' . strtoupper($value); + + if (defined($name)) { + $type = constant($name); + + if ($type->value > 100) { + return $type; + } + } + + if (str_contains($value, '\\')) { + return TokenType::T_FULLY_QUALIFIED_NAME; + } + + return TokenType::T_IDENTIFIER; + + // Recognize input parameters + case $value[0] === '?' || $value[0] === ':': + return TokenType::T_INPUT_PARAMETER; + + // Recognize symbols + case $value === '.': + return TokenType::T_DOT; + + case $value === ',': + return TokenType::T_COMMA; + + case $value === '(': + return TokenType::T_OPEN_PARENTHESIS; + + case $value === ')': + return TokenType::T_CLOSE_PARENTHESIS; + + case $value === '=': + return TokenType::T_EQUALS; + + case $value === '>': + return TokenType::T_GREATER_THAN; + + case $value === '<': + return TokenType::T_LOWER_THAN; + + case $value === '+': + return TokenType::T_PLUS; + + case $value === '-': + return TokenType::T_MINUS; + + case $value === '*': + return TokenType::T_MULTIPLY; + + case $value === '/': + return TokenType::T_DIVIDE; + + case $value === '!': + return TokenType::T_NEGATE; + + case $value === '{': + return TokenType::T_OPEN_CURLY_BRACE; + + case $value === '}': + return TokenType::T_CLOSE_CURLY_BRACE; + + // Default + default: + // Do nothing + } + + return $type; + } +} 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 @@ +name = self::normalizeName($name); + $this->typeSpecified = $type !== null; + + $this->setValue($value, $type); + } + + /** + * Retrieves the Parameter name. + */ + public function getName(): string + { + return $this->name; + } + + /** + * Retrieves the Parameter value. + */ + public function getValue(): mixed + { + return $this->value; + } + + /** + * Retrieves the Parameter type. + */ + public function getType(): mixed + { + return $this->type; + } + + /** + * Defines the Parameter value. + */ + public function setValue(mixed $value, mixed $type = null): void + { + $this->value = $value; + $this->type = $type ?: ParameterTypeInferer::inferType($value); + } + + public function typeWasSpecified(): bool + { + return $this->typeSpecified; + } +} 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 @@ +value) + ? Types::INTEGER + : Types::STRING; + } + + if (is_array($value)) { + $firstValue = current($value); + if ($firstValue instanceof BackedEnum) { + $firstValue = $firstValue->value; + } + + return is_int($firstValue) + ? ArrayParameterType::INTEGER + : ArrayParameterType::STRING; + } + + return ParameterType::STRING; + } + + private function __construct() + { + } +} 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 @@ + + * @psalm-type QueryComponent = array{ + * metadata?: ClassMetadata, + * parent?: string|null, + * relation?: AssociationMapping|null, + * map?: string|null, + * resultVariable?: AST\Node|string, + * nestingLevel: int, + * token: DqlToken, + * } + */ +final class Parser +{ + /** + * @readonly Maps BUILT-IN string function names to AST class names. + * @psalm-var array> + */ + private static array $stringFunctions = [ + 'concat' => Functions\ConcatFunction::class, + 'substring' => Functions\SubstringFunction::class, + 'trim' => Functions\TrimFunction::class, + 'lower' => Functions\LowerFunction::class, + 'upper' => Functions\UpperFunction::class, + 'identity' => Functions\IdentityFunction::class, + ]; + + /** + * @readonly Maps BUILT-IN numeric function names to AST class names. + * @psalm-var array> + */ + private static array $numericFunctions = [ + 'length' => Functions\LengthFunction::class, + 'locate' => Functions\LocateFunction::class, + 'abs' => Functions\AbsFunction::class, + 'sqrt' => Functions\SqrtFunction::class, + 'mod' => Functions\ModFunction::class, + 'size' => Functions\SizeFunction::class, + 'date_diff' => Functions\DateDiffFunction::class, + 'bit_and' => Functions\BitAndFunction::class, + 'bit_or' => Functions\BitOrFunction::class, + + // Aggregate functions + 'min' => Functions\MinFunction::class, + 'max' => Functions\MaxFunction::class, + 'avg' => Functions\AvgFunction::class, + 'sum' => Functions\SumFunction::class, + 'count' => Functions\CountFunction::class, + ]; + + /** + * @readonly Maps BUILT-IN datetime function names to AST class names. + * @psalm-var array> + */ + private static array $datetimeFunctions = [ + 'current_date' => Functions\CurrentDateFunction::class, + 'current_time' => Functions\CurrentTimeFunction::class, + 'current_timestamp' => Functions\CurrentTimestampFunction::class, + 'date_add' => Functions\DateAddFunction::class, + 'date_sub' => Functions\DateSubFunction::class, + ]; + + /* + * Expressions that were encountered during parsing of identifiers and expressions + * and still need to be validated. + */ + + /** @psalm-var list */ + private array $deferredIdentificationVariables = []; + + /** @psalm-var list */ + private array $deferredPathExpressions = []; + + /** @psalm-var list */ + private array $deferredResultVariables = []; + + /** @psalm-var list */ + private array $deferredNewObjectExpressions = []; + + /** + * The lexer. + */ + private readonly Lexer $lexer; + + /** + * The parser result. + */ + private readonly ParserResult $parserResult; + + /** + * The EntityManager. + */ + private readonly EntityManagerInterface $em; + + /** + * Map of declared query components in the parsed query. + * + * @psalm-var array + */ + private array $queryComponents = []; + + /** + * Keeps the nesting level of defined ResultVariables. + */ + private int $nestingLevel = 0; + + /** + * Any additional custom tree walkers that modify the AST. + * + * @psalm-var list> + */ + private array $customTreeWalkers = []; + + /** + * The custom last tree walker, if any, that is responsible for producing the output. + * + * @var class-string|null + */ + private $customOutputWalker; + + /** @psalm-var array */ + private array $identVariableExpressions = []; + + /** + * Creates a new query parser object. + * + * @param Query $query The Query to parse. + */ + public function __construct(private readonly Query $query) + { + $this->em = $query->getEntityManager(); + $this->lexer = new Lexer((string) $query->getDQL()); + $this->parserResult = new ParserResult(); + } + + /** + * Sets a custom tree walker that produces output. + * This tree walker will be run last over the AST, after any other walkers. + * + * @psalm-param class-string $className + */ + public function setCustomOutputTreeWalker(string $className): void + { + $this->customOutputWalker = $className; + } + + /** + * Adds a custom tree walker for modifying the AST. + * + * @psalm-param class-string $className + */ + public function addCustomTreeWalker(string $className): void + { + $this->customTreeWalkers[] = $className; + } + + /** + * Gets the lexer used by the parser. + */ + public function getLexer(): Lexer + { + return $this->lexer; + } + + /** + * Gets the ParserResult that is being filled with information during parsing. + */ + public function getParserResult(): ParserResult + { + return $this->parserResult; + } + + /** + * Gets the EntityManager used by the parser. + */ + public function getEntityManager(): EntityManagerInterface + { + return $this->em; + } + + /** + * Parses and builds AST for the given Query. + */ + public function getAST(): AST\SelectStatement|AST\UpdateStatement|AST\DeleteStatement + { + // Parse & build AST + $AST = $this->QueryLanguage(); + + // Process any deferred validations of some nodes in the AST. + // This also allows post-processing of the AST for modification purposes. + $this->processDeferredIdentificationVariables(); + + if ($this->deferredPathExpressions) { + $this->processDeferredPathExpressions(); + } + + if ($this->deferredResultVariables) { + $this->processDeferredResultVariables(); + } + + if ($this->deferredNewObjectExpressions) { + $this->processDeferredNewObjectExpressions($AST); + } + + $this->processRootEntityAliasSelected(); + + // TODO: Is there a way to remove this? It may impact the mixed hydration resultset a lot! + $this->fixIdentificationVariableOrder($AST); + + return $AST; + } + + /** + * Attempts to match the given token with the current lookahead token. + * + * If they match, updates the lookahead token; otherwise raises a syntax + * error. + * + * @throws QueryException If the tokens don't match. + */ + public function match(TokenType $token): void + { + $lookaheadType = $this->lexer->lookahead->type ?? null; + + // Short-circuit on first condition, usually types match + if ($lookaheadType === $token) { + $this->lexer->moveNext(); + + return; + } + + // If parameter is not identifier (1-99) must be exact match + if ($token->value < TokenType::T_IDENTIFIER->value) { + $this->syntaxError($this->lexer->getLiteral($token)); + } + + // If parameter is keyword (200+) must be exact match + if ($token->value > TokenType::T_IDENTIFIER->value) { + $this->syntaxError($this->lexer->getLiteral($token)); + } + + // If parameter is T_IDENTIFIER, then matches T_IDENTIFIER (100) and keywords (200+) + if ($token->value === TokenType::T_IDENTIFIER->value && $lookaheadType->value < TokenType::T_IDENTIFIER->value) { + $this->syntaxError($this->lexer->getLiteral($token)); + } + + $this->lexer->moveNext(); + } + + /** + * Frees this parser, enabling it to be reused. + * + * @param bool $deep Whether to clean peek and reset errors. + * @param int $position Position to reset. + */ + public function free(bool $deep = false, int $position = 0): void + { + // WARNING! Use this method with care. It resets the scanner! + $this->lexer->resetPosition($position); + + // Deep = true cleans peek and also any previously defined errors + if ($deep) { + $this->lexer->resetPeek(); + } + + $this->lexer->token = null; + $this->lexer->lookahead = null; + } + + /** + * Parses a query string. + */ + public function parse(): ParserResult + { + $AST = $this->getAST(); + + $customWalkers = $this->query->getHint(Query::HINT_CUSTOM_TREE_WALKERS); + if ($customWalkers !== false) { + $this->customTreeWalkers = $customWalkers; + } + + $customOutputWalker = $this->query->getHint(Query::HINT_CUSTOM_OUTPUT_WALKER); + if ($customOutputWalker !== false) { + $this->customOutputWalker = $customOutputWalker; + } + + // Run any custom tree walkers over the AST + if ($this->customTreeWalkers) { + $treeWalkerChain = new TreeWalkerChain($this->query, $this->parserResult, $this->queryComponents); + + foreach ($this->customTreeWalkers as $walker) { + $treeWalkerChain->addTreeWalker($walker); + } + + match (true) { + $AST instanceof AST\UpdateStatement => $treeWalkerChain->walkUpdateStatement($AST), + $AST instanceof AST\DeleteStatement => $treeWalkerChain->walkDeleteStatement($AST), + $AST instanceof AST\SelectStatement => $treeWalkerChain->walkSelectStatement($AST), + }; + + $this->queryComponents = $treeWalkerChain->getQueryComponents(); + } + + $outputWalkerClass = $this->customOutputWalker ?: SqlWalker::class; + $outputWalker = new $outputWalkerClass($this->query, $this->parserResult, $this->queryComponents); + + // Assign an SQL executor to the parser result + $this->parserResult->setSqlExecutor($outputWalker->getExecutor($AST)); + + return $this->parserResult; + } + + /** + * Fixes order of identification variables. + * + * They have to appear in the select clause in the same order as the + * declarations (from ... x join ... y join ... z ...) appear in the query + * as the hydration process relies on that order for proper operation. + */ + private function fixIdentificationVariableOrder(AST\SelectStatement|AST\DeleteStatement|AST\UpdateStatement $AST): void + { + if (count($this->identVariableExpressions) <= 1) { + return; + } + + assert($AST instanceof AST\SelectStatement); + + foreach ($this->queryComponents as $dqlAlias => $qComp) { + if (! isset($this->identVariableExpressions[$dqlAlias])) { + continue; + } + + $expr = $this->identVariableExpressions[$dqlAlias]; + $key = array_search($expr, $AST->selectClause->selectExpressions, true); + + unset($AST->selectClause->selectExpressions[$key]); + + $AST->selectClause->selectExpressions[] = $expr; + } + } + + /** + * Generates a new syntax error. + * + * @param string $expected Expected string. + * @param DqlToken|null $token Got token. + * + * @throws QueryException + */ + public function syntaxError(string $expected = '', Token|null $token = null): never + { + if ($token === null) { + $token = $this->lexer->lookahead; + } + + $tokenPos = $token->position ?? '-1'; + + $message = sprintf('line 0, col %d: Error: ', $tokenPos); + $message .= $expected !== '' ? sprintf('Expected %s, got ', $expected) : 'Unexpected '; + $message .= $this->lexer->lookahead === null ? 'end of string.' : sprintf("'%s'", $token->value); + + throw QueryException::syntaxError($message, QueryException::dqlError($this->query->getDQL() ?? '')); + } + + /** + * Generates a new semantical error. + * + * @param string $message Optional message. + * @psalm-param DqlToken|null $token + * + * @throws QueryException + */ + public function semanticalError(string $message = '', Token|null $token = null): never + { + if ($token === null) { + $token = $this->lexer->lookahead ?? new Token('fake token', 42, 0); + } + + // Minimum exposed chars ahead of token + $distance = 12; + + // Find a position of a final word to display in error string + $dql = $this->query->getDQL(); + $length = strlen($dql); + $pos = $token->position + $distance; + $pos = strpos($dql, ' ', $length > $pos ? $pos : $length); + $length = $pos !== false ? $pos - $token->position : $distance; + + $tokenPos = $token->position > 0 ? $token->position : '-1'; + $tokenStr = substr($dql, $token->position, $length); + + // Building informative message + $message = 'line 0, col ' . $tokenPos . " near '" . $tokenStr . "': Error: " . $message; + + throw QueryException::semanticalError($message, QueryException::dqlError($this->query->getDQL())); + } + + /** + * Peeks beyond the matched closing parenthesis and returns the first token after that one. + * + * @param bool $resetPeek Reset peek after finding the closing parenthesis. + * + * @psalm-return DqlToken|null + */ + private function peekBeyondClosingParenthesis(bool $resetPeek = true): Token|null + { + $token = $this->lexer->peek(); + $numUnmatched = 1; + + while ($numUnmatched > 0 && $token !== null) { + switch ($token->type) { + case TokenType::T_OPEN_PARENTHESIS: + ++$numUnmatched; + break; + + case TokenType::T_CLOSE_PARENTHESIS: + --$numUnmatched; + break; + + default: + // Do nothing + } + + $token = $this->lexer->peek(); + } + + if ($resetPeek) { + $this->lexer->resetPeek(); + } + + return $token; + } + + /** + * Checks if the given token indicates a mathematical operator. + * + * @psalm-param DqlToken|null $token + */ + private function isMathOperator(Token|null $token): bool + { + return $token !== null && in_array($token->type, [TokenType::T_PLUS, TokenType::T_MINUS, TokenType::T_DIVIDE, TokenType::T_MULTIPLY], true); + } + + /** + * Checks if the next-next (after lookahead) token starts a function. + * + * @return bool TRUE if the next-next tokens start a function, FALSE otherwise. + */ + private function isFunction(): bool + { + assert($this->lexer->lookahead !== null); + $lookaheadType = $this->lexer->lookahead->type; + $peek = $this->lexer->peek(); + + $this->lexer->resetPeek(); + + return $lookaheadType->value >= TokenType::T_IDENTIFIER->value && $peek !== null && $peek->type === TokenType::T_OPEN_PARENTHESIS; + } + + /** + * Checks whether the given token type indicates an aggregate function. + * + * @return bool TRUE if the token type is an aggregate function, FALSE otherwise. + */ + private function isAggregateFunction(TokenType $tokenType): bool + { + return in_array( + $tokenType, + [TokenType::T_AVG, TokenType::T_MIN, TokenType::T_MAX, TokenType::T_SUM, TokenType::T_COUNT], + true, + ); + } + + /** + * Checks whether the current lookahead token of the lexer has the type T_ALL, T_ANY or T_SOME. + */ + private function isNextAllAnySome(): bool + { + assert($this->lexer->lookahead !== null); + + return in_array( + $this->lexer->lookahead->type, + [TokenType::T_ALL, TokenType::T_ANY, TokenType::T_SOME], + true, + ); + } + + /** + * Validates that the given IdentificationVariable is semantically correct. + * It must exist in query components list. + */ + private function processDeferredIdentificationVariables(): void + { + foreach ($this->deferredIdentificationVariables as $deferredItem) { + $identVariable = $deferredItem['expression']; + + // Check if IdentificationVariable exists in queryComponents + if (! isset($this->queryComponents[$identVariable])) { + $this->semanticalError( + sprintf("'%s' is not defined.", $identVariable), + $deferredItem['token'], + ); + } + + $qComp = $this->queryComponents[$identVariable]; + + // Check if queryComponent points to an AbstractSchemaName or a ResultVariable + if (! isset($qComp['metadata'])) { + $this->semanticalError( + sprintf("'%s' does not point to a Class.", $identVariable), + $deferredItem['token'], + ); + } + + // Validate if identification variable nesting level is lower or equal than the current one + if ($qComp['nestingLevel'] > $deferredItem['nestingLevel']) { + $this->semanticalError( + sprintf("'%s' is used outside the scope of its declaration.", $identVariable), + $deferredItem['token'], + ); + } + } + } + + /** + * Validates that the given NewObjectExpression. + */ + private function processDeferredNewObjectExpressions(AST\SelectStatement $AST): void + { + foreach ($this->deferredNewObjectExpressions as $deferredItem) { + $expression = $deferredItem['expression']; + $token = $deferredItem['token']; + $className = $expression->className; + $args = $expression->args; + $fromClassName = $AST->fromClause->identificationVariableDeclarations[0]->rangeVariableDeclaration->abstractSchemaName ?? null; + + // If the namespace is not given then assumes the first FROM entity namespace + if (! str_contains($className, '\\') && ! class_exists($className) && is_string($fromClassName) && str_contains($fromClassName, '\\')) { + $namespace = substr($fromClassName, 0, strrpos($fromClassName, '\\')); + $fqcn = $namespace . '\\' . $className; + + if (class_exists($fqcn)) { + $expression->className = $fqcn; + $className = $fqcn; + } + } + + if (! class_exists($className)) { + $this->semanticalError(sprintf('Class "%s" is not defined.', $className), $token); + } + + $class = new ReflectionClass($className); + + if (! $class->isInstantiable()) { + $this->semanticalError(sprintf('Class "%s" can not be instantiated.', $className), $token); + } + + if ($class->getConstructor() === null) { + $this->semanticalError(sprintf('Class "%s" has not a valid constructor.', $className), $token); + } + + if ($class->getConstructor()->getNumberOfRequiredParameters() > count($args)) { + $this->semanticalError(sprintf('Number of arguments does not match with "%s" constructor declaration.', $className), $token); + } + } + } + + /** + * Validates that the given ResultVariable is semantically correct. + * It must exist in query components list. + */ + private function processDeferredResultVariables(): void + { + foreach ($this->deferredResultVariables as $deferredItem) { + $resultVariable = $deferredItem['expression']; + + // Check if ResultVariable exists in queryComponents + if (! isset($this->queryComponents[$resultVariable])) { + $this->semanticalError( + sprintf("'%s' is not defined.", $resultVariable), + $deferredItem['token'], + ); + } + + $qComp = $this->queryComponents[$resultVariable]; + + // Check if queryComponent points to an AbstractSchemaName or a ResultVariable + if (! isset($qComp['resultVariable'])) { + $this->semanticalError( + sprintf("'%s' does not point to a ResultVariable.", $resultVariable), + $deferredItem['token'], + ); + } + + // Validate if identification variable nesting level is lower or equal than the current one + if ($qComp['nestingLevel'] > $deferredItem['nestingLevel']) { + $this->semanticalError( + sprintf("'%s' is used outside the scope of its declaration.", $resultVariable), + $deferredItem['token'], + ); + } + } + } + + /** + * Validates that the given PathExpression is semantically correct for grammar rules: + * + * AssociationPathExpression ::= CollectionValuedPathExpression | SingleValuedAssociationPathExpression + * SingleValuedPathExpression ::= StateFieldPathExpression | SingleValuedAssociationPathExpression + * StateFieldPathExpression ::= IdentificationVariable "." StateField + * SingleValuedAssociationPathExpression ::= IdentificationVariable "." SingleValuedAssociationField + * CollectionValuedPathExpression ::= IdentificationVariable "." CollectionValuedAssociationField + */ + private function processDeferredPathExpressions(): void + { + foreach ($this->deferredPathExpressions as $deferredItem) { + $pathExpression = $deferredItem['expression']; + + $class = $this->getMetadataForDqlAlias($pathExpression->identificationVariable); + + $field = $pathExpression->field; + if ($field === null) { + $field = $pathExpression->field = $class->identifier[0]; + } + + // Check if field or association exists + if (! isset($class->associationMappings[$field]) && ! isset($class->fieldMappings[$field])) { + $this->semanticalError( + 'Class ' . $class->name . ' has no field or association named ' . $field, + $deferredItem['token'], + ); + } + + $fieldType = AST\PathExpression::TYPE_STATE_FIELD; + + if (isset($class->associationMappings[$field])) { + $assoc = $class->associationMappings[$field]; + + $fieldType = $assoc->isToOne() + ? AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION + : AST\PathExpression::TYPE_COLLECTION_VALUED_ASSOCIATION; + } + + // Validate if PathExpression is one of the expected types + $expectedType = $pathExpression->expectedType; + + if (! ($expectedType & $fieldType)) { + // We need to recognize which was expected type(s) + $expectedStringTypes = []; + + // Validate state field type + if ($expectedType & AST\PathExpression::TYPE_STATE_FIELD) { + $expectedStringTypes[] = 'StateFieldPathExpression'; + } + + // Validate single valued association (*-to-one) + if ($expectedType & AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION) { + $expectedStringTypes[] = 'SingleValuedAssociationField'; + } + + // Validate single valued association (*-to-many) + if ($expectedType & AST\PathExpression::TYPE_COLLECTION_VALUED_ASSOCIATION) { + $expectedStringTypes[] = 'CollectionValuedAssociationField'; + } + + // Build the error message + $semanticalError = 'Invalid PathExpression. '; + $semanticalError .= count($expectedStringTypes) === 1 + ? 'Must be a ' . $expectedStringTypes[0] . '.' + : implode(' or ', $expectedStringTypes) . ' expected.'; + + $this->semanticalError($semanticalError, $deferredItem['token']); + } + + // We need to force the type in PathExpression + $pathExpression->type = $fieldType; + } + } + + private function processRootEntityAliasSelected(): void + { + if (! count($this->identVariableExpressions)) { + return; + } + + foreach ($this->identVariableExpressions as $dqlAlias => $expr) { + if (isset($this->queryComponents[$dqlAlias]) && ! isset($this->queryComponents[$dqlAlias]['parent'])) { + return; + } + } + + $this->semanticalError('Cannot select entity through identification variables without choosing at least one root entity alias.'); + } + + /** + * QueryLanguage ::= SelectStatement | UpdateStatement | DeleteStatement + */ + public function QueryLanguage(): AST\SelectStatement|AST\UpdateStatement|AST\DeleteStatement + { + $statement = null; + + $this->lexer->moveNext(); + + switch ($this->lexer->lookahead->type ?? null) { + case TokenType::T_SELECT: + $statement = $this->SelectStatement(); + break; + + case TokenType::T_UPDATE: + $statement = $this->UpdateStatement(); + break; + + case TokenType::T_DELETE: + $statement = $this->DeleteStatement(); + break; + + default: + $this->syntaxError('SELECT, UPDATE or DELETE'); + break; + } + + // Check for end of string + if ($this->lexer->lookahead !== null) { + $this->syntaxError('end of string'); + } + + return $statement; + } + + /** + * SelectStatement ::= SelectClause FromClause [WhereClause] [GroupByClause] [HavingClause] [OrderByClause] + */ + public function SelectStatement(): AST\SelectStatement + { + $selectStatement = new AST\SelectStatement($this->SelectClause(), $this->FromClause()); + + $selectStatement->whereClause = $this->lexer->isNextToken(TokenType::T_WHERE) ? $this->WhereClause() : null; + $selectStatement->groupByClause = $this->lexer->isNextToken(TokenType::T_GROUP) ? $this->GroupByClause() : null; + $selectStatement->havingClause = $this->lexer->isNextToken(TokenType::T_HAVING) ? $this->HavingClause() : null; + $selectStatement->orderByClause = $this->lexer->isNextToken(TokenType::T_ORDER) ? $this->OrderByClause() : null; + + return $selectStatement; + } + + /** + * UpdateStatement ::= UpdateClause [WhereClause] + */ + public function UpdateStatement(): AST\UpdateStatement + { + $updateStatement = new AST\UpdateStatement($this->UpdateClause()); + + $updateStatement->whereClause = $this->lexer->isNextToken(TokenType::T_WHERE) ? $this->WhereClause() : null; + + return $updateStatement; + } + + /** + * DeleteStatement ::= DeleteClause [WhereClause] + */ + public function DeleteStatement(): AST\DeleteStatement + { + $deleteStatement = new AST\DeleteStatement($this->DeleteClause()); + + $deleteStatement->whereClause = $this->lexer->isNextToken(TokenType::T_WHERE) ? $this->WhereClause() : null; + + return $deleteStatement; + } + + /** + * IdentificationVariable ::= identifier + */ + public function IdentificationVariable(): string + { + $this->match(TokenType::T_IDENTIFIER); + + assert($this->lexer->token !== null); + $identVariable = $this->lexer->token->value; + + $this->deferredIdentificationVariables[] = [ + 'expression' => $identVariable, + 'nestingLevel' => $this->nestingLevel, + 'token' => $this->lexer->token, + ]; + + return $identVariable; + } + + /** + * AliasIdentificationVariable = identifier + */ + public function AliasIdentificationVariable(): string + { + $this->match(TokenType::T_IDENTIFIER); + + assert($this->lexer->token !== null); + $aliasIdentVariable = $this->lexer->token->value; + $exists = isset($this->queryComponents[$aliasIdentVariable]); + + if ($exists) { + $this->semanticalError( + sprintf("'%s' is already defined.", $aliasIdentVariable), + $this->lexer->token, + ); + } + + return $aliasIdentVariable; + } + + /** + * AbstractSchemaName ::= fully_qualified_name | identifier + */ + public function AbstractSchemaName(): string + { + if ($this->lexer->isNextToken(TokenType::T_FULLY_QUALIFIED_NAME)) { + $this->match(TokenType::T_FULLY_QUALIFIED_NAME); + assert($this->lexer->token !== null); + + return $this->lexer->token->value; + } + + $this->match(TokenType::T_IDENTIFIER); + assert($this->lexer->token !== null); + + return $this->lexer->token->value; + } + + /** + * Validates an AbstractSchemaName, making sure the class exists. + * + * @param string $schemaName The name to validate. + * + * @throws QueryException if the name does not exist. + */ + private function validateAbstractSchemaName(string $schemaName): void + { + assert($this->lexer->token !== null); + if (! (class_exists($schemaName, true) || interface_exists($schemaName, true))) { + $this->semanticalError( + sprintf("Class '%s' is not defined.", $schemaName), + $this->lexer->token, + ); + } + } + + /** + * AliasResultVariable ::= identifier + */ + public function AliasResultVariable(): string + { + $this->match(TokenType::T_IDENTIFIER); + + assert($this->lexer->token !== null); + $resultVariable = $this->lexer->token->value; + $exists = isset($this->queryComponents[$resultVariable]); + + if ($exists) { + $this->semanticalError( + sprintf("'%s' is already defined.", $resultVariable), + $this->lexer->token, + ); + } + + return $resultVariable; + } + + /** + * ResultVariable ::= identifier + */ + public function ResultVariable(): string + { + $this->match(TokenType::T_IDENTIFIER); + + assert($this->lexer->token !== null); + $resultVariable = $this->lexer->token->value; + + // Defer ResultVariable validation + $this->deferredResultVariables[] = [ + 'expression' => $resultVariable, + 'nestingLevel' => $this->nestingLevel, + 'token' => $this->lexer->token, + ]; + + return $resultVariable; + } + + /** + * JoinAssociationPathExpression ::= IdentificationVariable "." (CollectionValuedAssociationField | SingleValuedAssociationField) + */ + public function JoinAssociationPathExpression(): AST\JoinAssociationPathExpression + { + $identVariable = $this->IdentificationVariable(); + + if (! isset($this->queryComponents[$identVariable])) { + $this->semanticalError( + 'Identification Variable ' . $identVariable . ' used in join path expression but was not defined before.', + ); + } + + $this->match(TokenType::T_DOT); + $this->match(TokenType::T_IDENTIFIER); + + assert($this->lexer->token !== null); + $field = $this->lexer->token->value; + + // Validate association field + $class = $this->getMetadataForDqlAlias($identVariable); + + if (! $class->hasAssociation($field)) { + $this->semanticalError('Class ' . $class->name . ' has no association named ' . $field); + } + + return new AST\JoinAssociationPathExpression($identVariable, $field); + } + + /** + * Parses an arbitrary path expression and defers semantical validation + * based on expected types. + * + * PathExpression ::= IdentificationVariable {"." identifier}* + * + * @psalm-param int-mask-of $expectedTypes + */ + public function PathExpression(int $expectedTypes): AST\PathExpression + { + $identVariable = $this->IdentificationVariable(); + $field = null; + + assert($this->lexer->token !== null); + if ($this->lexer->isNextToken(TokenType::T_DOT)) { + $this->match(TokenType::T_DOT); + $this->match(TokenType::T_IDENTIFIER); + + $field = $this->lexer->token->value; + + while ($this->lexer->isNextToken(TokenType::T_DOT)) { + $this->match(TokenType::T_DOT); + $this->match(TokenType::T_IDENTIFIER); + $field .= '.' . $this->lexer->token->value; + } + } + + // Creating AST node + $pathExpr = new AST\PathExpression($expectedTypes, $identVariable, $field); + + // Defer PathExpression validation if requested to be deferred + $this->deferredPathExpressions[] = [ + 'expression' => $pathExpr, + 'nestingLevel' => $this->nestingLevel, + 'token' => $this->lexer->token, + ]; + + return $pathExpr; + } + + /** + * AssociationPathExpression ::= CollectionValuedPathExpression | SingleValuedAssociationPathExpression + */ + public function AssociationPathExpression(): AST\PathExpression + { + return $this->PathExpression( + AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION | + AST\PathExpression::TYPE_COLLECTION_VALUED_ASSOCIATION, + ); + } + + /** + * SingleValuedPathExpression ::= StateFieldPathExpression | SingleValuedAssociationPathExpression + */ + public function SingleValuedPathExpression(): AST\PathExpression + { + return $this->PathExpression( + AST\PathExpression::TYPE_STATE_FIELD | + AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION, + ); + } + + /** + * StateFieldPathExpression ::= IdentificationVariable "." StateField + */ + public function StateFieldPathExpression(): AST\PathExpression + { + return $this->PathExpression(AST\PathExpression::TYPE_STATE_FIELD); + } + + /** + * SingleValuedAssociationPathExpression ::= IdentificationVariable "." SingleValuedAssociationField + */ + public function SingleValuedAssociationPathExpression(): AST\PathExpression + { + return $this->PathExpression(AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION); + } + + /** + * CollectionValuedPathExpression ::= IdentificationVariable "." CollectionValuedAssociationField + */ + public function CollectionValuedPathExpression(): AST\PathExpression + { + return $this->PathExpression(AST\PathExpression::TYPE_COLLECTION_VALUED_ASSOCIATION); + } + + /** + * SelectClause ::= "SELECT" ["DISTINCT"] SelectExpression {"," SelectExpression} + */ + public function SelectClause(): AST\SelectClause + { + $isDistinct = false; + $this->match(TokenType::T_SELECT); + + // Check for DISTINCT + if ($this->lexer->isNextToken(TokenType::T_DISTINCT)) { + $this->match(TokenType::T_DISTINCT); + + $isDistinct = true; + } + + // Process SelectExpressions (1..N) + $selectExpressions = []; + $selectExpressions[] = $this->SelectExpression(); + + while ($this->lexer->isNextToken(TokenType::T_COMMA)) { + $this->match(TokenType::T_COMMA); + + $selectExpressions[] = $this->SelectExpression(); + } + + return new AST\SelectClause($selectExpressions, $isDistinct); + } + + /** + * SimpleSelectClause ::= "SELECT" ["DISTINCT"] SimpleSelectExpression + */ + public function SimpleSelectClause(): AST\SimpleSelectClause + { + $isDistinct = false; + $this->match(TokenType::T_SELECT); + + if ($this->lexer->isNextToken(TokenType::T_DISTINCT)) { + $this->match(TokenType::T_DISTINCT); + + $isDistinct = true; + } + + return new AST\SimpleSelectClause($this->SimpleSelectExpression(), $isDistinct); + } + + /** + * UpdateClause ::= "UPDATE" AbstractSchemaName ["AS"] AliasIdentificationVariable "SET" UpdateItem {"," UpdateItem}* + */ + public function UpdateClause(): AST\UpdateClause + { + $this->match(TokenType::T_UPDATE); + assert($this->lexer->lookahead !== null); + + $token = $this->lexer->lookahead; + $abstractSchemaName = $this->AbstractSchemaName(); + + $this->validateAbstractSchemaName($abstractSchemaName); + + if ($this->lexer->isNextToken(TokenType::T_AS)) { + $this->match(TokenType::T_AS); + } + + $aliasIdentificationVariable = $this->AliasIdentificationVariable(); + + $class = $this->em->getClassMetadata($abstractSchemaName); + + // Building queryComponent + $queryComponent = [ + 'metadata' => $class, + 'parent' => null, + 'relation' => null, + 'map' => null, + 'nestingLevel' => $this->nestingLevel, + 'token' => $token, + ]; + + $this->queryComponents[$aliasIdentificationVariable] = $queryComponent; + + $this->match(TokenType::T_SET); + + $updateItems = []; + $updateItems[] = $this->UpdateItem(); + + while ($this->lexer->isNextToken(TokenType::T_COMMA)) { + $this->match(TokenType::T_COMMA); + + $updateItems[] = $this->UpdateItem(); + } + + $updateClause = new AST\UpdateClause($abstractSchemaName, $updateItems); + $updateClause->aliasIdentificationVariable = $aliasIdentificationVariable; + + return $updateClause; + } + + /** + * DeleteClause ::= "DELETE" ["FROM"] AbstractSchemaName ["AS"] AliasIdentificationVariable + */ + public function DeleteClause(): AST\DeleteClause + { + $this->match(TokenType::T_DELETE); + + if ($this->lexer->isNextToken(TokenType::T_FROM)) { + $this->match(TokenType::T_FROM); + } + + assert($this->lexer->lookahead !== null); + $token = $this->lexer->lookahead; + $abstractSchemaName = $this->AbstractSchemaName(); + + $this->validateAbstractSchemaName($abstractSchemaName); + + $deleteClause = new AST\DeleteClause($abstractSchemaName); + + if ($this->lexer->isNextToken(TokenType::T_AS)) { + $this->match(TokenType::T_AS); + } + + $aliasIdentificationVariable = $this->lexer->isNextToken(TokenType::T_IDENTIFIER) + ? $this->AliasIdentificationVariable() + : 'alias_should_have_been_set'; + + $deleteClause->aliasIdentificationVariable = $aliasIdentificationVariable; + $class = $this->em->getClassMetadata($deleteClause->abstractSchemaName); + + // Building queryComponent + $queryComponent = [ + 'metadata' => $class, + 'parent' => null, + 'relation' => null, + 'map' => null, + 'nestingLevel' => $this->nestingLevel, + 'token' => $token, + ]; + + $this->queryComponents[$aliasIdentificationVariable] = $queryComponent; + + return $deleteClause; + } + + /** + * FromClause ::= "FROM" IdentificationVariableDeclaration {"," IdentificationVariableDeclaration}* + */ + public function FromClause(): AST\FromClause + { + $this->match(TokenType::T_FROM); + + $identificationVariableDeclarations = []; + $identificationVariableDeclarations[] = $this->IdentificationVariableDeclaration(); + + while ($this->lexer->isNextToken(TokenType::T_COMMA)) { + $this->match(TokenType::T_COMMA); + + $identificationVariableDeclarations[] = $this->IdentificationVariableDeclaration(); + } + + return new AST\FromClause($identificationVariableDeclarations); + } + + /** + * SubselectFromClause ::= "FROM" SubselectIdentificationVariableDeclaration {"," SubselectIdentificationVariableDeclaration}* + */ + public function SubselectFromClause(): AST\SubselectFromClause + { + $this->match(TokenType::T_FROM); + + $identificationVariables = []; + $identificationVariables[] = $this->SubselectIdentificationVariableDeclaration(); + + while ($this->lexer->isNextToken(TokenType::T_COMMA)) { + $this->match(TokenType::T_COMMA); + + $identificationVariables[] = $this->SubselectIdentificationVariableDeclaration(); + } + + return new AST\SubselectFromClause($identificationVariables); + } + + /** + * WhereClause ::= "WHERE" ConditionalExpression + */ + public function WhereClause(): AST\WhereClause + { + $this->match(TokenType::T_WHERE); + + return new AST\WhereClause($this->ConditionalExpression()); + } + + /** + * HavingClause ::= "HAVING" ConditionalExpression + */ + public function HavingClause(): AST\HavingClause + { + $this->match(TokenType::T_HAVING); + + return new AST\HavingClause($this->ConditionalExpression()); + } + + /** + * GroupByClause ::= "GROUP" "BY" GroupByItem {"," GroupByItem}* + */ + public function GroupByClause(): AST\GroupByClause + { + $this->match(TokenType::T_GROUP); + $this->match(TokenType::T_BY); + + $groupByItems = [$this->GroupByItem()]; + + while ($this->lexer->isNextToken(TokenType::T_COMMA)) { + $this->match(TokenType::T_COMMA); + + $groupByItems[] = $this->GroupByItem(); + } + + return new AST\GroupByClause($groupByItems); + } + + /** + * OrderByClause ::= "ORDER" "BY" OrderByItem {"," OrderByItem}* + */ + public function OrderByClause(): AST\OrderByClause + { + $this->match(TokenType::T_ORDER); + $this->match(TokenType::T_BY); + + $orderByItems = []; + $orderByItems[] = $this->OrderByItem(); + + while ($this->lexer->isNextToken(TokenType::T_COMMA)) { + $this->match(TokenType::T_COMMA); + + $orderByItems[] = $this->OrderByItem(); + } + + return new AST\OrderByClause($orderByItems); + } + + /** + * Subselect ::= SimpleSelectClause SubselectFromClause [WhereClause] [GroupByClause] [HavingClause] [OrderByClause] + */ + public function Subselect(): AST\Subselect + { + // Increase query nesting level + $this->nestingLevel++; + + $subselect = new AST\Subselect($this->SimpleSelectClause(), $this->SubselectFromClause()); + + $subselect->whereClause = $this->lexer->isNextToken(TokenType::T_WHERE) ? $this->WhereClause() : null; + $subselect->groupByClause = $this->lexer->isNextToken(TokenType::T_GROUP) ? $this->GroupByClause() : null; + $subselect->havingClause = $this->lexer->isNextToken(TokenType::T_HAVING) ? $this->HavingClause() : null; + $subselect->orderByClause = $this->lexer->isNextToken(TokenType::T_ORDER) ? $this->OrderByClause() : null; + + // Decrease query nesting level + $this->nestingLevel--; + + return $subselect; + } + + /** + * UpdateItem ::= SingleValuedPathExpression "=" NewValue + */ + public function UpdateItem(): AST\UpdateItem + { + $pathExpr = $this->SingleValuedPathExpression(); + + $this->match(TokenType::T_EQUALS); + + return new AST\UpdateItem($pathExpr, $this->NewValue()); + } + + /** + * GroupByItem ::= IdentificationVariable | ResultVariable | SingleValuedPathExpression + */ + public function GroupByItem(): string|AST\PathExpression + { + // We need to check if we are in a IdentificationVariable or SingleValuedPathExpression + $glimpse = $this->lexer->glimpse(); + + if ($glimpse !== null && $glimpse->type === TokenType::T_DOT) { + return $this->SingleValuedPathExpression(); + } + + assert($this->lexer->lookahead !== null); + // Still need to decide between IdentificationVariable or ResultVariable + $lookaheadValue = $this->lexer->lookahead->value; + + if (! isset($this->queryComponents[$lookaheadValue])) { + $this->semanticalError('Cannot group by undefined identification or result variable.'); + } + + return isset($this->queryComponents[$lookaheadValue]['metadata']) + ? $this->IdentificationVariable() + : $this->ResultVariable(); + } + + /** + * OrderByItem ::= ( + * SimpleArithmeticExpression | SingleValuedPathExpression | CaseExpression | + * ScalarExpression | ResultVariable | FunctionDeclaration + * ) ["ASC" | "DESC"] + */ + public function OrderByItem(): AST\OrderByItem + { + $this->lexer->peek(); // lookahead => '.' + $this->lexer->peek(); // lookahead => token after '.' + + $peek = $this->lexer->peek(); // lookahead => token after the token after the '.' + + $this->lexer->resetPeek(); + + $glimpse = $this->lexer->glimpse(); + + assert($this->lexer->lookahead !== null); + $expr = match (true) { + $this->isMathOperator($peek) => $this->SimpleArithmeticExpression(), + $glimpse !== null && $glimpse->type === TokenType::T_DOT => $this->SingleValuedPathExpression(), + $this->lexer->peek() && $this->isMathOperator($this->peekBeyondClosingParenthesis()) => $this->ScalarExpression(), + $this->lexer->lookahead->type === TokenType::T_CASE => $this->CaseExpression(), + $this->isFunction() => $this->FunctionDeclaration(), + default => $this->ResultVariable(), + }; + + $type = 'ASC'; + $item = new AST\OrderByItem($expr); + + switch (true) { + case $this->lexer->isNextToken(TokenType::T_DESC): + $this->match(TokenType::T_DESC); + $type = 'DESC'; + break; + + case $this->lexer->isNextToken(TokenType::T_ASC): + $this->match(TokenType::T_ASC); + break; + + default: + // Do nothing + } + + $item->type = $type; + + return $item; + } + + /** + * NewValue ::= SimpleArithmeticExpression | StringPrimary | DatetimePrimary | BooleanPrimary | + * EnumPrimary | SimpleEntityExpression | "NULL" + * + * NOTE: Since it is not possible to correctly recognize individual types, here is the full + * grammar that needs to be supported: + * + * NewValue ::= SimpleArithmeticExpression | "NULL" + * + * SimpleArithmeticExpression covers all *Primary grammar rules and also SimpleEntityExpression + */ + public function NewValue(): AST\ArithmeticExpression|AST\InputParameter|null + { + if ($this->lexer->isNextToken(TokenType::T_NULL)) { + $this->match(TokenType::T_NULL); + + return null; + } + + if ($this->lexer->isNextToken(TokenType::T_INPUT_PARAMETER)) { + $this->match(TokenType::T_INPUT_PARAMETER); + assert($this->lexer->token !== null); + + return new AST\InputParameter($this->lexer->token->value); + } + + return $this->ArithmeticExpression(); + } + + /** + * IdentificationVariableDeclaration ::= RangeVariableDeclaration [IndexBy] {Join}* + */ + public function IdentificationVariableDeclaration(): AST\IdentificationVariableDeclaration + { + $joins = []; + $rangeVariableDeclaration = $this->RangeVariableDeclaration(); + $indexBy = $this->lexer->isNextToken(TokenType::T_INDEX) + ? $this->IndexBy() + : null; + + $rangeVariableDeclaration->isRoot = true; + + while ( + $this->lexer->isNextToken(TokenType::T_LEFT) || + $this->lexer->isNextToken(TokenType::T_INNER) || + $this->lexer->isNextToken(TokenType::T_JOIN) + ) { + $joins[] = $this->Join(); + } + + return new AST\IdentificationVariableDeclaration( + $rangeVariableDeclaration, + $indexBy, + $joins, + ); + } + + /** + * SubselectIdentificationVariableDeclaration ::= IdentificationVariableDeclaration + * + * {Internal note: WARNING: Solution is harder than a bare implementation. + * Desired EBNF support: + * + * SubselectIdentificationVariableDeclaration ::= IdentificationVariableDeclaration | (AssociationPathExpression ["AS"] AliasIdentificationVariable) + * + * It demands that entire SQL generation to become programmatical. This is + * needed because association based subselect requires "WHERE" conditional + * expressions to be injected, but there is no scope to do that. Only scope + * accessible is "FROM", prohibiting an easy implementation without larger + * changes.} + */ + public function SubselectIdentificationVariableDeclaration(): AST\IdentificationVariableDeclaration + { + /* + NOT YET IMPLEMENTED! + + $glimpse = $this->lexer->glimpse(); + + if ($glimpse->type == TokenType::T_DOT) { + $associationPathExpression = $this->AssociationPathExpression(); + + if ($this->lexer->isNextToken(TokenType::T_AS)) { + $this->match(TokenType::T_AS); + } + + $aliasIdentificationVariable = $this->AliasIdentificationVariable(); + $identificationVariable = $associationPathExpression->identificationVariable; + $field = $associationPathExpression->associationField; + + $class = $this->queryComponents[$identificationVariable]['metadata']; + $targetClass = $this->em->getClassMetadata($class->associationMappings[$field]['targetEntity']); + + // Building queryComponent + $joinQueryComponent = array( + 'metadata' => $targetClass, + 'parent' => $identificationVariable, + 'relation' => $class->getAssociationMapping($field), + 'map' => null, + 'nestingLevel' => $this->nestingLevel, + 'token' => $this->lexer->lookahead + ); + + $this->queryComponents[$aliasIdentificationVariable] = $joinQueryComponent; + + return new AST\SubselectIdentificationVariableDeclaration( + $associationPathExpression, $aliasIdentificationVariable + ); + } + */ + + return $this->IdentificationVariableDeclaration(); + } + + /** + * Join ::= ["LEFT" ["OUTER"] | "INNER"] "JOIN" + * (JoinAssociationDeclaration | RangeVariableDeclaration) + * ["WITH" ConditionalExpression] + */ + public function Join(): AST\Join + { + // Check Join type + $joinType = AST\Join::JOIN_TYPE_INNER; + + switch (true) { + case $this->lexer->isNextToken(TokenType::T_LEFT): + $this->match(TokenType::T_LEFT); + + $joinType = AST\Join::JOIN_TYPE_LEFT; + + // Possible LEFT OUTER join + if ($this->lexer->isNextToken(TokenType::T_OUTER)) { + $this->match(TokenType::T_OUTER); + + $joinType = AST\Join::JOIN_TYPE_LEFTOUTER; + } + + break; + + case $this->lexer->isNextToken(TokenType::T_INNER): + $this->match(TokenType::T_INNER); + break; + + default: + // Do nothing + } + + $this->match(TokenType::T_JOIN); + + $next = $this->lexer->glimpse(); + assert($next !== null); + $joinDeclaration = $next->type === TokenType::T_DOT ? $this->JoinAssociationDeclaration() : $this->RangeVariableDeclaration(); + $adhocConditions = $this->lexer->isNextToken(TokenType::T_WITH); + $join = new AST\Join($joinType, $joinDeclaration); + + // Describe non-root join declaration + if ($joinDeclaration instanceof AST\RangeVariableDeclaration) { + $joinDeclaration->isRoot = false; + } + + // Check for ad-hoc Join conditions + if ($adhocConditions) { + $this->match(TokenType::T_WITH); + + $join->conditionalExpression = $this->ConditionalExpression(); + } + + return $join; + } + + /** + * RangeVariableDeclaration ::= AbstractSchemaName ["AS"] AliasIdentificationVariable + * + * @throws QueryException + */ + public function RangeVariableDeclaration(): AST\RangeVariableDeclaration + { + if ($this->lexer->isNextToken(TokenType::T_OPEN_PARENTHESIS) && $this->lexer->glimpse()->type === TokenType::T_SELECT) { + $this->semanticalError('Subquery is not supported here', $this->lexer->token); + } + + $abstractSchemaName = $this->AbstractSchemaName(); + + $this->validateAbstractSchemaName($abstractSchemaName); + + if ($this->lexer->isNextToken(TokenType::T_AS)) { + $this->match(TokenType::T_AS); + } + + assert($this->lexer->lookahead !== null); + $token = $this->lexer->lookahead; + $aliasIdentificationVariable = $this->AliasIdentificationVariable(); + $classMetadata = $this->em->getClassMetadata($abstractSchemaName); + + // Building queryComponent + $queryComponent = [ + 'metadata' => $classMetadata, + 'parent' => null, + 'relation' => null, + 'map' => null, + 'nestingLevel' => $this->nestingLevel, + 'token' => $token, + ]; + + $this->queryComponents[$aliasIdentificationVariable] = $queryComponent; + + return new AST\RangeVariableDeclaration($abstractSchemaName, $aliasIdentificationVariable); + } + + /** + * JoinAssociationDeclaration ::= JoinAssociationPathExpression ["AS"] AliasIdentificationVariable [IndexBy] + */ + public function JoinAssociationDeclaration(): AST\JoinAssociationDeclaration + { + $joinAssociationPathExpression = $this->JoinAssociationPathExpression(); + + if ($this->lexer->isNextToken(TokenType::T_AS)) { + $this->match(TokenType::T_AS); + } + + assert($this->lexer->lookahead !== null); + + $aliasIdentificationVariable = $this->AliasIdentificationVariable(); + $indexBy = $this->lexer->isNextToken(TokenType::T_INDEX) ? $this->IndexBy() : null; + + $identificationVariable = $joinAssociationPathExpression->identificationVariable; + $field = $joinAssociationPathExpression->associationField; + + $class = $this->getMetadataForDqlAlias($identificationVariable); + $targetClass = $this->em->getClassMetadata($class->associationMappings[$field]->targetEntity); + + // Building queryComponent + $joinQueryComponent = [ + 'metadata' => $targetClass, + 'parent' => $joinAssociationPathExpression->identificationVariable, + 'relation' => $class->getAssociationMapping($field), + 'map' => null, + 'nestingLevel' => $this->nestingLevel, + 'token' => $this->lexer->lookahead, + ]; + + $this->queryComponents[$aliasIdentificationVariable] = $joinQueryComponent; + + return new AST\JoinAssociationDeclaration($joinAssociationPathExpression, $aliasIdentificationVariable, $indexBy); + } + + /** + * NewObjectExpression ::= "NEW" AbstractSchemaName "(" NewObjectArg {"," NewObjectArg}* ")" + */ + public function NewObjectExpression(): AST\NewObjectExpression + { + $args = []; + $this->match(TokenType::T_NEW); + + $className = $this->AbstractSchemaName(); // note that this is not yet validated + $token = $this->lexer->token; + + $this->match(TokenType::T_OPEN_PARENTHESIS); + + $args[] = $this->NewObjectArg(); + + while ($this->lexer->isNextToken(TokenType::T_COMMA)) { + $this->match(TokenType::T_COMMA); + + $args[] = $this->NewObjectArg(); + } + + $this->match(TokenType::T_CLOSE_PARENTHESIS); + + $expression = new AST\NewObjectExpression($className, $args); + + // Defer NewObjectExpression validation + $this->deferredNewObjectExpressions[] = [ + 'token' => $token, + 'expression' => $expression, + 'nestingLevel' => $this->nestingLevel, + ]; + + return $expression; + } + + /** + * NewObjectArg ::= ScalarExpression | "(" Subselect ")" + */ + public function NewObjectArg(): mixed + { + assert($this->lexer->lookahead !== null); + $token = $this->lexer->lookahead; + $peek = $this->lexer->glimpse(); + + assert($peek !== null); + if ($token->type === TokenType::T_OPEN_PARENTHESIS && $peek->type === TokenType::T_SELECT) { + $this->match(TokenType::T_OPEN_PARENTHESIS); + $expression = $this->Subselect(); + $this->match(TokenType::T_CLOSE_PARENTHESIS); + + return $expression; + } + + return $this->ScalarExpression(); + } + + /** + * IndexBy ::= "INDEX" "BY" SingleValuedPathExpression + */ + public function IndexBy(): AST\IndexBy + { + $this->match(TokenType::T_INDEX); + $this->match(TokenType::T_BY); + $pathExpr = $this->SingleValuedPathExpression(); + + // Add the INDEX BY info to the query component + $this->queryComponents[$pathExpr->identificationVariable]['map'] = $pathExpr->field; + + return new AST\IndexBy($pathExpr); + } + + /** + * ScalarExpression ::= SimpleArithmeticExpression | StringPrimary | DateTimePrimary | + * StateFieldPathExpression | BooleanPrimary | CaseExpression | + * InstanceOfExpression + * + * @return mixed One of the possible expressions or subexpressions. + */ + public function ScalarExpression(): mixed + { + assert($this->lexer->token !== null); + assert($this->lexer->lookahead !== null); + $lookahead = $this->lexer->lookahead->type; + $peek = $this->lexer->glimpse(); + + switch (true) { + case $lookahead === TokenType::T_INTEGER: + case $lookahead === TokenType::T_FLOAT: + // SimpleArithmeticExpression : (- u.value ) or ( + u.value ) or ( - 1 ) or ( + 1 ) + case $lookahead === TokenType::T_MINUS: + case $lookahead === TokenType::T_PLUS: + return $this->SimpleArithmeticExpression(); + + case $lookahead === TokenType::T_STRING: + return $this->StringPrimary(); + + case $lookahead === TokenType::T_TRUE: + case $lookahead === TokenType::T_FALSE: + $this->match($lookahead); + + return new AST\Literal(AST\Literal::BOOLEAN, $this->lexer->token->value); + + case $lookahead === TokenType::T_INPUT_PARAMETER: + return match (true) { + $this->isMathOperator($peek) => $this->SimpleArithmeticExpression(), + default => $this->InputParameter(), + }; + + case $lookahead === TokenType::T_CASE: + case $lookahead === TokenType::T_COALESCE: + case $lookahead === TokenType::T_NULLIF: + // Since NULLIF and COALESCE can be identified as a function, + // we need to check these before checking for FunctionDeclaration + return $this->CaseExpression(); + + case $lookahead === TokenType::T_OPEN_PARENTHESIS: + return $this->SimpleArithmeticExpression(); + + // this check must be done before checking for a filed path expression + case $this->isFunction(): + $this->lexer->peek(); + + return match (true) { + $this->isMathOperator($this->peekBeyondClosingParenthesis()) => $this->SimpleArithmeticExpression(), + default => $this->FunctionDeclaration(), + }; + + // it is no function, so it must be a field path + case $lookahead === TokenType::T_IDENTIFIER: + $this->lexer->peek(); // lookahead => '.' + $this->lexer->peek(); // lookahead => token after '.' + $peek = $this->lexer->peek(); // lookahead => token after the token after the '.' + $this->lexer->resetPeek(); + + if ($this->isMathOperator($peek)) { + return $this->SimpleArithmeticExpression(); + } + + return $this->StateFieldPathExpression(); + + default: + $this->syntaxError(); + } + } + + /** + * CaseExpression ::= GeneralCaseExpression | SimpleCaseExpression | CoalesceExpression | NullifExpression + * GeneralCaseExpression ::= "CASE" WhenClause {WhenClause}* "ELSE" ScalarExpression "END" + * WhenClause ::= "WHEN" ConditionalExpression "THEN" ScalarExpression + * SimpleCaseExpression ::= "CASE" CaseOperand SimpleWhenClause {SimpleWhenClause}* "ELSE" ScalarExpression "END" + * CaseOperand ::= StateFieldPathExpression | TypeDiscriminator + * SimpleWhenClause ::= "WHEN" ScalarExpression "THEN" ScalarExpression + * CoalesceExpression ::= "COALESCE" "(" ScalarExpression {"," ScalarExpression}* ")" + * NullifExpression ::= "NULLIF" "(" ScalarExpression "," ScalarExpression ")" + * + * @return mixed One of the possible expressions or subexpressions. + */ + public function CaseExpression(): mixed + { + assert($this->lexer->lookahead !== null); + $lookahead = $this->lexer->lookahead->type; + + switch ($lookahead) { + case TokenType::T_NULLIF: + return $this->NullIfExpression(); + + case TokenType::T_COALESCE: + return $this->CoalesceExpression(); + + case TokenType::T_CASE: + $this->lexer->resetPeek(); + $peek = $this->lexer->peek(); + + assert($peek !== null); + if ($peek->type === TokenType::T_WHEN) { + return $this->GeneralCaseExpression(); + } + + return $this->SimpleCaseExpression(); + + default: + // Do nothing + break; + } + + $this->syntaxError(); + } + + /** + * CoalesceExpression ::= "COALESCE" "(" ScalarExpression {"," ScalarExpression}* ")" + */ + public function CoalesceExpression(): AST\CoalesceExpression + { + $this->match(TokenType::T_COALESCE); + $this->match(TokenType::T_OPEN_PARENTHESIS); + + // Process ScalarExpressions (1..N) + $scalarExpressions = []; + $scalarExpressions[] = $this->ScalarExpression(); + + while ($this->lexer->isNextToken(TokenType::T_COMMA)) { + $this->match(TokenType::T_COMMA); + + $scalarExpressions[] = $this->ScalarExpression(); + } + + $this->match(TokenType::T_CLOSE_PARENTHESIS); + + return new AST\CoalesceExpression($scalarExpressions); + } + + /** + * NullIfExpression ::= "NULLIF" "(" ScalarExpression "," ScalarExpression ")" + */ + public function NullIfExpression(): AST\NullIfExpression + { + $this->match(TokenType::T_NULLIF); + $this->match(TokenType::T_OPEN_PARENTHESIS); + + $firstExpression = $this->ScalarExpression(); + $this->match(TokenType::T_COMMA); + $secondExpression = $this->ScalarExpression(); + + $this->match(TokenType::T_CLOSE_PARENTHESIS); + + return new AST\NullIfExpression($firstExpression, $secondExpression); + } + + /** + * GeneralCaseExpression ::= "CASE" WhenClause {WhenClause}* "ELSE" ScalarExpression "END" + */ + public function GeneralCaseExpression(): AST\GeneralCaseExpression + { + $this->match(TokenType::T_CASE); + + // Process WhenClause (1..N) + $whenClauses = []; + + do { + $whenClauses[] = $this->WhenClause(); + } while ($this->lexer->isNextToken(TokenType::T_WHEN)); + + $this->match(TokenType::T_ELSE); + $scalarExpression = $this->ScalarExpression(); + $this->match(TokenType::T_END); + + return new AST\GeneralCaseExpression($whenClauses, $scalarExpression); + } + + /** + * SimpleCaseExpression ::= "CASE" CaseOperand SimpleWhenClause {SimpleWhenClause}* "ELSE" ScalarExpression "END" + * CaseOperand ::= StateFieldPathExpression | TypeDiscriminator + */ + public function SimpleCaseExpression(): AST\SimpleCaseExpression + { + $this->match(TokenType::T_CASE); + $caseOperand = $this->StateFieldPathExpression(); + + // Process SimpleWhenClause (1..N) + $simpleWhenClauses = []; + + do { + $simpleWhenClauses[] = $this->SimpleWhenClause(); + } while ($this->lexer->isNextToken(TokenType::T_WHEN)); + + $this->match(TokenType::T_ELSE); + $scalarExpression = $this->ScalarExpression(); + $this->match(TokenType::T_END); + + return new AST\SimpleCaseExpression($caseOperand, $simpleWhenClauses, $scalarExpression); + } + + /** + * WhenClause ::= "WHEN" ConditionalExpression "THEN" ScalarExpression + */ + public function WhenClause(): AST\WhenClause + { + $this->match(TokenType::T_WHEN); + $conditionalExpression = $this->ConditionalExpression(); + $this->match(TokenType::T_THEN); + + return new AST\WhenClause($conditionalExpression, $this->ScalarExpression()); + } + + /** + * SimpleWhenClause ::= "WHEN" ScalarExpression "THEN" ScalarExpression + */ + public function SimpleWhenClause(): AST\SimpleWhenClause + { + $this->match(TokenType::T_WHEN); + $conditionalExpression = $this->ScalarExpression(); + $this->match(TokenType::T_THEN); + + return new AST\SimpleWhenClause($conditionalExpression, $this->ScalarExpression()); + } + + /** + * SelectExpression ::= ( + * IdentificationVariable | ScalarExpression | AggregateExpression | FunctionDeclaration | + * "(" Subselect ")" | CaseExpression | NewObjectExpression + * ) [["AS"] ["HIDDEN"] AliasResultVariable] + */ + public function SelectExpression(): AST\SelectExpression + { + assert($this->lexer->lookahead !== null); + $expression = null; + $identVariable = null; + $peek = $this->lexer->glimpse(); + $lookaheadType = $this->lexer->lookahead->type; + assert($peek !== null); + + switch (true) { + // ScalarExpression (u.name) + case $lookaheadType === TokenType::T_IDENTIFIER && $peek->type === TokenType::T_DOT: + $expression = $this->ScalarExpression(); + break; + + // IdentificationVariable (u) + case $lookaheadType === TokenType::T_IDENTIFIER && $peek->type !== TokenType::T_OPEN_PARENTHESIS: + $expression = $identVariable = $this->IdentificationVariable(); + break; + + // CaseExpression (CASE ... or NULLIF(...) or COALESCE(...)) + case $lookaheadType === TokenType::T_CASE: + case $lookaheadType === TokenType::T_COALESCE: + case $lookaheadType === TokenType::T_NULLIF: + $expression = $this->CaseExpression(); + break; + + // DQL Function (SUM(u.value) or SUM(u.value) + 1) + case $this->isFunction(): + $this->lexer->peek(); // "(" + + $expression = match (true) { + $this->isMathOperator($this->peekBeyondClosingParenthesis()) => $this->ScalarExpression(), + default => $this->FunctionDeclaration(), + }; + + break; + + // Subselect + case $lookaheadType === TokenType::T_OPEN_PARENTHESIS && $peek->type === TokenType::T_SELECT: + $this->match(TokenType::T_OPEN_PARENTHESIS); + $expression = $this->Subselect(); + $this->match(TokenType::T_CLOSE_PARENTHESIS); + break; + + // Shortcut: ScalarExpression => SimpleArithmeticExpression + case $lookaheadType === TokenType::T_OPEN_PARENTHESIS: + case $lookaheadType === TokenType::T_INTEGER: + case $lookaheadType === TokenType::T_STRING: + case $lookaheadType === TokenType::T_FLOAT: + // SimpleArithmeticExpression : (- u.value ) or ( + u.value ) + case $lookaheadType === TokenType::T_MINUS: + case $lookaheadType === TokenType::T_PLUS: + $expression = $this->SimpleArithmeticExpression(); + break; + + // NewObjectExpression (New ClassName(id, name)) + case $lookaheadType === TokenType::T_NEW: + $expression = $this->NewObjectExpression(); + break; + + default: + $this->syntaxError( + 'IdentificationVariable | ScalarExpression | AggregateExpression | FunctionDeclaration | "(" Subselect ")" | CaseExpression', + $this->lexer->lookahead, + ); + } + + // [["AS"] ["HIDDEN"] AliasResultVariable] + $mustHaveAliasResultVariable = false; + + if ($this->lexer->isNextToken(TokenType::T_AS)) { + $this->match(TokenType::T_AS); + + $mustHaveAliasResultVariable = true; + } + + $hiddenAliasResultVariable = false; + + if ($this->lexer->isNextToken(TokenType::T_HIDDEN)) { + $this->match(TokenType::T_HIDDEN); + + $hiddenAliasResultVariable = true; + } + + $aliasResultVariable = null; + + if ($mustHaveAliasResultVariable || $this->lexer->isNextToken(TokenType::T_IDENTIFIER)) { + assert($expression instanceof AST\Node || is_string($expression)); + $token = $this->lexer->lookahead; + $aliasResultVariable = $this->AliasResultVariable(); + + // Include AliasResultVariable in query components. + $this->queryComponents[$aliasResultVariable] = [ + 'resultVariable' => $expression, + 'nestingLevel' => $this->nestingLevel, + 'token' => $token, + ]; + } + + // AST + + $expr = new AST\SelectExpression($expression, $aliasResultVariable, $hiddenAliasResultVariable); + + if ($identVariable) { + $this->identVariableExpressions[$identVariable] = $expr; + } + + return $expr; + } + + /** + * SimpleSelectExpression ::= ( + * StateFieldPathExpression | IdentificationVariable | FunctionDeclaration | + * AggregateExpression | "(" Subselect ")" | ScalarExpression + * ) [["AS"] AliasResultVariable] + */ + public function SimpleSelectExpression(): AST\SimpleSelectExpression + { + assert($this->lexer->lookahead !== null); + $peek = $this->lexer->glimpse(); + assert($peek !== null); + + switch ($this->lexer->lookahead->type) { + case TokenType::T_IDENTIFIER: + switch (true) { + case $peek->type === TokenType::T_DOT: + $expression = $this->StateFieldPathExpression(); + + return new AST\SimpleSelectExpression($expression); + + case $peek->type !== TokenType::T_OPEN_PARENTHESIS: + $expression = $this->IdentificationVariable(); + + return new AST\SimpleSelectExpression($expression); + + case $this->isFunction(): + // SUM(u.id) + COUNT(u.id) + if ($this->isMathOperator($this->peekBeyondClosingParenthesis())) { + return new AST\SimpleSelectExpression($this->ScalarExpression()); + } + + // COUNT(u.id) + if ($this->isAggregateFunction($this->lexer->lookahead->type)) { + return new AST\SimpleSelectExpression($this->AggregateExpression()); + } + + // IDENTITY(u) + return new AST\SimpleSelectExpression($this->FunctionDeclaration()); + + default: + // Do nothing + } + + break; + + case TokenType::T_OPEN_PARENTHESIS: + if ($peek->type !== TokenType::T_SELECT) { + // Shortcut: ScalarExpression => SimpleArithmeticExpression + $expression = $this->SimpleArithmeticExpression(); + + return new AST\SimpleSelectExpression($expression); + } + + // Subselect + $this->match(TokenType::T_OPEN_PARENTHESIS); + $expression = $this->Subselect(); + $this->match(TokenType::T_CLOSE_PARENTHESIS); + + return new AST\SimpleSelectExpression($expression); + + default: + // Do nothing + } + + $this->lexer->peek(); + + $expression = $this->ScalarExpression(); + $expr = new AST\SimpleSelectExpression($expression); + + if ($this->lexer->isNextToken(TokenType::T_AS)) { + $this->match(TokenType::T_AS); + } + + if ($this->lexer->isNextToken(TokenType::T_IDENTIFIER)) { + $token = $this->lexer->lookahead; + $resultVariable = $this->AliasResultVariable(); + $expr->fieldIdentificationVariable = $resultVariable; + + // Include AliasResultVariable in query components. + $this->queryComponents[$resultVariable] = [ + 'resultvariable' => $expr, + 'nestingLevel' => $this->nestingLevel, + 'token' => $token, + ]; + } + + return $expr; + } + + /** + * ConditionalExpression ::= ConditionalTerm {"OR" ConditionalTerm}* + */ + public function ConditionalExpression(): AST\ConditionalExpression|AST\ConditionalFactor|AST\ConditionalPrimary|AST\ConditionalTerm + { + $conditionalTerms = []; + $conditionalTerms[] = $this->ConditionalTerm(); + + while ($this->lexer->isNextToken(TokenType::T_OR)) { + $this->match(TokenType::T_OR); + + $conditionalTerms[] = $this->ConditionalTerm(); + } + + // Phase 1 AST optimization: Prevent AST\ConditionalExpression + // if only one AST\ConditionalTerm is defined + if (count($conditionalTerms) === 1) { + return $conditionalTerms[0]; + } + + return new AST\ConditionalExpression($conditionalTerms); + } + + /** + * ConditionalTerm ::= ConditionalFactor {"AND" ConditionalFactor}* + */ + public function ConditionalTerm(): AST\ConditionalFactor|AST\ConditionalPrimary|AST\ConditionalTerm + { + $conditionalFactors = []; + $conditionalFactors[] = $this->ConditionalFactor(); + + while ($this->lexer->isNextToken(TokenType::T_AND)) { + $this->match(TokenType::T_AND); + + $conditionalFactors[] = $this->ConditionalFactor(); + } + + // Phase 1 AST optimization: Prevent AST\ConditionalTerm + // if only one AST\ConditionalFactor is defined + if (count($conditionalFactors) === 1) { + return $conditionalFactors[0]; + } + + return new AST\ConditionalTerm($conditionalFactors); + } + + /** + * ConditionalFactor ::= ["NOT"] ConditionalPrimary + */ + public function ConditionalFactor(): AST\ConditionalFactor|AST\ConditionalPrimary + { + $not = false; + + if ($this->lexer->isNextToken(TokenType::T_NOT)) { + $this->match(TokenType::T_NOT); + + $not = true; + } + + $conditionalPrimary = $this->ConditionalPrimary(); + + // Phase 1 AST optimization: Prevent AST\ConditionalFactor + // if only one AST\ConditionalPrimary is defined + if (! $not) { + return $conditionalPrimary; + } + + return new AST\ConditionalFactor($conditionalPrimary, $not); + } + + /** + * ConditionalPrimary ::= SimpleConditionalExpression | "(" ConditionalExpression ")" + */ + public function ConditionalPrimary(): AST\ConditionalPrimary + { + $condPrimary = new AST\ConditionalPrimary(); + + if (! $this->lexer->isNextToken(TokenType::T_OPEN_PARENTHESIS)) { + $condPrimary->simpleConditionalExpression = $this->SimpleConditionalExpression(); + + return $condPrimary; + } + + // Peek beyond the matching closing parenthesis ')' + $peek = $this->peekBeyondClosingParenthesis(); + + if ( + $peek !== null && ( + in_array($peek->value, ['=', '<', '<=', '<>', '>', '>=', '!='], true) || + in_array($peek->type, [TokenType::T_NOT, TokenType::T_BETWEEN, TokenType::T_LIKE, TokenType::T_IN, TokenType::T_IS, TokenType::T_EXISTS], true) || + $this->isMathOperator($peek) + ) + ) { + $condPrimary->simpleConditionalExpression = $this->SimpleConditionalExpression(); + + return $condPrimary; + } + + $this->match(TokenType::T_OPEN_PARENTHESIS); + $condPrimary->conditionalExpression = $this->ConditionalExpression(); + $this->match(TokenType::T_CLOSE_PARENTHESIS); + + return $condPrimary; + } + + /** + * SimpleConditionalExpression ::= + * ComparisonExpression | BetweenExpression | LikeExpression | + * InExpression | NullComparisonExpression | ExistsExpression | + * EmptyCollectionComparisonExpression | CollectionMemberExpression | + * InstanceOfExpression + */ + public function SimpleConditionalExpression(): AST\ExistsExpression|AST\BetweenExpression|AST\LikeExpression|AST\InListExpression|AST\InSubselectExpression|AST\InstanceOfExpression|AST\CollectionMemberExpression|AST\NullComparisonExpression|AST\EmptyCollectionComparisonExpression|AST\ComparisonExpression + { + assert($this->lexer->lookahead !== null); + if ($this->lexer->isNextToken(TokenType::T_EXISTS)) { + return $this->ExistsExpression(); + } + + $token = $this->lexer->lookahead; + $peek = $this->lexer->glimpse(); + $lookahead = $token; + + if ($this->lexer->isNextToken(TokenType::T_NOT)) { + $token = $this->lexer->glimpse(); + } + + assert($token !== null); + assert($peek !== null); + if ($token->type === TokenType::T_IDENTIFIER || $token->type === TokenType::T_INPUT_PARAMETER || $this->isFunction()) { + // Peek beyond the matching closing parenthesis. + $beyond = $this->lexer->peek(); + + switch ($peek->value) { + case '(': + // Peeks beyond the matched closing parenthesis. + $token = $this->peekBeyondClosingParenthesis(false); + assert($token !== null); + + if ($token->type === TokenType::T_NOT) { + $token = $this->lexer->peek(); + assert($token !== null); + } + + if ($token->type === TokenType::T_IS) { + $lookahead = $this->lexer->peek(); + } + + break; + + default: + // Peek beyond the PathExpression or InputParameter. + $token = $beyond; + + while ($token->value === '.') { + $this->lexer->peek(); + + $token = $this->lexer->peek(); + assert($token !== null); + } + + // Also peek beyond a NOT if there is one. + assert($token !== null); + if ($token->type === TokenType::T_NOT) { + $token = $this->lexer->peek(); + assert($token !== null); + } + + // We need to go even further in case of IS (differentiate between NULL and EMPTY) + $lookahead = $this->lexer->peek(); + } + + assert($lookahead !== null); + // Also peek beyond a NOT if there is one. + if ($lookahead->type === TokenType::T_NOT) { + $lookahead = $this->lexer->peek(); + } + + $this->lexer->resetPeek(); + } + + if ($token->type === TokenType::T_BETWEEN) { + return $this->BetweenExpression(); + } + + if ($token->type === TokenType::T_LIKE) { + return $this->LikeExpression(); + } + + if ($token->type === TokenType::T_IN) { + return $this->InExpression(); + } + + if ($token->type === TokenType::T_INSTANCE) { + return $this->InstanceOfExpression(); + } + + if ($token->type === TokenType::T_MEMBER) { + return $this->CollectionMemberExpression(); + } + + assert($lookahead !== null); + if ($token->type === TokenType::T_IS && $lookahead->type === TokenType::T_NULL) { + return $this->NullComparisonExpression(); + } + + if ($token->type === TokenType::T_IS && $lookahead->type === TokenType::T_EMPTY) { + return $this->EmptyCollectionComparisonExpression(); + } + + return $this->ComparisonExpression(); + } + + /** + * EmptyCollectionComparisonExpression ::= CollectionValuedPathExpression "IS" ["NOT"] "EMPTY" + */ + public function EmptyCollectionComparisonExpression(): AST\EmptyCollectionComparisonExpression + { + $pathExpression = $this->CollectionValuedPathExpression(); + $this->match(TokenType::T_IS); + + $not = false; + if ($this->lexer->isNextToken(TokenType::T_NOT)) { + $this->match(TokenType::T_NOT); + $not = true; + } + + $this->match(TokenType::T_EMPTY); + + return new AST\EmptyCollectionComparisonExpression( + $pathExpression, + $not, + ); + } + + /** + * CollectionMemberExpression ::= EntityExpression ["NOT"] "MEMBER" ["OF"] CollectionValuedPathExpression + * + * EntityExpression ::= SingleValuedAssociationPathExpression | SimpleEntityExpression + * SimpleEntityExpression ::= IdentificationVariable | InputParameter + */ + public function CollectionMemberExpression(): AST\CollectionMemberExpression + { + $not = false; + $entityExpr = $this->EntityExpression(); + + if ($this->lexer->isNextToken(TokenType::T_NOT)) { + $this->match(TokenType::T_NOT); + + $not = true; + } + + $this->match(TokenType::T_MEMBER); + + if ($this->lexer->isNextToken(TokenType::T_OF)) { + $this->match(TokenType::T_OF); + } + + return new AST\CollectionMemberExpression( + $entityExpr, + $this->CollectionValuedPathExpression(), + $not, + ); + } + + /** + * Literal ::= string | char | integer | float | boolean + */ + public function Literal(): AST\Literal + { + assert($this->lexer->lookahead !== null); + assert($this->lexer->token !== null); + switch ($this->lexer->lookahead->type) { + case TokenType::T_STRING: + $this->match(TokenType::T_STRING); + + return new AST\Literal(AST\Literal::STRING, $this->lexer->token->value); + + case TokenType::T_INTEGER: + case TokenType::T_FLOAT: + $this->match( + $this->lexer->isNextToken(TokenType::T_INTEGER) ? TokenType::T_INTEGER : TokenType::T_FLOAT, + ); + + return new AST\Literal(AST\Literal::NUMERIC, $this->lexer->token->value); + + case TokenType::T_TRUE: + case TokenType::T_FALSE: + $this->match( + $this->lexer->isNextToken(TokenType::T_TRUE) ? TokenType::T_TRUE : TokenType::T_FALSE, + ); + + return new AST\Literal(AST\Literal::BOOLEAN, $this->lexer->token->value); + + default: + $this->syntaxError('Literal'); + } + } + + /** + * InParameter ::= ArithmeticExpression | InputParameter + */ + public function InParameter(): AST\InputParameter|AST\ArithmeticExpression + { + assert($this->lexer->lookahead !== null); + if ($this->lexer->lookahead->type === TokenType::T_INPUT_PARAMETER) { + return $this->InputParameter(); + } + + return $this->ArithmeticExpression(); + } + + /** + * InputParameter ::= PositionalParameter | NamedParameter + */ + public function InputParameter(): AST\InputParameter + { + $this->match(TokenType::T_INPUT_PARAMETER); + assert($this->lexer->token !== null); + + return new AST\InputParameter($this->lexer->token->value); + } + + /** + * ArithmeticExpression ::= SimpleArithmeticExpression | "(" Subselect ")" + */ + public function ArithmeticExpression(): AST\ArithmeticExpression + { + $expr = new AST\ArithmeticExpression(); + + if ($this->lexer->isNextToken(TokenType::T_OPEN_PARENTHESIS)) { + $peek = $this->lexer->glimpse(); + assert($peek !== null); + + if ($peek->type === TokenType::T_SELECT) { + $this->match(TokenType::T_OPEN_PARENTHESIS); + $expr->subselect = $this->Subselect(); + $this->match(TokenType::T_CLOSE_PARENTHESIS); + + return $expr; + } + } + + $expr->simpleArithmeticExpression = $this->SimpleArithmeticExpression(); + + return $expr; + } + + /** + * SimpleArithmeticExpression ::= ArithmeticTerm {("+" | "-") ArithmeticTerm}* + */ + public function SimpleArithmeticExpression(): AST\Node|string + { + $terms = []; + $terms[] = $this->ArithmeticTerm(); + + while (($isPlus = $this->lexer->isNextToken(TokenType::T_PLUS)) || $this->lexer->isNextToken(TokenType::T_MINUS)) { + $this->match($isPlus ? TokenType::T_PLUS : TokenType::T_MINUS); + + assert($this->lexer->token !== null); + $terms[] = $this->lexer->token->value; + $terms[] = $this->ArithmeticTerm(); + } + + // Phase 1 AST optimization: Prevent AST\SimpleArithmeticExpression + // if only one AST\ArithmeticTerm is defined + if (count($terms) === 1) { + return $terms[0]; + } + + return new AST\SimpleArithmeticExpression($terms); + } + + /** + * ArithmeticTerm ::= ArithmeticFactor {("*" | "/") ArithmeticFactor}* + */ + public function ArithmeticTerm(): AST\Node|string + { + $factors = []; + $factors[] = $this->ArithmeticFactor(); + + while (($isMult = $this->lexer->isNextToken(TokenType::T_MULTIPLY)) || $this->lexer->isNextToken(TokenType::T_DIVIDE)) { + $this->match($isMult ? TokenType::T_MULTIPLY : TokenType::T_DIVIDE); + + assert($this->lexer->token !== null); + $factors[] = $this->lexer->token->value; + $factors[] = $this->ArithmeticFactor(); + } + + // Phase 1 AST optimization: Prevent AST\ArithmeticTerm + // if only one AST\ArithmeticFactor is defined + if (count($factors) === 1) { + return $factors[0]; + } + + return new AST\ArithmeticTerm($factors); + } + + /** + * ArithmeticFactor ::= [("+" | "-")] ArithmeticPrimary + */ + public function ArithmeticFactor(): AST\Node|string|AST\ArithmeticFactor + { + $sign = null; + + $isPlus = $this->lexer->isNextToken(TokenType::T_PLUS); + if ($isPlus || $this->lexer->isNextToken(TokenType::T_MINUS)) { + $this->match($isPlus ? TokenType::T_PLUS : TokenType::T_MINUS); + $sign = $isPlus; + } + + $primary = $this->ArithmeticPrimary(); + + // Phase 1 AST optimization: Prevent AST\ArithmeticFactor + // if only one AST\ArithmeticPrimary is defined + if ($sign === null) { + return $primary; + } + + return new AST\ArithmeticFactor($primary, $sign); + } + + /** + * ArithmeticPrimary ::= SingleValuedPathExpression | Literal | ParenthesisExpression + * | FunctionsReturningNumerics | AggregateExpression | FunctionsReturningStrings + * | FunctionsReturningDatetime | IdentificationVariable | ResultVariable + * | InputParameter | CaseExpression + */ + public function ArithmeticPrimary(): AST\Node|string + { + if ($this->lexer->isNextToken(TokenType::T_OPEN_PARENTHESIS)) { + $this->match(TokenType::T_OPEN_PARENTHESIS); + + $expr = $this->SimpleArithmeticExpression(); + + $this->match(TokenType::T_CLOSE_PARENTHESIS); + + return new AST\ParenthesisExpression($expr); + } + + if ($this->lexer->lookahead === null) { + $this->syntaxError('ArithmeticPrimary'); + } + + switch ($this->lexer->lookahead->type) { + case TokenType::T_COALESCE: + case TokenType::T_NULLIF: + case TokenType::T_CASE: + return $this->CaseExpression(); + + case TokenType::T_IDENTIFIER: + $peek = $this->lexer->glimpse(); + + if ($peek !== null && $peek->value === '(') { + return $this->FunctionDeclaration(); + } + + if ($peek !== null && $peek->value === '.') { + return $this->SingleValuedPathExpression(); + } + + if (isset($this->queryComponents[$this->lexer->lookahead->value]['resultVariable'])) { + return $this->ResultVariable(); + } + + return $this->StateFieldPathExpression(); + + case TokenType::T_INPUT_PARAMETER: + return $this->InputParameter(); + + default: + $peek = $this->lexer->glimpse(); + + if ($peek !== null && $peek->value === '(') { + return $this->FunctionDeclaration(); + } + + return $this->Literal(); + } + } + + /** + * StringExpression ::= StringPrimary | ResultVariable | "(" Subselect ")" + */ + public function StringExpression(): AST\Subselect|AST\Node|string + { + $peek = $this->lexer->glimpse(); + assert($peek !== null); + + // Subselect + if ($this->lexer->isNextToken(TokenType::T_OPEN_PARENTHESIS) && $peek->type === TokenType::T_SELECT) { + $this->match(TokenType::T_OPEN_PARENTHESIS); + $expr = $this->Subselect(); + $this->match(TokenType::T_CLOSE_PARENTHESIS); + + return $expr; + } + + assert($this->lexer->lookahead !== null); + // ResultVariable (string) + if ( + $this->lexer->isNextToken(TokenType::T_IDENTIFIER) && + isset($this->queryComponents[$this->lexer->lookahead->value]['resultVariable']) + ) { + return $this->ResultVariable(); + } + + return $this->StringPrimary(); + } + + /** + * StringPrimary ::= StateFieldPathExpression | string | InputParameter | FunctionsReturningStrings | AggregateExpression | CaseExpression + */ + public function StringPrimary(): AST\Node + { + assert($this->lexer->lookahead !== null); + $lookaheadType = $this->lexer->lookahead->type; + + switch ($lookaheadType) { + case TokenType::T_IDENTIFIER: + $peek = $this->lexer->glimpse(); + assert($peek !== null); + + if ($peek->value === '.') { + return $this->StateFieldPathExpression(); + } + + if ($peek->value === '(') { + // do NOT directly go to FunctionsReturningString() because it doesn't check for custom functions. + return $this->FunctionDeclaration(); + } + + $this->syntaxError("'.' or '('"); + break; + + case TokenType::T_STRING: + $this->match(TokenType::T_STRING); + assert($this->lexer->token !== null); + + return new AST\Literal(AST\Literal::STRING, $this->lexer->token->value); + + case TokenType::T_INPUT_PARAMETER: + return $this->InputParameter(); + + case TokenType::T_CASE: + case TokenType::T_COALESCE: + case TokenType::T_NULLIF: + return $this->CaseExpression(); + + default: + assert($lookaheadType !== null); + if ($this->isAggregateFunction($lookaheadType)) { + return $this->AggregateExpression(); + } + } + + $this->syntaxError( + 'StateFieldPathExpression | string | InputParameter | FunctionsReturningStrings | AggregateExpression', + ); + } + + /** + * EntityExpression ::= SingleValuedAssociationPathExpression | SimpleEntityExpression + */ + public function EntityExpression(): AST\InputParameter|AST\PathExpression + { + $glimpse = $this->lexer->glimpse(); + assert($glimpse !== null); + + if ($this->lexer->isNextToken(TokenType::T_IDENTIFIER) && $glimpse->value === '.') { + return $this->SingleValuedAssociationPathExpression(); + } + + return $this->SimpleEntityExpression(); + } + + /** + * SimpleEntityExpression ::= IdentificationVariable | InputParameter + */ + public function SimpleEntityExpression(): AST\InputParameter|AST\PathExpression + { + if ($this->lexer->isNextToken(TokenType::T_INPUT_PARAMETER)) { + return $this->InputParameter(); + } + + return $this->StateFieldPathExpression(); + } + + /** + * AggregateExpression ::= + * ("AVG" | "MAX" | "MIN" | "SUM" | "COUNT") "(" ["DISTINCT"] SimpleArithmeticExpression ")" + */ + public function AggregateExpression(): AST\AggregateExpression + { + assert($this->lexer->lookahead !== null); + $lookaheadType = $this->lexer->lookahead->type; + $isDistinct = false; + + if (! in_array($lookaheadType, [TokenType::T_COUNT, TokenType::T_AVG, TokenType::T_MAX, TokenType::T_MIN, TokenType::T_SUM], true)) { + $this->syntaxError('One of: MAX, MIN, AVG, SUM, COUNT'); + } + + $this->match($lookaheadType); + assert($this->lexer->token !== null); + $functionName = $this->lexer->token->value; + $this->match(TokenType::T_OPEN_PARENTHESIS); + + if ($this->lexer->isNextToken(TokenType::T_DISTINCT)) { + $this->match(TokenType::T_DISTINCT); + $isDistinct = true; + } + + $pathExp = $this->SimpleArithmeticExpression(); + + $this->match(TokenType::T_CLOSE_PARENTHESIS); + + return new AST\AggregateExpression($functionName, $pathExp, $isDistinct); + } + + /** + * QuantifiedExpression ::= ("ALL" | "ANY" | "SOME") "(" Subselect ")" + */ + public function QuantifiedExpression(): AST\QuantifiedExpression + { + assert($this->lexer->lookahead !== null); + $lookaheadType = $this->lexer->lookahead->type; + $value = $this->lexer->lookahead->value; + + if (! in_array($lookaheadType, [TokenType::T_ALL, TokenType::T_ANY, TokenType::T_SOME], true)) { + $this->syntaxError('ALL, ANY or SOME'); + } + + $this->match($lookaheadType); + $this->match(TokenType::T_OPEN_PARENTHESIS); + + $qExpr = new AST\QuantifiedExpression($this->Subselect()); + $qExpr->type = $value; + + $this->match(TokenType::T_CLOSE_PARENTHESIS); + + return $qExpr; + } + + /** + * BetweenExpression ::= ArithmeticExpression ["NOT"] "BETWEEN" ArithmeticExpression "AND" ArithmeticExpression + */ + public function BetweenExpression(): AST\BetweenExpression + { + $not = false; + $arithExpr1 = $this->ArithmeticExpression(); + + if ($this->lexer->isNextToken(TokenType::T_NOT)) { + $this->match(TokenType::T_NOT); + $not = true; + } + + $this->match(TokenType::T_BETWEEN); + $arithExpr2 = $this->ArithmeticExpression(); + $this->match(TokenType::T_AND); + $arithExpr3 = $this->ArithmeticExpression(); + + return new AST\BetweenExpression($arithExpr1, $arithExpr2, $arithExpr3, $not); + } + + /** + * ComparisonExpression ::= ArithmeticExpression ComparisonOperator ( QuantifiedExpression | ArithmeticExpression ) + */ + public function ComparisonExpression(): AST\ComparisonExpression + { + $this->lexer->glimpse(); + + $leftExpr = $this->ArithmeticExpression(); + $operator = $this->ComparisonOperator(); + $rightExpr = $this->isNextAllAnySome() + ? $this->QuantifiedExpression() + : $this->ArithmeticExpression(); + + return new AST\ComparisonExpression($leftExpr, $operator, $rightExpr); + } + + /** + * InExpression ::= SingleValuedPathExpression ["NOT"] "IN" "(" (InParameter {"," InParameter}* | Subselect) ")" + */ + public function InExpression(): AST\InListExpression|AST\InSubselectExpression + { + $expression = $this->ArithmeticExpression(); + + $not = false; + if ($this->lexer->isNextToken(TokenType::T_NOT)) { + $this->match(TokenType::T_NOT); + $not = true; + } + + $this->match(TokenType::T_IN); + $this->match(TokenType::T_OPEN_PARENTHESIS); + + if ($this->lexer->isNextToken(TokenType::T_SELECT)) { + $inExpression = new AST\InSubselectExpression( + $expression, + $this->Subselect(), + $not, + ); + } else { + $literals = [$this->InParameter()]; + + while ($this->lexer->isNextToken(TokenType::T_COMMA)) { + $this->match(TokenType::T_COMMA); + $literals[] = $this->InParameter(); + } + + $inExpression = new AST\InListExpression( + $expression, + $literals, + $not, + ); + } + + $this->match(TokenType::T_CLOSE_PARENTHESIS); + + return $inExpression; + } + + /** + * InstanceOfExpression ::= IdentificationVariable ["NOT"] "INSTANCE" ["OF"] (InstanceOfParameter | "(" InstanceOfParameter {"," InstanceOfParameter}* ")") + */ + public function InstanceOfExpression(): AST\InstanceOfExpression + { + $identificationVariable = $this->IdentificationVariable(); + + $not = false; + if ($this->lexer->isNextToken(TokenType::T_NOT)) { + $this->match(TokenType::T_NOT); + $not = true; + } + + $this->match(TokenType::T_INSTANCE); + $this->match(TokenType::T_OF); + + $exprValues = $this->lexer->isNextToken(TokenType::T_OPEN_PARENTHESIS) + ? $this->InstanceOfParameterList() + : [$this->InstanceOfParameter()]; + + return new AST\InstanceOfExpression( + $identificationVariable, + $exprValues, + $not, + ); + } + + /** @return non-empty-list */ + public function InstanceOfParameterList(): array + { + $this->match(TokenType::T_OPEN_PARENTHESIS); + + $exprValues = [$this->InstanceOfParameter()]; + + while ($this->lexer->isNextToken(TokenType::T_COMMA)) { + $this->match(TokenType::T_COMMA); + + $exprValues[] = $this->InstanceOfParameter(); + } + + $this->match(TokenType::T_CLOSE_PARENTHESIS); + + return $exprValues; + } + + /** + * InstanceOfParameter ::= AbstractSchemaName | InputParameter + */ + public function InstanceOfParameter(): AST\InputParameter|string + { + if ($this->lexer->isNextToken(TokenType::T_INPUT_PARAMETER)) { + $this->match(TokenType::T_INPUT_PARAMETER); + assert($this->lexer->token !== null); + + return new AST\InputParameter($this->lexer->token->value); + } + + $abstractSchemaName = $this->AbstractSchemaName(); + + $this->validateAbstractSchemaName($abstractSchemaName); + + return $abstractSchemaName; + } + + /** + * LikeExpression ::= StringExpression ["NOT"] "LIKE" StringPrimary ["ESCAPE" char] + */ + public function LikeExpression(): AST\LikeExpression + { + $stringExpr = $this->StringExpression(); + $not = false; + + if ($this->lexer->isNextToken(TokenType::T_NOT)) { + $this->match(TokenType::T_NOT); + $not = true; + } + + $this->match(TokenType::T_LIKE); + + if ($this->lexer->isNextToken(TokenType::T_INPUT_PARAMETER)) { + $this->match(TokenType::T_INPUT_PARAMETER); + assert($this->lexer->token !== null); + $stringPattern = new AST\InputParameter($this->lexer->token->value); + } else { + $stringPattern = $this->StringPrimary(); + } + + $escapeChar = null; + + if ($this->lexer->lookahead !== null && $this->lexer->lookahead->type === TokenType::T_ESCAPE) { + $this->match(TokenType::T_ESCAPE); + $this->match(TokenType::T_STRING); + assert($this->lexer->token !== null); + + $escapeChar = new AST\Literal(AST\Literal::STRING, $this->lexer->token->value); + } + + return new AST\LikeExpression($stringExpr, $stringPattern, $escapeChar, $not); + } + + /** + * NullComparisonExpression ::= (InputParameter | NullIfExpression | CoalesceExpression | AggregateExpression | FunctionDeclaration | IdentificationVariable | SingleValuedPathExpression | ResultVariable) "IS" ["NOT"] "NULL" + */ + public function NullComparisonExpression(): AST\NullComparisonExpression + { + switch (true) { + case $this->lexer->isNextToken(TokenType::T_INPUT_PARAMETER): + $this->match(TokenType::T_INPUT_PARAMETER); + assert($this->lexer->token !== null); + + $expr = new AST\InputParameter($this->lexer->token->value); + break; + + case $this->lexer->isNextToken(TokenType::T_NULLIF): + $expr = $this->NullIfExpression(); + break; + + case $this->lexer->isNextToken(TokenType::T_COALESCE): + $expr = $this->CoalesceExpression(); + break; + + case $this->isFunction(): + $expr = $this->FunctionDeclaration(); + break; + + default: + // We need to check if we are in a IdentificationVariable or SingleValuedPathExpression + $glimpse = $this->lexer->glimpse(); + assert($glimpse !== null); + + if ($glimpse->type === TokenType::T_DOT) { + $expr = $this->SingleValuedPathExpression(); + + // Leave switch statement + break; + } + + assert($this->lexer->lookahead !== null); + $lookaheadValue = $this->lexer->lookahead->value; + + // Validate existing component + if (! isset($this->queryComponents[$lookaheadValue])) { + $this->semanticalError('Cannot add having condition on undefined result variable.'); + } + + // Validate SingleValuedPathExpression (ie.: "product") + if (isset($this->queryComponents[$lookaheadValue]['metadata'])) { + $expr = $this->SingleValuedPathExpression(); + break; + } + + // Validating ResultVariable + if (! isset($this->queryComponents[$lookaheadValue]['resultVariable'])) { + $this->semanticalError('Cannot add having condition on a non result variable.'); + } + + $expr = $this->ResultVariable(); + break; + } + + $this->match(TokenType::T_IS); + + $not = false; + if ($this->lexer->isNextToken(TokenType::T_NOT)) { + $this->match(TokenType::T_NOT); + + $not = true; + } + + $this->match(TokenType::T_NULL); + + return new AST\NullComparisonExpression($expr, $not); + } + + /** + * ExistsExpression ::= ["NOT"] "EXISTS" "(" Subselect ")" + */ + public function ExistsExpression(): AST\ExistsExpression + { + $not = false; + + if ($this->lexer->isNextToken(TokenType::T_NOT)) { + $this->match(TokenType::T_NOT); + $not = true; + } + + $this->match(TokenType::T_EXISTS); + $this->match(TokenType::T_OPEN_PARENTHESIS); + + $subselect = $this->Subselect(); + + $this->match(TokenType::T_CLOSE_PARENTHESIS); + + return new AST\ExistsExpression($subselect, $not); + } + + /** + * ComparisonOperator ::= "=" | "<" | "<=" | "<>" | ">" | ">=" | "!=" + */ + public function ComparisonOperator(): string + { + assert($this->lexer->lookahead !== null); + switch ($this->lexer->lookahead->value) { + case '=': + $this->match(TokenType::T_EQUALS); + + return '='; + + case '<': + $this->match(TokenType::T_LOWER_THAN); + $operator = '<'; + + if ($this->lexer->isNextToken(TokenType::T_EQUALS)) { + $this->match(TokenType::T_EQUALS); + $operator .= '='; + } elseif ($this->lexer->isNextToken(TokenType::T_GREATER_THAN)) { + $this->match(TokenType::T_GREATER_THAN); + $operator .= '>'; + } + + return $operator; + + case '>': + $this->match(TokenType::T_GREATER_THAN); + $operator = '>'; + + if ($this->lexer->isNextToken(TokenType::T_EQUALS)) { + $this->match(TokenType::T_EQUALS); + $operator .= '='; + } + + return $operator; + + case '!': + $this->match(TokenType::T_NEGATE); + $this->match(TokenType::T_EQUALS); + + return '<>'; + + default: + $this->syntaxError('=, <, <=, <>, >, >=, !='); + } + } + + /** + * FunctionDeclaration ::= FunctionsReturningStrings | FunctionsReturningNumerics | FunctionsReturningDatetime + */ + public function FunctionDeclaration(): Functions\FunctionNode + { + assert($this->lexer->lookahead !== null); + $token = $this->lexer->lookahead; + $funcName = strtolower($token->value); + + $customFunctionDeclaration = $this->CustomFunctionDeclaration(); + + // Check for custom functions functions first! + switch (true) { + case $customFunctionDeclaration !== null: + return $customFunctionDeclaration; + + case isset(self::$stringFunctions[$funcName]): + return $this->FunctionsReturningStrings(); + + case isset(self::$numericFunctions[$funcName]): + return $this->FunctionsReturningNumerics(); + + case isset(self::$datetimeFunctions[$funcName]): + return $this->FunctionsReturningDatetime(); + + default: + $this->syntaxError('known function', $token); + } + } + + /** + * Helper function for FunctionDeclaration grammar rule. + */ + private function CustomFunctionDeclaration(): Functions\FunctionNode|null + { + assert($this->lexer->lookahead !== null); + $token = $this->lexer->lookahead; + $funcName = strtolower($token->value); + + // Check for custom functions afterwards + $config = $this->em->getConfiguration(); + + return match (true) { + $config->getCustomStringFunction($funcName) !== null => $this->CustomFunctionsReturningStrings(), + $config->getCustomNumericFunction($funcName) !== null => $this->CustomFunctionsReturningNumerics(), + $config->getCustomDatetimeFunction($funcName) !== null => $this->CustomFunctionsReturningDatetime(), + default => null, + }; + } + + /** + * FunctionsReturningNumerics ::= + * "LENGTH" "(" StringPrimary ")" | + * "LOCATE" "(" StringPrimary "," StringPrimary ["," SimpleArithmeticExpression]")" | + * "ABS" "(" SimpleArithmeticExpression ")" | + * "SQRT" "(" SimpleArithmeticExpression ")" | + * "MOD" "(" SimpleArithmeticExpression "," SimpleArithmeticExpression ")" | + * "SIZE" "(" CollectionValuedPathExpression ")" | + * "DATE_DIFF" "(" ArithmeticPrimary "," ArithmeticPrimary ")" | + * "BIT_AND" "(" ArithmeticPrimary "," ArithmeticPrimary ")" | + * "BIT_OR" "(" ArithmeticPrimary "," ArithmeticPrimary ")" + */ + public function FunctionsReturningNumerics(): AST\Functions\FunctionNode + { + assert($this->lexer->lookahead !== null); + $funcNameLower = strtolower($this->lexer->lookahead->value); + $funcClass = self::$numericFunctions[$funcNameLower]; + + $function = new $funcClass($funcNameLower); + $function->parse($this); + + return $function; + } + + public function CustomFunctionsReturningNumerics(): AST\Functions\FunctionNode + { + assert($this->lexer->lookahead !== null); + // getCustomNumericFunction is case-insensitive + $functionName = strtolower($this->lexer->lookahead->value); + $functionClass = $this->em->getConfiguration()->getCustomNumericFunction($functionName); + + assert($functionClass !== null); + + $function = is_string($functionClass) + ? new $functionClass($functionName) + : $functionClass($functionName); + + $function->parse($this); + + return $function; + } + + /** + * FunctionsReturningDateTime ::= + * "CURRENT_DATE" | + * "CURRENT_TIME" | + * "CURRENT_TIMESTAMP" | + * "DATE_ADD" "(" ArithmeticPrimary "," ArithmeticPrimary "," StringPrimary ")" | + * "DATE_SUB" "(" ArithmeticPrimary "," ArithmeticPrimary "," StringPrimary ")" + */ + public function FunctionsReturningDatetime(): AST\Functions\FunctionNode + { + assert($this->lexer->lookahead !== null); + $funcNameLower = strtolower($this->lexer->lookahead->value); + $funcClass = self::$datetimeFunctions[$funcNameLower]; + + $function = new $funcClass($funcNameLower); + $function->parse($this); + + return $function; + } + + public function CustomFunctionsReturningDatetime(): AST\Functions\FunctionNode + { + assert($this->lexer->lookahead !== null); + // getCustomDatetimeFunction is case-insensitive + $functionName = $this->lexer->lookahead->value; + $functionClass = $this->em->getConfiguration()->getCustomDatetimeFunction($functionName); + + assert($functionClass !== null); + + $function = is_string($functionClass) + ? new $functionClass($functionName) + : $functionClass($functionName); + + $function->parse($this); + + return $function; + } + + /** + * FunctionsReturningStrings ::= + * "CONCAT" "(" StringPrimary "," StringPrimary {"," StringPrimary}* ")" | + * "SUBSTRING" "(" StringPrimary "," SimpleArithmeticExpression "," SimpleArithmeticExpression ")" | + * "TRIM" "(" [["LEADING" | "TRAILING" | "BOTH"] [char] "FROM"] StringPrimary ")" | + * "LOWER" "(" StringPrimary ")" | + * "UPPER" "(" StringPrimary ")" | + * "IDENTITY" "(" SingleValuedAssociationPathExpression {"," string} ")" + */ + public function FunctionsReturningStrings(): AST\Functions\FunctionNode + { + assert($this->lexer->lookahead !== null); + $funcNameLower = strtolower($this->lexer->lookahead->value); + $funcClass = self::$stringFunctions[$funcNameLower]; + + $function = new $funcClass($funcNameLower); + $function->parse($this); + + return $function; + } + + public function CustomFunctionsReturningStrings(): Functions\FunctionNode + { + assert($this->lexer->lookahead !== null); + // getCustomStringFunction is case-insensitive + $functionName = $this->lexer->lookahead->value; + $functionClass = $this->em->getConfiguration()->getCustomStringFunction($functionName); + + assert($functionClass !== null); + + $function = is_string($functionClass) + ? new $functionClass($functionName) + : $functionClass($functionName); + + $function->parse($this); + + return $function; + } + + private function getMetadataForDqlAlias(string $dqlAlias): ClassMetadata + { + if (! isset($this->queryComponents[$dqlAlias]['metadata'])) { + throw new LogicException(sprintf('No metadata for DQL alias: %s', $dqlAlias)); + } + + return $this->queryComponents[$dqlAlias]['metadata']; + } +} 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 @@ +> + */ + private array $parameterMappings = []; + + /** + * Initializes a new instance of the ParserResult class. + * The new instance is initialized with an empty ResultSetMapping. + */ + public function __construct() + { + $this->resultSetMapping = new ResultSetMapping(); + } + + /** + * Gets the ResultSetMapping for the parsed query. + * + * @return ResultSetMapping The result set mapping of the parsed query + */ + public function getResultSetMapping(): ResultSetMapping + { + return $this->resultSetMapping; + } + + /** + * Sets the ResultSetMapping of the parsed query. + */ + public function setResultSetMapping(ResultSetMapping $rsm): void + { + $this->resultSetMapping = $rsm; + } + + /** + * Sets the SQL executor that should be used for this ParserResult. + */ + public function setSqlExecutor(AbstractSqlExecutor $executor): void + { + $this->sqlExecutor = $executor; + } + + /** + * Gets the SQL executor used by this ParserResult. + */ + public function getSqlExecutor(): AbstractSqlExecutor + { + if ($this->sqlExecutor === null) { + throw new LogicException(sprintf( + 'Executor not set yet. Call %s::setSqlExecutor() first.', + self::class, + )); + } + + return $this->sqlExecutor; + } + + /** + * Adds a DQL to SQL parameter mapping. One DQL parameter name/position can map to + * several SQL parameter positions. + */ + public function addParameterMapping(string|int $dqlPosition, int $sqlPosition): void + { + $this->parameterMappings[$dqlPosition][] = $sqlPosition; + } + + /** + * Gets all DQL to SQL parameter mappings. + * + * @psalm-return array> The parameter mappings. + */ + public function getParameterMappings(): array + { + return $this->parameterMappings; + } + + /** + * Gets the SQL parameter positions for a DQL parameter name/position. + * + * @param string|int $dqlPosition The name or position of the DQL parameter. + * + * @return int[] The positions of the corresponding SQL parameters. + * @psalm-return list + */ + public function getSqlParameterPositions(string|int $dqlPosition): array + { + return $this->parameterMappings[$dqlPosition]; + } +} 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 @@ +println('(' . $name); + $this->indent++; + } + + /** + * Decreases indentation level by one and prints a closing parenthesis. + * + * This method is called after executing a production. + */ + public function endProduction(): void + { + $this->indent--; + $this->println(')'); + } + + /** + * Prints text indented with spaces depending on current indentation level. + * + * @param string $str The text. + */ + public function println(string $str): void + { + if (! $this->silent) { + echo str_repeat(' ', $this->indent), $str, "\n"; + } + } +} 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 @@ + or ? expected.'); + } + + public static function unknownParameter(string $key): self + { + return new self('Invalid parameter: token ' . $key . ' is not defined in the query.'); + } + + public static function parameterTypeMismatch(): self + { + return new self('DQL Query parameter and type numbers mismatch, but have to be exactly equal.'); + } + + public static function invalidPathExpression(PathExpression $pathExpr): self + { + return new self( + "Invalid PathExpression '" . $pathExpr->identificationVariable . '.' . $pathExpr->field . "'.", + ); + } + + public static function invalidLiteral(string|Stringable $literal): self + { + return new self("Invalid literal '" . $literal . "'"); + } + + public static function iterateWithFetchJoinCollectionNotAllowed(AssociationMapping $assoc): self + { + return new self( + 'Invalid query operation: Not allowed to iterate over fetch join collections ' . + 'in class ' . $assoc->sourceEntity . ' association ' . $assoc->fieldName, + ); + } + + /** + * @param string[] $assoc + * @psalm-param array $assoc + */ + public static function overwritingJoinConditionsNotYetSupported(array $assoc): self + { + return new self( + 'Unsupported query operation: It is not yet possible to overwrite the join ' . + 'conditions in class ' . $assoc['sourceEntityName'] . ' association ' . $assoc['fieldName'] . '. ' . + 'Use WITH to append additional join conditions to the association.', + ); + } + + public static function associationPathInverseSideNotSupported(PathExpression $pathExpr): self + { + return new self( + 'A single-valued association path expression to an inverse side is not supported in DQL queries. ' . + 'Instead of "' . $pathExpr->identificationVariable . '.' . $pathExpr->field . '" use an explicit join.', + ); + } + + public static function iterateWithFetchJoinNotAllowed(AssociationMapping $assoc): self + { + return new self( + 'Iterate with fetch join in class ' . $assoc->sourceEntity . + ' using association ' . $assoc->fieldName . ' not allowed.', + ); + } + + public static function eagerFetchJoinWithNotAllowed(string $sourceEntity, string $fieldName): self + { + return new self( + 'Associations with fetch-mode=EAGER may not be using WITH conditions in + "' . $sourceEntity . '#' . $fieldName . '".', + ); + } + + public static function iterateWithMixedResultNotAllowed(): self + { + return new self('Iterating a query with mixed results (using scalars) is not supported.'); + } + + public static function associationPathCompositeKeyNotSupported(): self + { + return new self( + 'A single-valued association path expression to an entity with a composite primary ' . + 'key is not supported. Explicitly name the components of the composite primary key ' . + 'in the query.', + ); + } + + public static function instanceOfUnrelatedClass(string $className, string $rootClass): self + { + return new self("Cannot check if a child of '" . $rootClass . "' is instanceof '" . $className . "', " . + 'inheritance hierarchy does not exists between these two classes.'); + } + + public static function invalidQueryComponent(string $dqlAlias): self + { + return new self( + "Invalid query component given for DQL alias '" . $dqlAlias . "', " . + "requires 'metadata', 'parent', 'relation', 'map', 'nestingLevel' and 'token' keys.", + ); + } +} 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 @@ + Expr\Comparison::GT, + Comparison::GTE => Expr\Comparison::GTE, + Comparison::LT => Expr\Comparison::LT, + Comparison::LTE => Expr\Comparison::LTE, + ]; + + private readonly Expr $expr; + + /** @var list */ + private array $parameters = []; + + /** @param mixed[] $queryAliases */ + public function __construct( + private readonly array $queryAliases, + ) { + $this->expr = new Expr(); + } + + /** + * Gets bound parameters. + * Filled after {@link dispach()}. + * + * @return ArrayCollection + */ + public function getParameters(): ArrayCollection + { + return new ArrayCollection($this->parameters); + } + + public function clearParameters(): void + { + $this->parameters = []; + } + + /** + * Converts Criteria expression to Query one based on static map. + */ + private static function convertComparisonOperator(string $criteriaOperator): string|null + { + return self::OPERATOR_MAP[$criteriaOperator] ?? null; + } + + public function walkCompositeExpression(CompositeExpression $expr): mixed + { + $expressionList = []; + + foreach ($expr->getExpressionList() as $child) { + $expressionList[] = $this->dispatch($child); + } + + return match ($expr->getType()) { + CompositeExpression::TYPE_AND => new Expr\Andx($expressionList), + CompositeExpression::TYPE_OR => new Expr\Orx($expressionList), + CompositeExpression::TYPE_NOT => $this->expr->not($expressionList[0]), + default => throw new RuntimeException('Unknown composite ' . $expr->getType()), + }; + } + + public function walkComparison(Comparison $comparison): mixed + { + if (! isset($this->queryAliases[0])) { + throw new QueryException('No aliases are set before invoking walkComparison().'); + } + + $field = $this->queryAliases[0] . '.' . $comparison->getField(); + + foreach ($this->queryAliases as $alias) { + if (str_starts_with($comparison->getField() . '.', $alias . '.')) { + $field = $comparison->getField(); + break; + } + } + + $parameterName = str_replace('.', '_', $comparison->getField()); + + foreach ($this->parameters as $parameter) { + if ($parameter->getName() === $parameterName) { + $parameterName .= '_' . count($this->parameters); + break; + } + } + + $parameter = new Parameter($parameterName, $this->walkValue($comparison->getValue())); + $placeholder = ':' . $parameterName; + + switch ($comparison->getOperator()) { + case Comparison::IN: + $this->parameters[] = $parameter; + + return $this->expr->in($field, $placeholder); + + case Comparison::NIN: + $this->parameters[] = $parameter; + + return $this->expr->notIn($field, $placeholder); + + case Comparison::EQ: + case Comparison::IS: + if ($this->walkValue($comparison->getValue()) === null) { + return $this->expr->isNull($field); + } + + $this->parameters[] = $parameter; + + return $this->expr->eq($field, $placeholder); + + case Comparison::NEQ: + if ($this->walkValue($comparison->getValue()) === null) { + return $this->expr->isNotNull($field); + } + + $this->parameters[] = $parameter; + + return $this->expr->neq($field, $placeholder); + + case Comparison::CONTAINS: + $parameter->setValue('%' . $parameter->getValue() . '%', $parameter->getType()); + $this->parameters[] = $parameter; + + return $this->expr->like($field, $placeholder); + + case Comparison::MEMBER_OF: + return $this->expr->isMemberOf($comparison->getField(), $comparison->getValue()->getValue()); + + case Comparison::STARTS_WITH: + $parameter->setValue($parameter->getValue() . '%', $parameter->getType()); + $this->parameters[] = $parameter; + + return $this->expr->like($field, $placeholder); + + case Comparison::ENDS_WITH: + $parameter->setValue('%' . $parameter->getValue(), $parameter->getType()); + $this->parameters[] = $parameter; + + return $this->expr->like($field, $placeholder); + + default: + $operator = self::convertComparisonOperator($comparison->getOperator()); + if ($operator) { + $this->parameters[] = $parameter; + + return new Expr\Comparison( + $field, + $operator, + $placeholder, + ); + } + + throw new RuntimeException('Unknown comparison operator: ' . $comparison->getOperator()); + } + } + + public function walkValue(Value $value): mixed + { + return $value->getValue(); + } +} 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 @@ +Users should use the public methods. + * + * @todo Think about whether the number of lookup maps can be reduced. + */ +class ResultSetMapping +{ + /** + * Whether the result is mixed (contains scalar values together with field values). + * + * @ignore + */ + public bool $isMixed = false; + + /** + * Whether the result is a select statement. + * + * @ignore + */ + public bool $isSelect = true; + + /** + * Maps alias names to class names. + * + * @ignore + * @psalm-var array + */ + public array $aliasMap = []; + + /** + * Maps alias names to related association field names. + * + * @ignore + * @psalm-var array + */ + public array $relationMap = []; + + /** + * Maps alias names to parent alias names. + * + * @ignore + * @psalm-var array + */ + public array $parentAliasMap = []; + + /** + * Maps column names in the result set to field names for each class. + * + * @ignore + * @psalm-var array + */ + public array $fieldMappings = []; + + /** + * Maps column names in the result set to the alias/field name to use in the mapped result. + * + * @ignore + * @psalm-var array + */ + public array $scalarMappings = []; + + /** + * Maps scalar columns to enums + * + * @ignore + * @psalm-var array + */ + public $enumMappings = []; + + /** + * Maps column names in the result set to the alias/field type to use in the mapped result. + * + * @ignore + * @psalm-var array + */ + public array $typeMappings = []; + + /** + * Maps entities in the result set to the alias name to use in the mapped result. + * + * @ignore + * @psalm-var array + */ + public array $entityMappings = []; + + /** + * Maps column names of meta columns (foreign keys, discriminator columns, ...) to field names. + * + * @ignore + * @psalm-var array + */ + public array $metaMappings = []; + + /** + * Maps column names in the result set to the alias they belong to. + * + * @ignore + * @psalm-var array + */ + public array $columnOwnerMap = []; + + /** + * List of columns in the result set that are used as discriminator columns. + * + * @ignore + * @psalm-var array + */ + public array $discriminatorColumns = []; + + /** + * Maps alias names to field names that should be used for indexing. + * + * @ignore + * @psalm-var array + */ + public array $indexByMap = []; + + /** + * Map from column names to class names that declare the field the column is mapped to. + * + * @ignore + * @psalm-var array + */ + public array $declaringClasses = []; + + /** + * This is necessary to hydrate derivate foreign keys correctly. + * + * @psalm-var array> + */ + public array $isIdentifierColumn = []; + + /** + * Maps column names in the result set to field names for each new object expression. + * + * @psalm-var array> + */ + public array $newObjectMappings = []; + + /** + * Maps metadata parameter names to the metadata attribute. + * + * @psalm-var array + */ + public array $metadataParameterMapping = []; + + /** + * Contains query parameter names to be resolved as discriminator values + * + * @psalm-var array + */ + public array $discriminatorParameters = []; + + /** + * Adds an entity result to this ResultSetMapping. + * + * @param string $class The class name of the entity. + * @param string $alias The alias for the class. The alias must be unique among all entity + * results or joined entity results within this ResultSetMapping. + * @param string|null $resultAlias The result alias with which the entity result should be + * placed in the result structure. + * @psalm-param class-string $class + * + * @return $this + * + * @todo Rename: addRootEntity + */ + public function addEntityResult(string $class, string $alias, string|null $resultAlias = null): static + { + $this->aliasMap[$alias] = $class; + $this->entityMappings[$alias] = $resultAlias; + + if ($resultAlias !== null) { + $this->isMixed = true; + } + + return $this; + } + + /** + * Sets a discriminator column for an entity result or joined entity result. + * The discriminator column will be used to determine the concrete class name to + * instantiate. + * + * @param string $alias The alias of the entity result or joined entity result the discriminator + * column should be used for. + * @param string $discrColumn The name of the discriminator column in the SQL result set. + * + * @return $this + * + * @todo Rename: addDiscriminatorColumn + */ + public function setDiscriminatorColumn(string $alias, string $discrColumn): static + { + $this->discriminatorColumns[$alias] = $discrColumn; + $this->columnOwnerMap[$discrColumn] = $alias; + + return $this; + } + + /** + * Sets a field to use for indexing an entity result or joined entity result. + * + * @param string $alias The alias of an entity result or joined entity result. + * @param string $fieldName The name of the field to use for indexing. + * + * @return $this + */ + public function addIndexBy(string $alias, string $fieldName): static + { + $found = false; + + foreach ([...$this->metaMappings, ...$this->fieldMappings] as $columnName => $columnFieldName) { + if (! ($columnFieldName === $fieldName && $this->columnOwnerMap[$columnName] === $alias)) { + continue; + } + + $this->addIndexByColumn($alias, $columnName); + $found = true; + + break; + } + + /* TODO: check if this exception can be put back, for now it's gone because of assumptions made by some ORM internals + if ( ! $found) { + $message = sprintf( + 'Cannot add index by for DQL alias %s and field %s without calling addFieldResult() for them before.', + $alias, + $fieldName + ); + + throw new \LogicException($message); + } + */ + + return $this; + } + + /** + * Sets to index by a scalar result column name. + * + * @return $this + */ + public function addIndexByScalar(string $resultColumnName): static + { + $this->indexByMap['scalars'] = $resultColumnName; + + return $this; + } + + /** + * Sets a column to use for indexing an entity or joined entity result by the given alias name. + * + * @return $this + */ + public function addIndexByColumn(string $alias, string $resultColumnName): static + { + $this->indexByMap[$alias] = $resultColumnName; + + return $this; + } + + /** + * Checks whether an entity result or joined entity result with a given alias has + * a field set for indexing. + * + * @todo Rename: isIndexed($alias) + */ + public function hasIndexBy(string $alias): bool + { + return isset($this->indexByMap[$alias]); + } + + /** + * Checks whether the column with the given name is mapped as a field result + * as part of an entity result or joined entity result. + * + * @param string $columnName The name of the column in the SQL result set. + * + * @todo Rename: isField + */ + public function isFieldResult(string $columnName): bool + { + return isset($this->fieldMappings[$columnName]); + } + + /** + * Adds a field to the result that belongs to an entity or joined entity. + * + * @param string $alias The alias of the root entity or joined entity to which the field belongs. + * @param string $columnName The name of the column in the SQL result set. + * @param string $fieldName The name of the field on the declaring class. + * @param string|null $declaringClass The name of the class that declares/owns the specified field. + * When $alias refers to a superclass in a mapped hierarchy but + * the field $fieldName is defined on a subclass, specify that here. + * If not specified, the field is assumed to belong to the class + * designated by $alias. + * @psalm-param class-string|null $declaringClass + * + * @return $this + * + * @todo Rename: addField + */ + public function addFieldResult(string $alias, string $columnName, string $fieldName, string|null $declaringClass = null): static + { + // column name (in result set) => field name + $this->fieldMappings[$columnName] = $fieldName; + // column name => alias of owner + $this->columnOwnerMap[$columnName] = $alias; + // field name => class name of declaring class + $this->declaringClasses[$columnName] = $declaringClass ?: $this->aliasMap[$alias]; + + if (! $this->isMixed && $this->scalarMappings) { + $this->isMixed = true; + } + + return $this; + } + + /** + * Adds a joined entity result. + * + * @param string $class The class name of the joined entity. + * @param string $alias The unique alias to use for the joined entity. + * @param string $parentAlias The alias of the entity result that is the parent of this joined result. + * @param string $relation The association field that connects the parent entity result + * with the joined entity result. + * @psalm-param class-string $class + * + * @return $this + * + * @todo Rename: addJoinedEntity + */ + public function addJoinedEntityResult(string $class, string $alias, string $parentAlias, string $relation): static + { + $this->aliasMap[$alias] = $class; + $this->parentAliasMap[$alias] = $parentAlias; + $this->relationMap[$alias] = $relation; + + return $this; + } + + /** + * Adds a scalar result mapping. + * + * @param string $columnName The name of the column in the SQL result set. + * @param string|int $alias The result alias with which the scalar result should be placed in the result structure. + * @param string $type The column type + * + * @return $this + * + * @todo Rename: addScalar + */ + public function addScalarResult(string $columnName, string|int $alias, string $type = 'string'): static + { + $this->scalarMappings[$columnName] = $alias; + $this->typeMappings[$columnName] = $type; + + if (! $this->isMixed && $this->fieldMappings) { + $this->isMixed = true; + } + + return $this; + } + + /** + * Adds a scalar result mapping. + * + * @param string $columnName The name of the column in the SQL result set. + * @param string $enumType The enum type + * + * @return $this + */ + public function addEnumResult(string $columnName, string $enumType): static + { + $this->enumMappings[$columnName] = $enumType; + + return $this; + } + + /** + * Adds a metadata parameter mappings. + */ + public function addMetadataParameterMapping(string|int $parameter, string $attribute): void + { + $this->metadataParameterMapping[$parameter] = $attribute; + } + + /** + * Checks whether a column with a given name is mapped as a scalar result. + * + * @todo Rename: isScalar + */ + public function isScalarResult(string $columnName): bool + { + return isset($this->scalarMappings[$columnName]); + } + + /** + * Gets the name of the class of an entity result or joined entity result, + * identified by the given unique alias. + * + * @psalm-return class-string + */ + public function getClassName(string $alias): string + { + return $this->aliasMap[$alias]; + } + + /** + * Gets the field alias for a column that is mapped as a scalar value. + * + * @param string $columnName The name of the column in the SQL result set. + */ + public function getScalarAlias(string $columnName): string|int + { + return $this->scalarMappings[$columnName]; + } + + /** + * Gets the name of the class that owns a field mapping for the specified column. + * + * @psalm-return class-string + */ + public function getDeclaringClass(string $columnName): string + { + return $this->declaringClasses[$columnName]; + } + + public function getRelation(string $alias): string + { + return $this->relationMap[$alias]; + } + + public function isRelation(string $alias): bool + { + return isset($this->relationMap[$alias]); + } + + /** + * Gets the alias of the class that owns a field mapping for the specified column. + */ + public function getEntityAlias(string $columnName): string + { + return $this->columnOwnerMap[$columnName]; + } + + /** + * Gets the parent alias of the given alias. + */ + public function getParentAlias(string $alias): string + { + return $this->parentAliasMap[$alias]; + } + + /** + * Checks whether the given alias has a parent alias. + */ + public function hasParentAlias(string $alias): bool + { + return isset($this->parentAliasMap[$alias]); + } + + /** + * Gets the field name for a column name. + */ + public function getFieldName(string $columnName): string + { + return $this->fieldMappings[$columnName]; + } + + /** @psalm-return array */ + public function getAliasMap(): array + { + return $this->aliasMap; + } + + /** + * Gets the number of different entities that appear in the mapped result. + * + * @psalm-return 0|positive-int + */ + public function getEntityResultCount(): int + { + return count($this->aliasMap); + } + + /** + * Checks whether this ResultSetMapping defines a mixed result. + * + * Mixed results can only occur in object and array (graph) hydration. In such a + * case a mixed result means that scalar values are mixed with objects/array in + * the result. + */ + public function isMixedResult(): bool + { + return $this->isMixed; + } + + /** + * Adds a meta column (foreign key or discriminator column) to the result set. + * + * @param string $alias The result alias with which the meta result should be placed in the result structure. + * @param string $columnName The name of the column in the SQL result set. + * @param string $fieldName The name of the field on the declaring class. + * @param string|null $type The column type + * + * @return $this + * + * @todo Make all methods of this class require all parameters and not infer anything + */ + public function addMetaResult( + string $alias, + string $columnName, + string $fieldName, + bool $isIdentifierColumn = false, + string|null $type = null, + ): static { + $this->metaMappings[$columnName] = $fieldName; + $this->columnOwnerMap[$columnName] = $alias; + + if ($isIdentifierColumn) { + $this->isIdentifierColumn[$alias][$columnName] = true; + } + + if ($type) { + $this->typeMappings[$columnName] = $type; + } + + return $this; + } +} 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 @@ + queryColumnName). + * @psalm-param class-string $class + * @psalm-param array $renamedColumns + * @psalm-param self::COLUMN_RENAMING_*|null $renameMode + */ + public function addRootEntityFromClassMetadata( + string $class, + string $alias, + array $renamedColumns = [], + int|null $renameMode = null, + ): void { + $renameMode = $renameMode ?: $this->defaultRenameMode; + $columnAliasMap = $this->getColumnAliasMap($class, $renameMode, $renamedColumns); + + $this->addEntityResult($class, $alias); + $this->addAllClassFields($class, $alias, $columnAliasMap); + } + + /** + * Adds a joined entity and all of its fields to the result set. + * + * @param string $class The class name of the joined entity. + * @param string $alias The unique alias to use for the joined entity. + * @param string $parentAlias The alias of the entity result that is the parent of this joined result. + * @param string $relation The association field that connects the parent entity result + * with the joined entity result. + * @param string[] $renamedColumns Columns that have been renamed (tableColumnName => queryColumnName). + * @psalm-param class-string $class + * @psalm-param array $renamedColumns + * @psalm-param self::COLUMN_RENAMING_*|null $renameMode + */ + public function addJoinedEntityFromClassMetadata( + string $class, + string $alias, + string $parentAlias, + string $relation, + array $renamedColumns = [], + int|null $renameMode = null, + ): void { + $renameMode = $renameMode ?: $this->defaultRenameMode; + $columnAliasMap = $this->getColumnAliasMap($class, $renameMode, $renamedColumns); + + $this->addJoinedEntityResult($class, $alias, $parentAlias, $relation); + $this->addAllClassFields($class, $alias, $columnAliasMap); + } + + /** + * Adds all fields of the given class to the result set mapping (columns and meta fields). + * + * @param string[] $columnAliasMap + * @psalm-param array $columnAliasMap + * + * @throws InvalidArgumentException + */ + protected function addAllClassFields(string $class, string $alias, array $columnAliasMap = []): void + { + $classMetadata = $this->em->getClassMetadata($class); + $platform = $this->em->getConnection()->getDatabasePlatform(); + + if (! $this->isInheritanceSupported($classMetadata)) { + throw new InvalidArgumentException('ResultSetMapping builder does not currently support your inheritance scheme.'); + } + + foreach ($classMetadata->getColumnNames() as $columnName) { + $propertyName = $classMetadata->getFieldName($columnName); + $columnAlias = $this->getSQLResultCasing($platform, $columnAliasMap[$columnName]); + + if (isset($this->fieldMappings[$columnAlias])) { + throw new InvalidArgumentException(sprintf( + "The column '%s' conflicts with another column in the mapper.", + $columnName, + )); + } + + $this->addFieldResult($alias, $columnAlias, $propertyName); + + $enumType = $classMetadata->getFieldMapping($propertyName)->enumType ?? null; + if (! empty($enumType)) { + $this->addEnumResult($columnAlias, $enumType); + } + } + + foreach ($classMetadata->associationMappings as $associationMapping) { + if ($associationMapping->isToOneOwningSide()) { + $targetClass = $this->em->getClassMetadata($associationMapping->targetEntity); + $isIdentifier = isset($associationMapping->id) && $associationMapping->id === true; + + foreach ($associationMapping->joinColumns as $joinColumn) { + $columnName = $joinColumn->name; + $columnAlias = $this->getSQLResultCasing($platform, $columnAliasMap[$columnName]); + $columnType = PersisterHelper::getTypeOfColumn($joinColumn->referencedColumnName, $targetClass, $this->em); + + if (isset($this->metaMappings[$columnAlias])) { + throw new InvalidArgumentException(sprintf( + "The column '%s' conflicts with another column in the mapper.", + $columnAlias, + )); + } + + $this->addMetaResult($alias, $columnAlias, $columnName, $isIdentifier, $columnType); + } + } + } + } + + private function isInheritanceSupported(ClassMetadata $classMetadata): bool + { + if ( + $classMetadata->isInheritanceTypeSingleTable() + && in_array($classMetadata->name, $classMetadata->discriminatorMap, true) + ) { + return true; + } + + return ! ($classMetadata->isInheritanceTypeSingleTable() || $classMetadata->isInheritanceTypeJoined()); + } + + /** + * Gets column alias for a given column. + * + * @psalm-param array $customRenameColumns + * + * @psalm-assert self::COLUMN_RENAMING_* $mode + */ + private function getColumnAlias(string $columnName, int $mode, array $customRenameColumns): string + { + return match ($mode) { + self::COLUMN_RENAMING_INCREMENT => $columnName . $this->sqlCounter++, + self::COLUMN_RENAMING_CUSTOM => $customRenameColumns[$columnName] ?? $columnName, + self::COLUMN_RENAMING_NONE => $columnName, + default => throw new InvalidArgumentException(sprintf('%d is not a valid value for $mode', $mode)), + }; + } + + /** + * Retrieves a class columns and join columns aliases that are used in the SELECT clause. + * + * This depends on the renaming mode selected by the user. + * + * @psalm-param class-string $className + * @psalm-param self::COLUMN_RENAMING_* $mode + * @psalm-param array $customRenameColumns + * + * @return string[] + * @psalm-return array + */ + private function getColumnAliasMap( + string $className, + int $mode, + array $customRenameColumns, + ): array { + if ($customRenameColumns) { // for BC with 2.2-2.3 API + $mode = self::COLUMN_RENAMING_CUSTOM; + } + + $columnAlias = []; + $class = $this->em->getClassMetadata($className); + + foreach ($class->getColumnNames() as $columnName) { + $columnAlias[$columnName] = $this->getColumnAlias($columnName, $mode, $customRenameColumns); + } + + foreach ($class->associationMappings as $associationMapping) { + if ($associationMapping->isToOneOwningSide()) { + foreach ($associationMapping->joinColumns as $joinColumn) { + $columnName = $joinColumn->name; + $columnAlias[$columnName] = $this->getColumnAlias($columnName, $mode, $customRenameColumns); + } + } + } + + return $columnAlias; + } + + /** + * Generates the Select clause from this ResultSetMappingBuilder. + * + * Works only for all the entity results. The select parts for scalar + * expressions have to be written manually. + * + * @param string[] $tableAliases + * @psalm-param array $tableAliases + */ + public function generateSelectClause(array $tableAliases = []): string + { + $sql = ''; + + foreach ($this->columnOwnerMap as $columnName => $dqlAlias) { + $tableAlias = $tableAliases[$dqlAlias] ?? $dqlAlias; + + if ($sql !== '') { + $sql .= ', '; + } + + if (isset($this->fieldMappings[$columnName])) { + $class = $this->em->getClassMetadata($this->declaringClasses[$columnName]); + $fieldName = $this->fieldMappings[$columnName]; + $classFieldMapping = $class->fieldMappings[$fieldName]; + $columnSql = $tableAlias . '.' . $classFieldMapping->columnName; + + $type = Type::getType($classFieldMapping->type); + $columnSql = $type->convertToPHPValueSQL($columnSql, $this->em->getConnection()->getDatabasePlatform()); + + $sql .= $columnSql; + } elseif (isset($this->metaMappings[$columnName])) { + $sql .= $tableAlias . '.' . $this->metaMappings[$columnName]; + } elseif (isset($this->discriminatorColumns[$dqlAlias])) { + $sql .= $tableAlias . '.' . $this->discriminatorColumns[$dqlAlias]; + } + + $sql .= ' AS ' . $columnName; + } + + return $sql; + } + + public function __toString(): string + { + return $this->generateSelectClause([]); + } +} 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 @@ +> + */ + private array $scalarResultAliasMap = []; + + /** + * Map from Table-Alias + Column-Name to OrderBy-Direction. + * + * @var array + */ + private array $orderedColumnsMap = []; + + /** + * Map from DQL-Alias + Field-Name to SQL Column Alias. + * + * @var array> + */ + private array $scalarFields = []; + + /** + * A list of classes that appear in non-scalar SelectExpressions. + * + * @psalm-var array + */ + private array $selectedClasses = []; + + /** + * The DQL alias of the root class of the currently traversed query. + * + * @psalm-var list + */ + private array $rootAliases = []; + + /** + * Flag that indicates whether to generate SQL table aliases in the SQL. + * These should only be generated for SELECT queries, not for UPDATE/DELETE. + */ + private bool $useSqlTableAliases = true; + + /** + * The database platform abstraction. + */ + private readonly AbstractPlatform $platform; + + /** + * The quote strategy. + */ + private readonly QuoteStrategy $quoteStrategy; + + /** @psalm-param array $queryComponents The query components (symbol table). */ + public function __construct( + private readonly Query $query, + private readonly ParserResult $parserResult, + private array $queryComponents, + ) { + $this->rsm = $parserResult->getResultSetMapping(); + $this->em = $query->getEntityManager(); + $this->conn = $this->em->getConnection(); + $this->platform = $this->conn->getDatabasePlatform(); + $this->quoteStrategy = $this->em->getConfiguration()->getQuoteStrategy(); + } + + /** + * Gets the Query instance used by the walker. + */ + public function getQuery(): Query + { + return $this->query; + } + + /** + * Gets the Connection used by the walker. + */ + public function getConnection(): Connection + { + return $this->conn; + } + + /** + * Gets the EntityManager used by the walker. + */ + public function getEntityManager(): EntityManagerInterface + { + return $this->em; + } + + /** + * Gets the information about a single query component. + * + * @param string $dqlAlias The DQL alias. + * + * @return mixed[] + * @psalm-return QueryComponent + */ + public function getQueryComponent(string $dqlAlias): array + { + return $this->queryComponents[$dqlAlias]; + } + + public function getMetadataForDqlAlias(string $dqlAlias): ClassMetadata + { + return $this->queryComponents[$dqlAlias]['metadata'] + ?? throw new LogicException(sprintf('No metadata for DQL alias: %s', $dqlAlias)); + } + + /** + * Returns internal queryComponents array. + * + * @return array + */ + public function getQueryComponents(): array + { + return $this->queryComponents; + } + + /** + * Sets or overrides a query component for a given dql alias. + * + * @psalm-param QueryComponent $queryComponent + */ + public function setQueryComponent(string $dqlAlias, array $queryComponent): void + { + $requiredKeys = ['metadata', 'parent', 'relation', 'map', 'nestingLevel', 'token']; + + if (array_diff($requiredKeys, array_keys($queryComponent))) { + throw QueryException::invalidQueryComponent($dqlAlias); + } + + $this->queryComponents[$dqlAlias] = $queryComponent; + } + + /** + * Gets an executor that can be used to execute the result of this walker. + */ + public function getExecutor(AST\SelectStatement|AST\UpdateStatement|AST\DeleteStatement $statement): Exec\AbstractSqlExecutor + { + return match (true) { + $statement instanceof AST\SelectStatement + => new Exec\SingleSelectExecutor($statement, $this), + $statement instanceof AST\UpdateStatement + => $this->em->getClassMetadata($statement->updateClause->abstractSchemaName)->isInheritanceTypeJoined() + ? new Exec\MultiTableUpdateExecutor($statement, $this) + : new Exec\SingleTableDeleteUpdateExecutor($statement, $this), + $statement instanceof AST\DeleteStatement + => $this->em->getClassMetadata($statement->deleteClause->abstractSchemaName)->isInheritanceTypeJoined() + ? new Exec\MultiTableDeleteExecutor($statement, $this) + : new Exec\SingleTableDeleteUpdateExecutor($statement, $this), + }; + } + + /** + * Generates a unique, short SQL table alias. + */ + public function getSQLTableAlias(string $tableName, string $dqlAlias = ''): string + { + $tableName .= $dqlAlias ? '@[' . $dqlAlias . ']' : ''; + + if (! isset($this->tableAliasMap[$tableName])) { + $this->tableAliasMap[$tableName] = (preg_match('/[a-z]/i', $tableName[0]) ? strtolower($tableName[0]) : 't') + . $this->tableAliasCounter++ . '_'; + } + + return $this->tableAliasMap[$tableName]; + } + + /** + * Forces the SqlWalker to use a specific alias for a table name, rather than + * generating an alias on its own. + */ + public function setSQLTableAlias(string $tableName, string $alias, string $dqlAlias = ''): string + { + $tableName .= $dqlAlias ? '@[' . $dqlAlias . ']' : ''; + + $this->tableAliasMap[$tableName] = $alias; + + return $alias; + } + + /** + * Gets an SQL column alias for a column name. + */ + public function getSQLColumnAlias(string $columnName): string + { + return $this->quoteStrategy->getColumnAlias($columnName, $this->aliasCounter++, $this->platform); + } + + /** + * Generates the SQL JOINs that are necessary for Class Table Inheritance + * for the given class. + */ + private function generateClassTableInheritanceJoins( + ClassMetadata $class, + string $dqlAlias, + ): string { + $sql = ''; + + $baseTableAlias = $this->getSQLTableAlias($class->getTableName(), $dqlAlias); + + // INNER JOIN parent class tables + foreach ($class->parentClasses as $parentClassName) { + $parentClass = $this->em->getClassMetadata($parentClassName); + $tableAlias = $this->getSQLTableAlias($parentClass->getTableName(), $dqlAlias); + + // If this is a joined association we must use left joins to preserve the correct result. + $sql .= isset($this->queryComponents[$dqlAlias]['relation']) ? ' LEFT ' : ' INNER '; + $sql .= 'JOIN ' . $this->quoteStrategy->getTableName($parentClass, $this->platform) . ' ' . $tableAlias . ' ON '; + + $sqlParts = []; + + foreach ($this->quoteStrategy->getIdentifierColumnNames($class, $this->platform) as $columnName) { + $sqlParts[] = $baseTableAlias . '.' . $columnName . ' = ' . $tableAlias . '.' . $columnName; + } + + // Add filters on the root class + $sqlParts[] = $this->generateFilterConditionSQL($parentClass, $tableAlias); + + $sql .= implode(' AND ', array_filter($sqlParts)); + } + + // LEFT JOIN child class tables + foreach ($class->subClasses as $subClassName) { + $subClass = $this->em->getClassMetadata($subClassName); + $tableAlias = $this->getSQLTableAlias($subClass->getTableName(), $dqlAlias); + + $sql .= ' LEFT JOIN ' . $this->quoteStrategy->getTableName($subClass, $this->platform) . ' ' . $tableAlias . ' ON '; + + $sqlParts = []; + + foreach ($this->quoteStrategy->getIdentifierColumnNames($subClass, $this->platform) as $columnName) { + $sqlParts[] = $baseTableAlias . '.' . $columnName . ' = ' . $tableAlias . '.' . $columnName; + } + + $sql .= implode(' AND ', $sqlParts); + } + + return $sql; + } + + private function generateOrderedCollectionOrderByItems(): string + { + $orderedColumns = []; + + foreach ($this->selectedClasses as $selectedClass) { + $dqlAlias = $selectedClass['dqlAlias']; + $qComp = $this->queryComponents[$dqlAlias]; + + if (! isset($qComp['relation']->orderBy)) { + continue; + } + + assert(isset($qComp['metadata'])); + $persister = $this->em->getUnitOfWork()->getEntityPersister($qComp['metadata']->name); + + foreach ($qComp['relation']->orderBy as $fieldName => $orientation) { + $columnName = $this->quoteStrategy->getColumnName($fieldName, $qComp['metadata'], $this->platform); + $tableName = $qComp['metadata']->isInheritanceTypeJoined() + ? $persister->getOwningTable($fieldName) + : $qComp['metadata']->getTableName(); + + $orderedColumn = $this->getSQLTableAlias($tableName, $dqlAlias) . '.' . $columnName; + + // OrderByClause should replace an ordered relation. see - DDC-2475 + if (isset($this->orderedColumnsMap[$orderedColumn])) { + continue; + } + + $this->orderedColumnsMap[$orderedColumn] = $orientation; + $orderedColumns[] = $orderedColumn . ' ' . $orientation; + } + } + + return implode(', ', $orderedColumns); + } + + /** + * Generates a discriminator column SQL condition for the class with the given DQL alias. + * + * @psalm-param list $dqlAliases List of root DQL aliases to inspect for discriminator restrictions. + */ + private function generateDiscriminatorColumnConditionSQL(array $dqlAliases): string + { + $sqlParts = []; + + foreach ($dqlAliases as $dqlAlias) { + $class = $this->getMetadataForDqlAlias($dqlAlias); + + if (! $class->isInheritanceTypeSingleTable()) { + continue; + } + + $sqlTableAlias = $this->useSqlTableAliases + ? $this->getSQLTableAlias($class->getTableName(), $dqlAlias) . '.' + : ''; + + $conn = $this->em->getConnection(); + $values = []; + + if ($class->discriminatorValue !== null) { // discriminators can be 0 + $values[] = $class->getDiscriminatorColumn()->type === 'integer' && is_int($class->discriminatorValue) + ? $class->discriminatorValue + : $conn->quote((string) $class->discriminatorValue); + } + + foreach ($class->subClasses as $subclassName) { + $subclassMetadata = $this->em->getClassMetadata($subclassName); + + // Abstract entity classes show up in the list of subClasses, but may be omitted + // from the discriminator map. In that case, they have a null discriminator value. + if ($subclassMetadata->discriminatorValue === null) { + continue; + } + + $values[] = $subclassMetadata->getDiscriminatorColumn()->type === 'integer' && is_int($subclassMetadata->discriminatorValue) + ? $subclassMetadata->discriminatorValue + : $conn->quote((string) $subclassMetadata->discriminatorValue); + } + + if ($values !== []) { + $sqlParts[] = $sqlTableAlias . $class->getDiscriminatorColumn()->name . ' IN (' . implode(', ', $values) . ')'; + } else { + $sqlParts[] = '1=0'; // impossible condition + } + } + + $sql = implode(' AND ', $sqlParts); + + return count($sqlParts) > 1 ? '(' . $sql . ')' : $sql; + } + + /** + * Generates the filter SQL for a given entity and table alias. + */ + private function generateFilterConditionSQL( + ClassMetadata $targetEntity, + string $targetTableAlias, + ): string { + if (! $this->em->hasFilters()) { + return ''; + } + + switch ($targetEntity->inheritanceType) { + case ClassMetadata::INHERITANCE_TYPE_NONE: + break; + case ClassMetadata::INHERITANCE_TYPE_JOINED: + // The classes in the inheritance will be added to the query one by one, + // but only the root node is getting filtered + if ($targetEntity->name !== $targetEntity->rootEntityName) { + return ''; + } + + break; + case ClassMetadata::INHERITANCE_TYPE_SINGLE_TABLE: + // With STI the table will only be queried once, make sure that the filters + // are added to the root entity + $targetEntity = $this->em->getClassMetadata($targetEntity->rootEntityName); + break; + default: + //@todo: throw exception? + return ''; + } + + $filterClauses = []; + foreach ($this->em->getFilters()->getEnabledFilters() as $filter) { + $filterExpr = $filter->addFilterConstraint($targetEntity, $targetTableAlias); + if ($filterExpr !== '') { + $filterClauses[] = '(' . $filterExpr . ')'; + } + } + + return implode(' AND ', $filterClauses); + } + + /** + * Walks down a SelectStatement AST node, thereby generating the appropriate SQL. + */ + public function walkSelectStatement(AST\SelectStatement $selectStatement): string + { + $limit = $this->query->getMaxResults(); + $offset = $this->query->getFirstResult(); + $lockMode = $this->query->getHint(Query::HINT_LOCK_MODE) ?: LockMode::NONE; + $sql = $this->walkSelectClause($selectStatement->selectClause) + . $this->walkFromClause($selectStatement->fromClause) + . $this->walkWhereClause($selectStatement->whereClause); + + if ($selectStatement->groupByClause) { + $sql .= $this->walkGroupByClause($selectStatement->groupByClause); + } + + if ($selectStatement->havingClause) { + $sql .= $this->walkHavingClause($selectStatement->havingClause); + } + + if ($selectStatement->orderByClause) { + $sql .= $this->walkOrderByClause($selectStatement->orderByClause); + } + + $orderBySql = $this->generateOrderedCollectionOrderByItems(); + if (! $selectStatement->orderByClause && $orderBySql) { + $sql .= ' ORDER BY ' . $orderBySql; + } + + $sql = $this->platform->modifyLimitQuery($sql, $limit, $offset); + + if ($lockMode === LockMode::NONE) { + return $sql; + } + + if ($lockMode === LockMode::PESSIMISTIC_READ) { + return $sql . ' ' . $this->getReadLockSQL($this->platform); + } + + if ($lockMode === LockMode::PESSIMISTIC_WRITE) { + return $sql . ' ' . $this->getWriteLockSQL($this->platform); + } + + if ($lockMode !== LockMode::OPTIMISTIC) { + throw QueryException::invalidLockMode(); + } + + foreach ($this->selectedClasses as $selectedClass) { + if (! $selectedClass['class']->isVersioned) { + throw OptimisticLockException::lockFailed($selectedClass['class']->name); + } + } + + return $sql; + } + + /** + * Walks down a UpdateStatement AST node, thereby generating the appropriate SQL. + */ + public function walkUpdateStatement(AST\UpdateStatement $updateStatement): string + { + $this->useSqlTableAliases = false; + $this->rsm->isSelect = false; + + return $this->walkUpdateClause($updateStatement->updateClause) + . $this->walkWhereClause($updateStatement->whereClause); + } + + /** + * Walks down a DeleteStatement AST node, thereby generating the appropriate SQL. + */ + public function walkDeleteStatement(AST\DeleteStatement $deleteStatement): string + { + $this->useSqlTableAliases = false; + $this->rsm->isSelect = false; + + return $this->walkDeleteClause($deleteStatement->deleteClause) + . $this->walkWhereClause($deleteStatement->whereClause); + } + + /** + * Walks down an IdentificationVariable AST node, thereby generating the appropriate SQL. + * This one differs of ->walkIdentificationVariable() because it generates the entity identifiers. + */ + public function walkEntityIdentificationVariable(string $identVariable): string + { + $class = $this->getMetadataForDqlAlias($identVariable); + $tableAlias = $this->getSQLTableAlias($class->getTableName(), $identVariable); + $sqlParts = []; + + foreach ($this->quoteStrategy->getIdentifierColumnNames($class, $this->platform) as $columnName) { + $sqlParts[] = $tableAlias . '.' . $columnName; + } + + return implode(', ', $sqlParts); + } + + /** + * Walks down an IdentificationVariable (no AST node associated), thereby generating the SQL. + */ + public function walkIdentificationVariable(string $identificationVariable, string|null $fieldName = null): string + { + $class = $this->getMetadataForDqlAlias($identificationVariable); + + if ( + $fieldName !== null && $class->isInheritanceTypeJoined() && + isset($class->fieldMappings[$fieldName]->inherited) + ) { + $class = $this->em->getClassMetadata($class->fieldMappings[$fieldName]->inherited); + } + + return $this->getSQLTableAlias($class->getTableName(), $identificationVariable); + } + + /** + * Walks down a PathExpression AST node, thereby generating the appropriate SQL. + */ + public function walkPathExpression(AST\PathExpression $pathExpr): string + { + $sql = ''; + assert($pathExpr->field !== null); + + switch ($pathExpr->type) { + case AST\PathExpression::TYPE_STATE_FIELD: + $fieldName = $pathExpr->field; + $dqlAlias = $pathExpr->identificationVariable; + $class = $this->getMetadataForDqlAlias($dqlAlias); + + if ($this->useSqlTableAliases) { + $sql .= $this->walkIdentificationVariable($dqlAlias, $fieldName) . '.'; + } + + $sql .= $this->quoteStrategy->getColumnName($fieldName, $class, $this->platform); + break; + + case AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION: + // 1- the owning side: + // Just use the foreign key, i.e. u.group_id + $fieldName = $pathExpr->field; + $dqlAlias = $pathExpr->identificationVariable; + $class = $this->getMetadataForDqlAlias($dqlAlias); + + if (isset($class->associationMappings[$fieldName]->inherited)) { + $class = $this->em->getClassMetadata($class->associationMappings[$fieldName]->inherited); + } + + $assoc = $class->associationMappings[$fieldName]; + + if (! $assoc->isOwningSide()) { + throw QueryException::associationPathInverseSideNotSupported($pathExpr); + } + + assert($assoc->isToOneOwningSide()); + + // COMPOSITE KEYS NOT (YET?) SUPPORTED + if (count($assoc->sourceToTargetKeyColumns) > 1) { + throw QueryException::associationPathCompositeKeyNotSupported(); + } + + if ($this->useSqlTableAliases) { + $sql .= $this->getSQLTableAlias($class->getTableName(), $dqlAlias) . '.'; + } + + $sql .= reset($assoc->targetToSourceKeyColumns); + break; + + default: + throw QueryException::invalidPathExpression($pathExpr); + } + + return $sql; + } + + /** + * Walks down a SelectClause AST node, thereby generating the appropriate SQL. + */ + public function walkSelectClause(AST\SelectClause $selectClause): string + { + $sql = 'SELECT ' . ($selectClause->isDistinct ? 'DISTINCT ' : ''); + $sqlSelectExpressions = array_filter(array_map($this->walkSelectExpression(...), $selectClause->selectExpressions)); + + if ($this->query->getHint(Query::HINT_INTERNAL_ITERATION) === true && $selectClause->isDistinct) { + $this->query->setHint(self::HINT_DISTINCT, true); + } + + $addMetaColumns = $this->query->getHydrationMode() === Query::HYDRATE_OBJECT + || $this->query->getHint(Query::HINT_INCLUDE_META_COLUMNS); + + foreach ($this->selectedClasses as $selectedClass) { + $class = $selectedClass['class']; + $dqlAlias = $selectedClass['dqlAlias']; + $resultAlias = $selectedClass['resultAlias']; + + // Register as entity or joined entity result + if (! isset($this->queryComponents[$dqlAlias]['relation'])) { + $this->rsm->addEntityResult($class->name, $dqlAlias, $resultAlias); + } else { + assert(isset($this->queryComponents[$dqlAlias]['parent'])); + + $this->rsm->addJoinedEntityResult( + $class->name, + $dqlAlias, + $this->queryComponents[$dqlAlias]['parent'], + $this->queryComponents[$dqlAlias]['relation']->fieldName, + ); + } + + if ($class->isInheritanceTypeSingleTable() || $class->isInheritanceTypeJoined()) { + // Add discriminator columns to SQL + $rootClass = $this->em->getClassMetadata($class->rootEntityName); + $tblAlias = $this->getSQLTableAlias($rootClass->getTableName(), $dqlAlias); + $discrColumn = $rootClass->getDiscriminatorColumn(); + $columnAlias = $this->getSQLColumnAlias($discrColumn->name); + + $sqlSelectExpressions[] = $tblAlias . '.' . $discrColumn->name . ' AS ' . $columnAlias; + + $this->rsm->setDiscriminatorColumn($dqlAlias, $columnAlias); + $this->rsm->addMetaResult($dqlAlias, $columnAlias, $discrColumn->fieldName, false, $discrColumn->type); + if (! empty($discrColumn->enumType)) { + $this->rsm->addEnumResult($columnAlias, $discrColumn->enumType); + } + } + + // Add foreign key columns to SQL, if necessary + if (! $addMetaColumns && ! $class->containsForeignIdentifier) { + continue; + } + + // Add foreign key columns of class and also parent classes + foreach ($class->associationMappings as $assoc) { + if ( + ! $assoc->isToOneOwningSide() + || ( ! $addMetaColumns && ! isset($assoc->id)) + ) { + continue; + } + + $targetClass = $this->em->getClassMetadata($assoc->targetEntity); + $isIdentifier = (isset($assoc->id) && $assoc->id === true); + $owningClass = isset($assoc->inherited) ? $this->em->getClassMetadata($assoc->inherited) : $class; + $sqlTableAlias = $this->getSQLTableAlias($owningClass->getTableName(), $dqlAlias); + + foreach ($assoc->joinColumns as $joinColumn) { + $columnName = $joinColumn->name; + $columnAlias = $this->getSQLColumnAlias($columnName); + $columnType = PersisterHelper::getTypeOfColumn($joinColumn->referencedColumnName, $targetClass, $this->em); + + $quotedColumnName = $this->quoteStrategy->getJoinColumnName($joinColumn, $class, $this->platform); + $sqlSelectExpressions[] = $sqlTableAlias . '.' . $quotedColumnName . ' AS ' . $columnAlias; + + $this->rsm->addMetaResult($dqlAlias, $columnAlias, $columnName, $isIdentifier, $columnType); + } + } + + // Add foreign key columns to SQL, if necessary + if (! $addMetaColumns) { + continue; + } + + // Add foreign key columns of subclasses + foreach ($class->subClasses as $subClassName) { + $subClass = $this->em->getClassMetadata($subClassName); + $sqlTableAlias = $this->getSQLTableAlias($subClass->getTableName(), $dqlAlias); + + foreach ($subClass->associationMappings as $assoc) { + // Skip if association is inherited + if (isset($assoc->inherited)) { + continue; + } + + if ($assoc->isToOneOwningSide()) { + $targetClass = $this->em->getClassMetadata($assoc->targetEntity); + + foreach ($assoc->joinColumns as $joinColumn) { + $columnName = $joinColumn->name; + $columnAlias = $this->getSQLColumnAlias($columnName); + $columnType = PersisterHelper::getTypeOfColumn($joinColumn->referencedColumnName, $targetClass, $this->em); + + $quotedColumnName = $this->quoteStrategy->getJoinColumnName($joinColumn, $subClass, $this->platform); + $sqlSelectExpressions[] = $sqlTableAlias . '.' . $quotedColumnName . ' AS ' . $columnAlias; + + $this->rsm->addMetaResult($dqlAlias, $columnAlias, $columnName, $subClass->isIdentifier($columnName), $columnType); + } + } + } + } + } + + return $sql . implode(', ', $sqlSelectExpressions); + } + + /** + * Walks down a FromClause AST node, thereby generating the appropriate SQL. + */ + public function walkFromClause(AST\FromClause $fromClause): string + { + $identificationVarDecls = $fromClause->identificationVariableDeclarations; + $sqlParts = []; + + foreach ($identificationVarDecls as $identificationVariableDecl) { + $sqlParts[] = $this->walkIdentificationVariableDeclaration($identificationVariableDecl); + } + + return ' FROM ' . implode(', ', $sqlParts); + } + + /** + * Walks down a IdentificationVariableDeclaration AST node, thereby generating the appropriate SQL. + */ + public function walkIdentificationVariableDeclaration(AST\IdentificationVariableDeclaration $identificationVariableDecl): string + { + $sql = $this->walkRangeVariableDeclaration($identificationVariableDecl->rangeVariableDeclaration); + + if ($identificationVariableDecl->indexBy) { + $this->walkIndexBy($identificationVariableDecl->indexBy); + } + + foreach ($identificationVariableDecl->joins as $join) { + $sql .= $this->walkJoin($join); + } + + return $sql; + } + + /** + * Walks down a IndexBy AST node. + */ + public function walkIndexBy(AST\IndexBy $indexBy): void + { + $pathExpression = $indexBy->singleValuedPathExpression; + $alias = $pathExpression->identificationVariable; + assert($pathExpression->field !== null); + + switch ($pathExpression->type) { + case AST\PathExpression::TYPE_STATE_FIELD: + $field = $pathExpression->field; + break; + + case AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION: + // Just use the foreign key, i.e. u.group_id + $fieldName = $pathExpression->field; + $class = $this->getMetadataForDqlAlias($alias); + + if (isset($class->associationMappings[$fieldName]->inherited)) { + $class = $this->em->getClassMetadata($class->associationMappings[$fieldName]->inherited); + } + + $association = $class->associationMappings[$fieldName]; + + if (! $association->isOwningSide()) { + throw QueryException::associationPathInverseSideNotSupported($pathExpression); + } + + assert($association->isToOneOwningSide()); + + if (count($association->sourceToTargetKeyColumns) > 1) { + throw QueryException::associationPathCompositeKeyNotSupported(); + } + + $field = reset($association->targetToSourceKeyColumns); + break; + + default: + throw QueryException::invalidPathExpression($pathExpression); + } + + if (isset($this->scalarFields[$alias][$field])) { + $this->rsm->addIndexByScalar($this->scalarFields[$alias][$field]); + + return; + } + + $this->rsm->addIndexBy($alias, $field); + } + + /** + * Walks down a RangeVariableDeclaration AST node, thereby generating the appropriate SQL. + */ + public function walkRangeVariableDeclaration(AST\RangeVariableDeclaration $rangeVariableDeclaration): string + { + return $this->generateRangeVariableDeclarationSQL($rangeVariableDeclaration, false); + } + + /** + * Generate appropriate SQL for RangeVariableDeclaration AST node + */ + private function generateRangeVariableDeclarationSQL( + AST\RangeVariableDeclaration $rangeVariableDeclaration, + bool $buildNestedJoins, + ): string { + $class = $this->em->getClassMetadata($rangeVariableDeclaration->abstractSchemaName); + $dqlAlias = $rangeVariableDeclaration->aliasIdentificationVariable; + + if ($rangeVariableDeclaration->isRoot) { + $this->rootAliases[] = $dqlAlias; + } + + $sql = $this->platform->appendLockHint( + $this->quoteStrategy->getTableName($class, $this->platform) . ' ' . + $this->getSQLTableAlias($class->getTableName(), $dqlAlias), + $this->query->getHint(Query::HINT_LOCK_MODE) ?: LockMode::NONE, + ); + + if (! $class->isInheritanceTypeJoined()) { + return $sql; + } + + $classTableInheritanceJoins = $this->generateClassTableInheritanceJoins($class, $dqlAlias); + + if (! $buildNestedJoins) { + return $sql . $classTableInheritanceJoins; + } + + return $classTableInheritanceJoins === '' ? $sql : '(' . $sql . $classTableInheritanceJoins . ')'; + } + + /** + * Walks down a JoinAssociationDeclaration AST node, thereby generating the appropriate SQL. + * + * @psalm-param AST\Join::JOIN_TYPE_* $joinType + * + * @throws QueryException + */ + public function walkJoinAssociationDeclaration( + AST\JoinAssociationDeclaration $joinAssociationDeclaration, + int $joinType = AST\Join::JOIN_TYPE_INNER, + AST\ConditionalExpression|AST\Phase2OptimizableConditional|null $condExpr = null, + ): string { + $sql = ''; + + $associationPathExpression = $joinAssociationDeclaration->joinAssociationPathExpression; + $joinedDqlAlias = $joinAssociationDeclaration->aliasIdentificationVariable; + $indexBy = $joinAssociationDeclaration->indexBy; + + $relation = $this->queryComponents[$joinedDqlAlias]['relation'] ?? null; + assert($relation !== null); + $targetClass = $this->em->getClassMetadata($relation->targetEntity); + $sourceClass = $this->em->getClassMetadata($relation->sourceEntity); + $targetTableName = $this->quoteStrategy->getTableName($targetClass, $this->platform); + + $targetTableAlias = $this->getSQLTableAlias($targetClass->getTableName(), $joinedDqlAlias); + $sourceTableAlias = $this->getSQLTableAlias($sourceClass->getTableName(), $associationPathExpression->identificationVariable); + + // Ensure we got the owning side, since it has all mapping info + $assoc = $this->em->getMetadataFactory()->getOwningSide($relation); + + if ($this->query->getHint(Query::HINT_INTERNAL_ITERATION) === true && (! $this->query->getHint(self::HINT_DISTINCT) || isset($this->selectedClasses[$joinedDqlAlias]))) { + if ($relation->isToMany()) { + throw QueryException::iterateWithFetchJoinNotAllowed($assoc); + } + } + + $fetchMode = $this->query->getHint('fetchMode')[$assoc->sourceEntity][$assoc->fieldName] ?? $relation->fetch; + + if ($fetchMode === ClassMetadata::FETCH_EAGER && $condExpr !== null) { + throw QueryException::eagerFetchJoinWithNotAllowed($assoc->sourceEntity, $assoc->fieldName); + } + + // This condition is not checking ClassMetadata::MANY_TO_ONE, because by definition it cannot + // be the owning side and previously we ensured that $assoc is always the owning side of the associations. + // The owning side is necessary at this point because only it contains the JoinColumn information. + switch (true) { + case $assoc->isToOne(): + assert($assoc->isToOneOwningSide()); + $conditions = []; + + foreach ($assoc->joinColumns as $joinColumn) { + $quotedSourceColumn = $this->quoteStrategy->getJoinColumnName($joinColumn, $targetClass, $this->platform); + $quotedTargetColumn = $this->quoteStrategy->getReferencedJoinColumnName($joinColumn, $targetClass, $this->platform); + + if ($relation->isOwningSide()) { + $conditions[] = $sourceTableAlias . '.' . $quotedSourceColumn . ' = ' . $targetTableAlias . '.' . $quotedTargetColumn; + + continue; + } + + $conditions[] = $sourceTableAlias . '.' . $quotedTargetColumn . ' = ' . $targetTableAlias . '.' . $quotedSourceColumn; + } + + // Apply remaining inheritance restrictions + $discrSql = $this->generateDiscriminatorColumnConditionSQL([$joinedDqlAlias]); + + if ($discrSql) { + $conditions[] = $discrSql; + } + + // Apply the filters + $filterExpr = $this->generateFilterConditionSQL($targetClass, $targetTableAlias); + + if ($filterExpr) { + $conditions[] = $filterExpr; + } + + $targetTableJoin = [ + 'table' => $targetTableName . ' ' . $targetTableAlias, + 'condition' => implode(' AND ', $conditions), + ]; + break; + + case $assoc->isManyToMany(): + // Join relation table + $joinTable = $assoc->joinTable; + $joinTableAlias = $this->getSQLTableAlias($joinTable->name, $joinedDqlAlias); + $joinTableName = $this->quoteStrategy->getJoinTableName($assoc, $sourceClass, $this->platform); + + $conditions = []; + $relationColumns = $relation->isOwningSide() + ? $assoc->joinTable->joinColumns + : $assoc->joinTable->inverseJoinColumns; + + foreach ($relationColumns as $joinColumn) { + $quotedSourceColumn = $this->quoteStrategy->getJoinColumnName($joinColumn, $targetClass, $this->platform); + $quotedTargetColumn = $this->quoteStrategy->getReferencedJoinColumnName($joinColumn, $targetClass, $this->platform); + + $conditions[] = $sourceTableAlias . '.' . $quotedTargetColumn . ' = ' . $joinTableAlias . '.' . $quotedSourceColumn; + } + + $sql .= $joinTableName . ' ' . $joinTableAlias . ' ON ' . implode(' AND ', $conditions); + + // Join target table + $sql .= $joinType === AST\Join::JOIN_TYPE_LEFT || $joinType === AST\Join::JOIN_TYPE_LEFTOUTER ? ' LEFT JOIN ' : ' INNER JOIN '; + + $conditions = []; + $relationColumns = $relation->isOwningSide() + ? $assoc->joinTable->inverseJoinColumns + : $assoc->joinTable->joinColumns; + + foreach ($relationColumns as $joinColumn) { + $quotedSourceColumn = $this->quoteStrategy->getJoinColumnName($joinColumn, $targetClass, $this->platform); + $quotedTargetColumn = $this->quoteStrategy->getReferencedJoinColumnName($joinColumn, $targetClass, $this->platform); + + $conditions[] = $targetTableAlias . '.' . $quotedTargetColumn . ' = ' . $joinTableAlias . '.' . $quotedSourceColumn; + } + + // Apply remaining inheritance restrictions + $discrSql = $this->generateDiscriminatorColumnConditionSQL([$joinedDqlAlias]); + + if ($discrSql) { + $conditions[] = $discrSql; + } + + // Apply the filters + $filterExpr = $this->generateFilterConditionSQL($targetClass, $targetTableAlias); + + if ($filterExpr) { + $conditions[] = $filterExpr; + } + + $targetTableJoin = [ + 'table' => $targetTableName . ' ' . $targetTableAlias, + 'condition' => implode(' AND ', $conditions), + ]; + break; + + default: + throw new BadMethodCallException('Type of association must be one of *_TO_ONE or MANY_TO_MANY'); + } + + // Handle WITH clause + $withCondition = $condExpr === null ? '' : ('(' . $this->walkConditionalExpression($condExpr) . ')'); + + if ($targetClass->isInheritanceTypeJoined()) { + $ctiJoins = $this->generateClassTableInheritanceJoins($targetClass, $joinedDqlAlias); + // If we have WITH condition, we need to build nested joins for target class table and cti joins + if ($withCondition && $ctiJoins) { + $sql .= '(' . $targetTableJoin['table'] . $ctiJoins . ') ON ' . $targetTableJoin['condition']; + } else { + $sql .= $targetTableJoin['table'] . ' ON ' . $targetTableJoin['condition'] . $ctiJoins; + } + } else { + $sql .= $targetTableJoin['table'] . ' ON ' . $targetTableJoin['condition']; + } + + if ($withCondition) { + $sql .= ' AND ' . $withCondition; + } + + // Apply the indexes + if ($indexBy) { + // For Many-To-One or One-To-One associations this obviously makes no sense, but is ignored silently. + $this->walkIndexBy($indexBy); + } elseif ($relation->isIndexed()) { + $this->rsm->addIndexBy($joinedDqlAlias, $relation->indexBy()); + } + + return $sql; + } + + /** + * Walks down a FunctionNode AST node, thereby generating the appropriate SQL. + */ + public function walkFunction(AST\Functions\FunctionNode $function): string + { + return $function->getSql($this); + } + + /** + * Walks down an OrderByClause AST node, thereby generating the appropriate SQL. + */ + public function walkOrderByClause(AST\OrderByClause $orderByClause): string + { + $orderByItems = array_map($this->walkOrderByItem(...), $orderByClause->orderByItems); + + $collectionOrderByItems = $this->generateOrderedCollectionOrderByItems(); + if ($collectionOrderByItems !== '') { + $orderByItems = array_merge($orderByItems, (array) $collectionOrderByItems); + } + + return ' ORDER BY ' . implode(', ', $orderByItems); + } + + /** + * Walks down an OrderByItem AST node, thereby generating the appropriate SQL. + */ + public function walkOrderByItem(AST\OrderByItem $orderByItem): string + { + $type = strtoupper($orderByItem->type); + $expr = $orderByItem->expression; + $sql = $expr instanceof AST\Node + ? $expr->dispatch($this) + : $this->walkResultVariable($this->queryComponents[$expr]['token']->value); + + $this->orderedColumnsMap[$sql] = $type; + + if ($expr instanceof AST\Subselect) { + return '(' . $sql . ') ' . $type; + } + + return $sql . ' ' . $type; + } + + /** + * Walks down a HavingClause AST node, thereby generating the appropriate SQL. + */ + public function walkHavingClause(AST\HavingClause $havingClause): string + { + return ' HAVING ' . $this->walkConditionalExpression($havingClause->conditionalExpression); + } + + /** + * Walks down a Join AST node and creates the corresponding SQL. + */ + public function walkJoin(AST\Join $join): string + { + $joinType = $join->joinType; + $joinDeclaration = $join->joinAssociationDeclaration; + + $sql = $joinType === AST\Join::JOIN_TYPE_LEFT || $joinType === AST\Join::JOIN_TYPE_LEFTOUTER + ? ' LEFT JOIN ' + : ' INNER JOIN '; + + switch (true) { + case $joinDeclaration instanceof AST\RangeVariableDeclaration: + $class = $this->em->getClassMetadata($joinDeclaration->abstractSchemaName); + $dqlAlias = $joinDeclaration->aliasIdentificationVariable; + $tableAlias = $this->getSQLTableAlias($class->table['name'], $dqlAlias); + $conditions = []; + + if ($join->conditionalExpression) { + $conditions[] = '(' . $this->walkConditionalExpression($join->conditionalExpression) . ')'; + } + + $isUnconditionalJoin = $conditions === []; + $condExprConjunction = $class->isInheritanceTypeJoined() && $joinType !== AST\Join::JOIN_TYPE_LEFT && $joinType !== AST\Join::JOIN_TYPE_LEFTOUTER && $isUnconditionalJoin + ? ' AND ' + : ' ON '; + + $sql .= $this->generateRangeVariableDeclarationSQL($joinDeclaration, ! $isUnconditionalJoin); + + // Apply remaining inheritance restrictions + $discrSql = $this->generateDiscriminatorColumnConditionSQL([$dqlAlias]); + + if ($discrSql) { + $conditions[] = $discrSql; + } + + // Apply the filters + $filterExpr = $this->generateFilterConditionSQL($class, $tableAlias); + + if ($filterExpr) { + $conditions[] = $filterExpr; + } + + if ($conditions) { + $sql .= $condExprConjunction . implode(' AND ', $conditions); + } + + break; + + case $joinDeclaration instanceof AST\JoinAssociationDeclaration: + $sql .= $this->walkJoinAssociationDeclaration($joinDeclaration, $joinType, $join->conditionalExpression); + break; + } + + return $sql; + } + + /** + * Walks down a CoalesceExpression AST node and generates the corresponding SQL. + */ + public function walkCoalesceExpression(AST\CoalesceExpression $coalesceExpression): string + { + $sql = 'COALESCE('; + + $scalarExpressions = []; + + foreach ($coalesceExpression->scalarExpressions as $scalarExpression) { + $scalarExpressions[] = $this->walkSimpleArithmeticExpression($scalarExpression); + } + + return $sql . implode(', ', $scalarExpressions) . ')'; + } + + /** + * Walks down a NullIfExpression AST node and generates the corresponding SQL. + */ + public function walkNullIfExpression(AST\NullIfExpression $nullIfExpression): string + { + $firstExpression = is_string($nullIfExpression->firstExpression) + ? $this->conn->quote($nullIfExpression->firstExpression) + : $this->walkSimpleArithmeticExpression($nullIfExpression->firstExpression); + + $secondExpression = is_string($nullIfExpression->secondExpression) + ? $this->conn->quote($nullIfExpression->secondExpression) + : $this->walkSimpleArithmeticExpression($nullIfExpression->secondExpression); + + return 'NULLIF(' . $firstExpression . ', ' . $secondExpression . ')'; + } + + /** + * Walks down a GeneralCaseExpression AST node and generates the corresponding SQL. + */ + public function walkGeneralCaseExpression(AST\GeneralCaseExpression $generalCaseExpression): string + { + $sql = 'CASE'; + + foreach ($generalCaseExpression->whenClauses as $whenClause) { + $sql .= ' WHEN ' . $this->walkConditionalExpression($whenClause->caseConditionExpression); + $sql .= ' THEN ' . $this->walkSimpleArithmeticExpression($whenClause->thenScalarExpression); + } + + $sql .= ' ELSE ' . $this->walkSimpleArithmeticExpression($generalCaseExpression->elseScalarExpression) . ' END'; + + return $sql; + } + + /** + * Walks down a SimpleCaseExpression AST node and generates the corresponding SQL. + */ + public function walkSimpleCaseExpression(AST\SimpleCaseExpression $simpleCaseExpression): string + { + $sql = 'CASE ' . $this->walkStateFieldPathExpression($simpleCaseExpression->caseOperand); + + foreach ($simpleCaseExpression->simpleWhenClauses as $simpleWhenClause) { + $sql .= ' WHEN ' . $this->walkSimpleArithmeticExpression($simpleWhenClause->caseScalarExpression); + $sql .= ' THEN ' . $this->walkSimpleArithmeticExpression($simpleWhenClause->thenScalarExpression); + } + + $sql .= ' ELSE ' . $this->walkSimpleArithmeticExpression($simpleCaseExpression->elseScalarExpression) . ' END'; + + return $sql; + } + + /** + * Walks down a SelectExpression AST node and generates the corresponding SQL. + */ + public function walkSelectExpression(AST\SelectExpression $selectExpression): string + { + $sql = ''; + $expr = $selectExpression->expression; + $hidden = $selectExpression->hiddenAliasResultVariable; + + switch (true) { + case $expr instanceof AST\PathExpression: + if ($expr->type !== AST\PathExpression::TYPE_STATE_FIELD) { + throw QueryException::invalidPathExpression($expr); + } + + assert($expr->field !== null); + $fieldName = $expr->field; + $dqlAlias = $expr->identificationVariable; + $class = $this->getMetadataForDqlAlias($dqlAlias); + + $resultAlias = $selectExpression->fieldIdentificationVariable ?: $fieldName; + $tableName = $class->isInheritanceTypeJoined() + ? $this->em->getUnitOfWork()->getEntityPersister($class->name)->getOwningTable($fieldName) + : $class->getTableName(); + + $sqlTableAlias = $this->getSQLTableAlias($tableName, $dqlAlias); + $fieldMapping = $class->fieldMappings[$fieldName]; + $columnName = $this->quoteStrategy->getColumnName($fieldName, $class, $this->platform); + $columnAlias = $this->getSQLColumnAlias($fieldMapping->columnName); + $col = $sqlTableAlias . '.' . $columnName; + + $type = Type::getType($fieldMapping->type); + $col = $type->convertToPHPValueSQL($col, $this->conn->getDatabasePlatform()); + + $sql .= $col . ' AS ' . $columnAlias; + + $this->scalarResultAliasMap[$resultAlias] = $columnAlias; + + if (! $hidden) { + $this->rsm->addScalarResult($columnAlias, $resultAlias, $fieldMapping->type); + $this->scalarFields[$dqlAlias][$fieldName] = $columnAlias; + + if (! empty($fieldMapping->enumType)) { + $this->rsm->addEnumResult($columnAlias, $fieldMapping->enumType); + } + } + + break; + + case $expr instanceof AST\AggregateExpression: + case $expr instanceof AST\Functions\FunctionNode: + case $expr instanceof AST\SimpleArithmeticExpression: + case $expr instanceof AST\ArithmeticTerm: + case $expr instanceof AST\ArithmeticFactor: + case $expr instanceof AST\ParenthesisExpression: + case $expr instanceof AST\Literal: + case $expr instanceof AST\NullIfExpression: + case $expr instanceof AST\CoalesceExpression: + case $expr instanceof AST\GeneralCaseExpression: + case $expr instanceof AST\SimpleCaseExpression: + $columnAlias = $this->getSQLColumnAlias('sclr'); + $resultAlias = $selectExpression->fieldIdentificationVariable ?: $this->scalarResultCounter++; + + $sql .= $expr->dispatch($this) . ' AS ' . $columnAlias; + + $this->scalarResultAliasMap[$resultAlias] = $columnAlias; + + if ($hidden) { + break; + } + + if (! $expr instanceof Query\AST\TypedExpression) { + // Conceptually we could resolve field type here by traverse through AST to retrieve field type, + // but this is not a feasible solution; assume 'string'. + $this->rsm->addScalarResult($columnAlias, $resultAlias, 'string'); + + break; + } + + $this->rsm->addScalarResult($columnAlias, $resultAlias, Type::getTypeRegistry()->lookupName($expr->getReturnType())); + + break; + + case $expr instanceof AST\Subselect: + $columnAlias = $this->getSQLColumnAlias('sclr'); + $resultAlias = $selectExpression->fieldIdentificationVariable ?: $this->scalarResultCounter++; + + $sql .= '(' . $this->walkSubselect($expr) . ') AS ' . $columnAlias; + + $this->scalarResultAliasMap[$resultAlias] = $columnAlias; + + if (! $hidden) { + // We cannot resolve field type here; assume 'string'. + $this->rsm->addScalarResult($columnAlias, $resultAlias, 'string'); + } + + break; + + case $expr instanceof AST\NewObjectExpression: + $sql .= $this->walkNewObject($expr, $selectExpression->fieldIdentificationVariable); + break; + + default: + $dqlAlias = $expr; + $class = $this->getMetadataForDqlAlias($dqlAlias); + $resultAlias = $selectExpression->fieldIdentificationVariable ?: null; + + if (! isset($this->selectedClasses[$dqlAlias])) { + $this->selectedClasses[$dqlAlias] = [ + 'class' => $class, + 'dqlAlias' => $dqlAlias, + 'resultAlias' => $resultAlias, + ]; + } + + $sqlParts = []; + + // Select all fields from the queried class + foreach ($class->fieldMappings as $fieldName => $mapping) { + $tableName = isset($mapping->inherited) + ? $this->em->getClassMetadata($mapping->inherited)->getTableName() + : $class->getTableName(); + + $sqlTableAlias = $this->getSQLTableAlias($tableName, $dqlAlias); + $columnAlias = $this->getSQLColumnAlias($mapping->columnName); + $quotedColumnName = $this->quoteStrategy->getColumnName($fieldName, $class, $this->platform); + + $col = $sqlTableAlias . '.' . $quotedColumnName; + + $type = Type::getType($mapping->type); + $col = $type->convertToPHPValueSQL($col, $this->platform); + + $sqlParts[] = $col . ' AS ' . $columnAlias; + + $this->scalarResultAliasMap[$resultAlias][] = $columnAlias; + + $this->rsm->addFieldResult($dqlAlias, $columnAlias, $fieldName, $class->name); + + if (! empty($mapping->enumType)) { + $this->rsm->addEnumResult($columnAlias, $mapping->enumType); + } + } + + // Add any additional fields of subclasses (excluding inherited fields) + // 1) on Single Table Inheritance: always, since its marginal overhead + // 2) on Class Table Inheritance + foreach ($class->subClasses as $subClassName) { + $subClass = $this->em->getClassMetadata($subClassName); + $sqlTableAlias = $this->getSQLTableAlias($subClass->getTableName(), $dqlAlias); + + foreach ($subClass->fieldMappings as $fieldName => $mapping) { + if (isset($mapping->inherited)) { + continue; + } + + $columnAlias = $this->getSQLColumnAlias($mapping->columnName); + $quotedColumnName = $this->quoteStrategy->getColumnName($fieldName, $subClass, $this->platform); + + $col = $sqlTableAlias . '.' . $quotedColumnName; + + $type = Type::getType($mapping->type); + $col = $type->convertToPHPValueSQL($col, $this->platform); + + $sqlParts[] = $col . ' AS ' . $columnAlias; + + $this->scalarResultAliasMap[$resultAlias][] = $columnAlias; + + $this->rsm->addFieldResult($dqlAlias, $columnAlias, $fieldName, $subClassName); + } + } + + $sql .= implode(', ', $sqlParts); + } + + return $sql; + } + + public function walkQuantifiedExpression(AST\QuantifiedExpression $qExpr): string + { + return ' ' . strtoupper($qExpr->type) . '(' . $this->walkSubselect($qExpr->subselect) . ')'; + } + + /** + * Walks down a Subselect AST node, thereby generating the appropriate SQL. + */ + public function walkSubselect(AST\Subselect $subselect): string + { + $useAliasesBefore = $this->useSqlTableAliases; + $rootAliasesBefore = $this->rootAliases; + + $this->rootAliases = []; // reset the rootAliases for the subselect + $this->useSqlTableAliases = true; + + $sql = $this->walkSimpleSelectClause($subselect->simpleSelectClause); + $sql .= $this->walkSubselectFromClause($subselect->subselectFromClause); + $sql .= $this->walkWhereClause($subselect->whereClause); + + $sql .= $subselect->groupByClause ? $this->walkGroupByClause($subselect->groupByClause) : ''; + $sql .= $subselect->havingClause ? $this->walkHavingClause($subselect->havingClause) : ''; + $sql .= $subselect->orderByClause ? $this->walkOrderByClause($subselect->orderByClause) : ''; + + $this->rootAliases = $rootAliasesBefore; // put the main aliases back + $this->useSqlTableAliases = $useAliasesBefore; + + return $sql; + } + + /** + * Walks down a SubselectFromClause AST node, thereby generating the appropriate SQL. + */ + public function walkSubselectFromClause(AST\SubselectFromClause $subselectFromClause): string + { + $identificationVarDecls = $subselectFromClause->identificationVariableDeclarations; + $sqlParts = []; + + foreach ($identificationVarDecls as $subselectIdVarDecl) { + $sqlParts[] = $this->walkIdentificationVariableDeclaration($subselectIdVarDecl); + } + + return ' FROM ' . implode(', ', $sqlParts); + } + + /** + * Walks down a SimpleSelectClause AST node, thereby generating the appropriate SQL. + */ + public function walkSimpleSelectClause(AST\SimpleSelectClause $simpleSelectClause): string + { + return 'SELECT' . ($simpleSelectClause->isDistinct ? ' DISTINCT' : '') + . $this->walkSimpleSelectExpression($simpleSelectClause->simpleSelectExpression); + } + + public function walkParenthesisExpression(AST\ParenthesisExpression $parenthesisExpression): string + { + return sprintf('(%s)', $parenthesisExpression->expression->dispatch($this)); + } + + public function walkNewObject(AST\NewObjectExpression $newObjectExpression, string|null $newObjectResultAlias = null): string + { + $sqlSelectExpressions = []; + $objIndex = $newObjectResultAlias ?: $this->newObjectCounter++; + + foreach ($newObjectExpression->args as $argIndex => $e) { + $resultAlias = $this->scalarResultCounter++; + $columnAlias = $this->getSQLColumnAlias('sclr'); + $fieldType = 'string'; + + switch (true) { + case $e instanceof AST\NewObjectExpression: + $sqlSelectExpressions[] = $e->dispatch($this); + break; + + case $e instanceof AST\Subselect: + $sqlSelectExpressions[] = '(' . $e->dispatch($this) . ') AS ' . $columnAlias; + break; + + case $e instanceof AST\PathExpression: + assert($e->field !== null); + $dqlAlias = $e->identificationVariable; + $class = $this->getMetadataForDqlAlias($dqlAlias); + $fieldName = $e->field; + $fieldMapping = $class->fieldMappings[$fieldName]; + $fieldType = $fieldMapping->type; + $col = trim($e->dispatch($this)); + + $type = Type::getType($fieldType); + $col = $type->convertToPHPValueSQL($col, $this->platform); + + $sqlSelectExpressions[] = $col . ' AS ' . $columnAlias; + + if (! empty($fieldMapping->enumType)) { + $this->rsm->addEnumResult($columnAlias, $fieldMapping->enumType); + } + + break; + + case $e instanceof AST\Literal: + switch ($e->type) { + case AST\Literal::BOOLEAN: + $fieldType = 'boolean'; + break; + + case AST\Literal::NUMERIC: + $fieldType = is_float($e->value) ? 'float' : 'integer'; + break; + } + + $sqlSelectExpressions[] = trim($e->dispatch($this)) . ' AS ' . $columnAlias; + break; + + default: + $sqlSelectExpressions[] = trim($e->dispatch($this)) . ' AS ' . $columnAlias; + break; + } + + $this->scalarResultAliasMap[$resultAlias] = $columnAlias; + $this->rsm->addScalarResult($columnAlias, $resultAlias, $fieldType); + + $this->rsm->newObjectMappings[$columnAlias] = [ + 'className' => $newObjectExpression->className, + 'objIndex' => $objIndex, + 'argIndex' => $argIndex, + ]; + } + + return implode(', ', $sqlSelectExpressions); + } + + /** + * Walks down a SimpleSelectExpression AST node, thereby generating the appropriate SQL. + */ + public function walkSimpleSelectExpression(AST\SimpleSelectExpression $simpleSelectExpression): string + { + $expr = $simpleSelectExpression->expression; + $sql = ' '; + + switch (true) { + case $expr instanceof AST\PathExpression: + $sql .= $this->walkPathExpression($expr); + break; + + case $expr instanceof AST\Subselect: + $alias = $simpleSelectExpression->fieldIdentificationVariable ?: $this->scalarResultCounter++; + + $columnAlias = 'sclr' . $this->aliasCounter++; + $this->scalarResultAliasMap[$alias] = $columnAlias; + + $sql .= '(' . $this->walkSubselect($expr) . ') AS ' . $columnAlias; + break; + + case $expr instanceof AST\Functions\FunctionNode: + case $expr instanceof AST\SimpleArithmeticExpression: + case $expr instanceof AST\ArithmeticTerm: + case $expr instanceof AST\ArithmeticFactor: + case $expr instanceof AST\Literal: + case $expr instanceof AST\NullIfExpression: + case $expr instanceof AST\CoalesceExpression: + case $expr instanceof AST\GeneralCaseExpression: + case $expr instanceof AST\SimpleCaseExpression: + $alias = $simpleSelectExpression->fieldIdentificationVariable ?: $this->scalarResultCounter++; + + $columnAlias = $this->getSQLColumnAlias('sclr'); + $this->scalarResultAliasMap[$alias] = $columnAlias; + + $sql .= $expr->dispatch($this) . ' AS ' . $columnAlias; + break; + + case $expr instanceof AST\ParenthesisExpression: + $sql .= $this->walkParenthesisExpression($expr); + break; + + default: // IdentificationVariable + $sql .= $this->walkEntityIdentificationVariable($expr); + break; + } + + return $sql; + } + + /** + * Walks down an AggregateExpression AST node, thereby generating the appropriate SQL. + */ + public function walkAggregateExpression(AST\AggregateExpression $aggExpression): string + { + return $aggExpression->functionName . '(' . ($aggExpression->isDistinct ? 'DISTINCT ' : '') + . $this->walkSimpleArithmeticExpression($aggExpression->pathExpression) . ')'; + } + + /** + * Walks down a GroupByClause AST node, thereby generating the appropriate SQL. + */ + public function walkGroupByClause(AST\GroupByClause $groupByClause): string + { + $sqlParts = []; + + foreach ($groupByClause->groupByItems as $groupByItem) { + $sqlParts[] = $this->walkGroupByItem($groupByItem); + } + + return ' GROUP BY ' . implode(', ', $sqlParts); + } + + /** + * Walks down a GroupByItem AST node, thereby generating the appropriate SQL. + */ + public function walkGroupByItem(AST\PathExpression|string $groupByItem): string + { + // StateFieldPathExpression + if (! is_string($groupByItem)) { + return $this->walkPathExpression($groupByItem); + } + + // ResultVariable + if (isset($this->queryComponents[$groupByItem]['resultVariable'])) { + $resultVariable = $this->queryComponents[$groupByItem]['resultVariable']; + + if ($resultVariable instanceof AST\PathExpression) { + return $this->walkPathExpression($resultVariable); + } + + if ($resultVariable instanceof AST\Node && isset($resultVariable->pathExpression)) { + return $this->walkPathExpression($resultVariable->pathExpression); + } + + return $this->walkResultVariable($groupByItem); + } + + // IdentificationVariable + $sqlParts = []; + + foreach ($this->getMetadataForDqlAlias($groupByItem)->fieldNames as $field) { + $item = new AST\PathExpression(AST\PathExpression::TYPE_STATE_FIELD, $groupByItem, $field); + $item->type = AST\PathExpression::TYPE_STATE_FIELD; + + $sqlParts[] = $this->walkPathExpression($item); + } + + foreach ($this->getMetadataForDqlAlias($groupByItem)->associationMappings as $mapping) { + if ($mapping->isToOneOwningSide()) { + $item = new AST\PathExpression(AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION, $groupByItem, $mapping->fieldName); + $item->type = AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION; + + $sqlParts[] = $this->walkPathExpression($item); + } + } + + return implode(', ', $sqlParts); + } + + /** + * Walks down a DeleteClause AST node, thereby generating the appropriate SQL. + */ + public function walkDeleteClause(AST\DeleteClause $deleteClause): string + { + $class = $this->em->getClassMetadata($deleteClause->abstractSchemaName); + $tableName = $class->getTableName(); + $sql = 'DELETE FROM ' . $this->quoteStrategy->getTableName($class, $this->platform); + + $this->setSQLTableAlias($tableName, $tableName, $deleteClause->aliasIdentificationVariable); + $this->rootAliases[] = $deleteClause->aliasIdentificationVariable; + + return $sql; + } + + /** + * Walks down an UpdateClause AST node, thereby generating the appropriate SQL. + */ + public function walkUpdateClause(AST\UpdateClause $updateClause): string + { + $class = $this->em->getClassMetadata($updateClause->abstractSchemaName); + $tableName = $class->getTableName(); + $sql = 'UPDATE ' . $this->quoteStrategy->getTableName($class, $this->platform); + + $this->setSQLTableAlias($tableName, $tableName, $updateClause->aliasIdentificationVariable); + $this->rootAliases[] = $updateClause->aliasIdentificationVariable; + + return $sql . ' SET ' . implode(', ', array_map($this->walkUpdateItem(...), $updateClause->updateItems)); + } + + /** + * Walks down an UpdateItem AST node, thereby generating the appropriate SQL. + */ + public function walkUpdateItem(AST\UpdateItem $updateItem): string + { + $useTableAliasesBefore = $this->useSqlTableAliases; + $this->useSqlTableAliases = false; + + $sql = $this->walkPathExpression($updateItem->pathExpression) . ' = '; + $newValue = $updateItem->newValue; + + $sql .= match (true) { + $newValue instanceof AST\Node => $newValue->dispatch($this), + $newValue === null => 'NULL', + }; + + $this->useSqlTableAliases = $useTableAliasesBefore; + + return $sql; + } + + /** + * Walks down a WhereClause AST node, thereby generating the appropriate SQL. + * + * WhereClause or not, the appropriate discriminator sql is added. + */ + public function walkWhereClause(AST\WhereClause|null $whereClause): string + { + $condSql = $whereClause !== null ? $this->walkConditionalExpression($whereClause->conditionalExpression) : ''; + $discrSql = $this->generateDiscriminatorColumnConditionSQL($this->rootAliases); + + if ($this->em->hasFilters()) { + $filterClauses = []; + foreach ($this->rootAliases as $dqlAlias) { + $class = $this->getMetadataForDqlAlias($dqlAlias); + $tableAlias = $this->getSQLTableAlias($class->table['name'], $dqlAlias); + + $filterExpr = $this->generateFilterConditionSQL($class, $tableAlias); + if ($filterExpr) { + $filterClauses[] = $filterExpr; + } + } + + if (count($filterClauses)) { + if ($condSql) { + $condSql = '(' . $condSql . ') AND '; + } + + $condSql .= implode(' AND ', $filterClauses); + } + } + + if ($condSql) { + return ' WHERE ' . (! $discrSql ? $condSql : '(' . $condSql . ') AND ' . $discrSql); + } + + if ($discrSql) { + return ' WHERE ' . $discrSql; + } + + return ''; + } + + /** + * Walk down a ConditionalExpression AST node, thereby generating the appropriate SQL. + */ + public function walkConditionalExpression( + AST\ConditionalExpression|AST\Phase2OptimizableConditional $condExpr, + ): string { + // Phase 2 AST optimization: Skip processing of ConditionalExpression + // if only one ConditionalTerm is defined + if (! ($condExpr instanceof AST\ConditionalExpression)) { + return $this->walkConditionalTerm($condExpr); + } + + return implode(' OR ', array_map($this->walkConditionalTerm(...), $condExpr->conditionalTerms)); + } + + /** + * Walks down a ConditionalTerm AST node, thereby generating the appropriate SQL. + */ + public function walkConditionalTerm( + AST\ConditionalTerm|AST\ConditionalPrimary|AST\ConditionalFactor $condTerm, + ): string { + // Phase 2 AST optimization: Skip processing of ConditionalTerm + // if only one ConditionalFactor is defined + if (! ($condTerm instanceof AST\ConditionalTerm)) { + return $this->walkConditionalFactor($condTerm); + } + + return implode(' AND ', array_map($this->walkConditionalFactor(...), $condTerm->conditionalFactors)); + } + + /** + * Walks down a ConditionalFactor AST node, thereby generating the appropriate SQL. + */ + public function walkConditionalFactor( + AST\ConditionalFactor|AST\ConditionalPrimary $factor, + ): string { + // Phase 2 AST optimization: Skip processing of ConditionalFactor + // if only one ConditionalPrimary is defined + return ! ($factor instanceof AST\ConditionalFactor) + ? $this->walkConditionalPrimary($factor) + : ($factor->not ? 'NOT ' : '') . $this->walkConditionalPrimary($factor->conditionalPrimary); + } + + /** + * Walks down a ConditionalPrimary AST node, thereby generating the appropriate SQL. + */ + public function walkConditionalPrimary(AST\ConditionalPrimary $primary): string + { + if ($primary->isSimpleConditionalExpression()) { + return $primary->simpleConditionalExpression->dispatch($this); + } + + if ($primary->isConditionalExpression()) { + $condExpr = $primary->conditionalExpression; + + return '(' . $this->walkConditionalExpression($condExpr) . ')'; + } + + throw new LogicException('Unexpected state of ConditionalPrimary node.'); + } + + /** + * Walks down an ExistsExpression AST node, thereby generating the appropriate SQL. + */ + public function walkExistsExpression(AST\ExistsExpression $existsExpr): string + { + $sql = $existsExpr->not ? 'NOT ' : ''; + + $sql .= 'EXISTS (' . $this->walkSubselect($existsExpr->subselect) . ')'; + + return $sql; + } + + /** + * Walks down a CollectionMemberExpression AST node, thereby generating the appropriate SQL. + */ + public function walkCollectionMemberExpression(AST\CollectionMemberExpression $collMemberExpr): string + { + $sql = $collMemberExpr->not ? 'NOT ' : ''; + $sql .= 'EXISTS (SELECT 1 FROM '; + + $entityExpr = $collMemberExpr->entityExpression; + $collPathExpr = $collMemberExpr->collectionValuedPathExpression; + assert($collPathExpr->field !== null); + + $fieldName = $collPathExpr->field; + $dqlAlias = $collPathExpr->identificationVariable; + + $class = $this->getMetadataForDqlAlias($dqlAlias); + + switch (true) { + // InputParameter + case $entityExpr instanceof AST\InputParameter: + $dqlParamKey = $entityExpr->name; + $entitySql = '?'; + break; + + // SingleValuedAssociationPathExpression | IdentificationVariable + case $entityExpr instanceof AST\PathExpression: + $entitySql = $this->walkPathExpression($entityExpr); + break; + + default: + throw new BadMethodCallException('Not implemented'); + } + + $assoc = $class->associationMappings[$fieldName]; + + if ($assoc->isOneToMany()) { + $targetClass = $this->em->getClassMetadata($assoc->targetEntity); + $targetTableAlias = $this->getSQLTableAlias($targetClass->getTableName()); + $sourceTableAlias = $this->getSQLTableAlias($class->getTableName(), $dqlAlias); + + $sql .= $this->quoteStrategy->getTableName($targetClass, $this->platform) . ' ' . $targetTableAlias . ' WHERE '; + + $owningAssoc = $targetClass->associationMappings[$assoc->mappedBy]; + assert($owningAssoc->isManyToOne()); + $sqlParts = []; + + foreach ($owningAssoc->targetToSourceKeyColumns as $targetColumn => $sourceColumn) { + $targetColumn = $this->quoteStrategy->getColumnName($class->fieldNames[$targetColumn], $class, $this->platform); + + $sqlParts[] = $sourceTableAlias . '.' . $targetColumn . ' = ' . $targetTableAlias . '.' . $sourceColumn; + } + + foreach ($this->quoteStrategy->getIdentifierColumnNames($targetClass, $this->platform) as $targetColumnName) { + if (isset($dqlParamKey)) { + $this->parserResult->addParameterMapping($dqlParamKey, $this->sqlParamIndex++); + } + + $sqlParts[] = $targetTableAlias . '.' . $targetColumnName . ' = ' . $entitySql; + } + + $sql .= implode(' AND ', $sqlParts); + } else { // many-to-many + $targetClass = $this->em->getClassMetadata($assoc->targetEntity); + + $owningAssoc = $this->em->getMetadataFactory()->getOwningSide($assoc); + assert($owningAssoc->isManyToManyOwningSide()); + $joinTable = $owningAssoc->joinTable; + + // SQL table aliases + $joinTableAlias = $this->getSQLTableAlias($joinTable->name); + $sourceTableAlias = $this->getSQLTableAlias($class->getTableName(), $dqlAlias); + + $sql .= $this->quoteStrategy->getJoinTableName($owningAssoc, $targetClass, $this->platform) . ' ' . $joinTableAlias . ' WHERE '; + + $joinColumns = $assoc->isOwningSide() ? $joinTable->joinColumns : $joinTable->inverseJoinColumns; + $sqlParts = []; + + foreach ($joinColumns as $joinColumn) { + $targetColumn = $this->quoteStrategy->getColumnName($class->fieldNames[$joinColumn->referencedColumnName], $class, $this->platform); + + $sqlParts[] = $joinTableAlias . '.' . $joinColumn->name . ' = ' . $sourceTableAlias . '.' . $targetColumn; + } + + $joinColumns = $assoc->isOwningSide() ? $joinTable->inverseJoinColumns : $joinTable->joinColumns; + + foreach ($joinColumns as $joinColumn) { + if (isset($dqlParamKey)) { + $this->parserResult->addParameterMapping($dqlParamKey, $this->sqlParamIndex++); + } + + $sqlParts[] = $joinTableAlias . '.' . $joinColumn->name . ' IN (' . $entitySql . ')'; + } + + $sql .= implode(' AND ', $sqlParts); + } + + return $sql . ')'; + } + + /** + * Walks down an EmptyCollectionComparisonExpression AST node, thereby generating the appropriate SQL. + */ + public function walkEmptyCollectionComparisonExpression(AST\EmptyCollectionComparisonExpression $emptyCollCompExpr): string + { + $sizeFunc = new AST\Functions\SizeFunction('size'); + $sizeFunc->collectionPathExpression = $emptyCollCompExpr->expression; + + return $sizeFunc->getSql($this) . ($emptyCollCompExpr->not ? ' > 0' : ' = 0'); + } + + /** + * Walks down a NullComparisonExpression AST node, thereby generating the appropriate SQL. + */ + public function walkNullComparisonExpression(AST\NullComparisonExpression $nullCompExpr): string + { + $expression = $nullCompExpr->expression; + $comparison = ' IS' . ($nullCompExpr->not ? ' NOT' : '') . ' NULL'; + + // Handle ResultVariable + if (is_string($expression) && isset($this->queryComponents[$expression]['resultVariable'])) { + return $this->walkResultVariable($expression) . $comparison; + } + + // Handle InputParameter mapping inclusion to ParserResult + if ($expression instanceof AST\InputParameter) { + return $this->walkInputParameter($expression) . $comparison; + } + + assert(! is_string($expression)); + + return $expression->dispatch($this) . $comparison; + } + + /** + * Walks down an InExpression AST node, thereby generating the appropriate SQL. + */ + public function walkInListExpression(AST\InListExpression $inExpr): string + { + return $this->walkArithmeticExpression($inExpr->expression) + . ($inExpr->not ? ' NOT' : '') . ' IN (' + . implode(', ', array_map($this->walkInParameter(...), $inExpr->literals)) + . ')'; + } + + /** + * Walks down an InExpression AST node, thereby generating the appropriate SQL. + */ + public function walkInSubselectExpression(AST\InSubselectExpression $inExpr): string + { + return $this->walkArithmeticExpression($inExpr->expression) + . ($inExpr->not ? ' NOT' : '') . ' IN (' + . $this->walkSubselect($inExpr->subselect) + . ')'; + } + + /** + * Walks down an InstanceOfExpression AST node, thereby generating the appropriate SQL. + * + * @throws QueryException + */ + public function walkInstanceOfExpression(AST\InstanceOfExpression $instanceOfExpr): string + { + $sql = ''; + + $dqlAlias = $instanceOfExpr->identificationVariable; + $discrClass = $class = $this->getMetadataForDqlAlias($dqlAlias); + + if ($class->discriminatorColumn) { + $discrClass = $this->em->getClassMetadata($class->rootEntityName); + } + + if ($this->useSqlTableAliases) { + $sql .= $this->getSQLTableAlias($discrClass->getTableName(), $dqlAlias) . '.'; + } + + $sql .= $class->getDiscriminatorColumn()->name . ($instanceOfExpr->not ? ' NOT IN ' : ' IN '); + $sql .= $this->getChildDiscriminatorsFromClassMetadata($discrClass, $instanceOfExpr); + + return $sql; + } + + public function walkInParameter(mixed $inParam): string + { + return $inParam instanceof AST\InputParameter + ? $this->walkInputParameter($inParam) + : $this->walkArithmeticExpression($inParam); + } + + /** + * Walks down a literal that represents an AST node, thereby generating the appropriate SQL. + */ + public function walkLiteral(AST\Literal $literal): string + { + return match ($literal->type) { + AST\Literal::STRING => $this->conn->quote($literal->value), + AST\Literal::BOOLEAN => (string) $this->conn->getDatabasePlatform()->convertBooleans(strtolower($literal->value) === 'true'), + AST\Literal::NUMERIC => (string) $literal->value, + default => throw QueryException::invalidLiteral($literal), + }; + } + + /** + * Walks down a BetweenExpression AST node, thereby generating the appropriate SQL. + */ + public function walkBetweenExpression(AST\BetweenExpression $betweenExpr): string + { + $sql = $this->walkArithmeticExpression($betweenExpr->expression); + + if ($betweenExpr->not) { + $sql .= ' NOT'; + } + + $sql .= ' BETWEEN ' . $this->walkArithmeticExpression($betweenExpr->leftBetweenExpression) + . ' AND ' . $this->walkArithmeticExpression($betweenExpr->rightBetweenExpression); + + return $sql; + } + + /** + * Walks down a LikeExpression AST node, thereby generating the appropriate SQL. + */ + public function walkLikeExpression(AST\LikeExpression $likeExpr): string + { + $stringExpr = $likeExpr->stringExpression; + if (is_string($stringExpr)) { + if (! isset($this->queryComponents[$stringExpr]['resultVariable'])) { + throw new LogicException(sprintf('No result variable found for string expression "%s".', $stringExpr)); + } + + $leftExpr = $this->walkResultVariable($stringExpr); + } else { + $leftExpr = $stringExpr->dispatch($this); + } + + $sql = $leftExpr . ($likeExpr->not ? ' NOT' : '') . ' LIKE '; + + if ($likeExpr->stringPattern instanceof AST\InputParameter) { + $sql .= $this->walkInputParameter($likeExpr->stringPattern); + } elseif ($likeExpr->stringPattern instanceof AST\Functions\FunctionNode) { + $sql .= $this->walkFunction($likeExpr->stringPattern); + } elseif ($likeExpr->stringPattern instanceof AST\PathExpression) { + $sql .= $this->walkPathExpression($likeExpr->stringPattern); + } else { + $sql .= $this->walkLiteral($likeExpr->stringPattern); + } + + if ($likeExpr->escapeChar) { + $sql .= ' ESCAPE ' . $this->walkLiteral($likeExpr->escapeChar); + } + + return $sql; + } + + /** + * Walks down a StateFieldPathExpression AST node, thereby generating the appropriate SQL. + */ + public function walkStateFieldPathExpression(AST\PathExpression $stateFieldPathExpression): string + { + return $this->walkPathExpression($stateFieldPathExpression); + } + + /** + * Walks down a ComparisonExpression AST node, thereby generating the appropriate SQL. + */ + public function walkComparisonExpression(AST\ComparisonExpression $compExpr): string + { + $leftExpr = $compExpr->leftExpression; + $rightExpr = $compExpr->rightExpression; + $sql = ''; + + $sql .= $leftExpr instanceof AST\Node + ? $leftExpr->dispatch($this) + : (is_numeric($leftExpr) ? $leftExpr : $this->conn->quote($leftExpr)); + + $sql .= ' ' . $compExpr->operator . ' '; + + $sql .= $rightExpr instanceof AST\Node + ? $rightExpr->dispatch($this) + : (is_numeric($rightExpr) ? $rightExpr : $this->conn->quote($rightExpr)); + + return $sql; + } + + /** + * Walks down an InputParameter AST node, thereby generating the appropriate SQL. + */ + public function walkInputParameter(AST\InputParameter $inputParam): string + { + $this->parserResult->addParameterMapping($inputParam->name, $this->sqlParamIndex++); + + $parameter = $this->query->getParameter($inputParam->name); + + if ($parameter) { + $type = $parameter->getType(); + if (is_string($type) && Type::hasType($type)) { + return Type::getType($type)->convertToDatabaseValueSQL('?', $this->platform); + } + } + + return '?'; + } + + /** + * Walks down an ArithmeticExpression AST node, thereby generating the appropriate SQL. + */ + public function walkArithmeticExpression(AST\ArithmeticExpression $arithmeticExpr): string + { + return $arithmeticExpr->isSimpleArithmeticExpression() + ? $this->walkSimpleArithmeticExpression($arithmeticExpr->simpleArithmeticExpression) + : '(' . $this->walkSubselect($arithmeticExpr->subselect) . ')'; + } + + /** + * Walks down an SimpleArithmeticExpression AST node, thereby generating the appropriate SQL. + */ + public function walkSimpleArithmeticExpression(AST\Node|string $simpleArithmeticExpr): string + { + if (! ($simpleArithmeticExpr instanceof AST\SimpleArithmeticExpression)) { + return $this->walkArithmeticTerm($simpleArithmeticExpr); + } + + return implode(' ', array_map($this->walkArithmeticTerm(...), $simpleArithmeticExpr->arithmeticTerms)); + } + + /** + * Walks down an ArithmeticTerm AST node, thereby generating the appropriate SQL. + */ + public function walkArithmeticTerm(AST\Node|string $term): string + { + if (is_string($term)) { + return isset($this->queryComponents[$term]) + ? $this->walkResultVariable($this->queryComponents[$term]['token']->value) + : $term; + } + + // Phase 2 AST optimization: Skip processing of ArithmeticTerm + // if only one ArithmeticFactor is defined + if (! ($term instanceof AST\ArithmeticTerm)) { + return $this->walkArithmeticFactor($term); + } + + return implode(' ', array_map($this->walkArithmeticFactor(...), $term->arithmeticFactors)); + } + + /** + * Walks down an ArithmeticFactor that represents an AST node, thereby generating the appropriate SQL. + */ + public function walkArithmeticFactor(AST\Node|string $factor): string + { + if (is_string($factor)) { + return isset($this->queryComponents[$factor]) + ? $this->walkResultVariable($this->queryComponents[$factor]['token']->value) + : $factor; + } + + // Phase 2 AST optimization: Skip processing of ArithmeticFactor + // if only one ArithmeticPrimary is defined + if (! ($factor instanceof AST\ArithmeticFactor)) { + return $this->walkArithmeticPrimary($factor); + } + + $sign = $factor->isNegativeSigned() ? '-' : ($factor->isPositiveSigned() ? '+' : ''); + + return $sign . $this->walkArithmeticPrimary($factor->arithmeticPrimary); + } + + /** + * Walks down an ArithmeticPrimary that represents an AST node, thereby generating the appropriate SQL. + */ + public function walkArithmeticPrimary(AST\Node|string $primary): string + { + if ($primary instanceof AST\SimpleArithmeticExpression) { + return '(' . $this->walkSimpleArithmeticExpression($primary) . ')'; + } + + if ($primary instanceof AST\Node) { + return $primary->dispatch($this); + } + + return $this->walkEntityIdentificationVariable($primary); + } + + /** + * Walks down a StringPrimary that represents an AST node, thereby generating the appropriate SQL. + */ + public function walkStringPrimary(AST\Node|string $stringPrimary): string + { + return is_string($stringPrimary) + ? $this->conn->quote($stringPrimary) + : $stringPrimary->dispatch($this); + } + + /** + * Walks down a ResultVariable that represents an AST node, thereby generating the appropriate SQL. + */ + public function walkResultVariable(string $resultVariable): string + { + if (! isset($this->scalarResultAliasMap[$resultVariable])) { + throw new InvalidArgumentException(sprintf('Unknown result variable: %s', $resultVariable)); + } + + $resultAlias = $this->scalarResultAliasMap[$resultVariable]; + + if (is_array($resultAlias)) { + return implode(', ', $resultAlias); + } + + return $resultAlias; + } + + /** + * @return string The list in parentheses of valid child discriminators from the given class + * + * @throws QueryException + */ + private function getChildDiscriminatorsFromClassMetadata( + ClassMetadata $rootClass, + AST\InstanceOfExpression $instanceOfExpr, + ): string { + $sqlParameterList = []; + $discriminators = []; + foreach ($instanceOfExpr->value as $parameter) { + if ($parameter instanceof AST\InputParameter) { + $this->rsm->discriminatorParameters[$parameter->name] = $parameter->name; + $sqlParameterList[] = $this->walkInParameter($parameter); + continue; + } + + $metadata = $this->em->getClassMetadata($parameter); + + if ($metadata->getName() !== $rootClass->name && ! $metadata->getReflectionClass()->isSubclassOf($rootClass->name)) { + throw QueryException::instanceOfUnrelatedClass($parameter, $rootClass->name); + } + + $discriminators += HierarchyDiscriminatorResolver::resolveDiscriminatorsForClass($metadata, $this->em); + } + + foreach (array_keys($discriminators) as $discriminatorValue) { + $sqlParameterList[] = $rootClass->getDiscriminatorColumn()->type === 'integer' && is_int($discriminatorValue) + ? $discriminatorValue + : $this->conn->quote((string) $discriminatorValue); + } + + return '(' . implode(', ', $sqlParameterList) . ')'; + } +} 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 @@ += 100 + case T_FULLY_QUALIFIED_NAME = 101; + case T_IDENTIFIER = 102; + + // All keyword tokens should be >= 200 + case T_ALL = 200; + case T_AND = 201; + case T_ANY = 202; + case T_AS = 203; + case T_ASC = 204; + case T_AVG = 205; + case T_BETWEEN = 206; + case T_BOTH = 207; + case T_BY = 208; + case T_CASE = 209; + case T_COALESCE = 210; + case T_COUNT = 211; + case T_DELETE = 212; + case T_DESC = 213; + case T_DISTINCT = 214; + case T_ELSE = 215; + case T_EMPTY = 216; + case T_END = 217; + case T_ESCAPE = 218; + case T_EXISTS = 219; + case T_FALSE = 220; + case T_FROM = 221; + case T_GROUP = 222; + case T_HAVING = 223; + case T_HIDDEN = 224; + case T_IN = 225; + case T_INDEX = 226; + case T_INNER = 227; + case T_INSTANCE = 228; + case T_IS = 229; + case T_JOIN = 230; + case T_LEADING = 231; + case T_LEFT = 232; + case T_LIKE = 233; + case T_MAX = 234; + case T_MEMBER = 235; + case T_MIN = 236; + case T_NEW = 237; + case T_NOT = 238; + case T_NULL = 239; + case T_NULLIF = 240; + case T_OF = 241; + case T_OR = 242; + case T_ORDER = 243; + case T_OUTER = 244; + case T_SELECT = 246; + case T_SET = 247; + case T_SOME = 248; + case T_SUM = 249; + case T_THEN = 250; + case T_TRAILING = 251; + case T_TRUE = 252; + case T_UPDATE = 253; + case T_WHEN = 254; + case T_WHERE = 255; + case T_WITH = 256; +} 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 @@ + $queryComponents The query components (symbol table). + */ + public function __construct(AbstractQuery $query, ParserResult $parserResult, array $queryComponents); + + /** + * Returns internal queryComponents array. + * + * @psalm-return array + */ + public function getQueryComponents(): array; + + /** + * Walks down a SelectStatement AST node. + */ + public function walkSelectStatement(AST\SelectStatement $selectStatement): void; + + /** + * Walks down an UpdateStatement AST node. + */ + public function walkUpdateStatement(AST\UpdateStatement $updateStatement): void; + + /** + * Walks down a DeleteStatement AST node. + */ + public function walkDeleteStatement(AST\DeleteStatement $deleteStatement): void; +} 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 @@ +queryComponents; + } + + public function walkSelectStatement(AST\SelectStatement $selectStatement): void + { + } + + public function walkUpdateStatement(AST\UpdateStatement $updateStatement): void + { + } + + public function walkDeleteStatement(AST\DeleteStatement $deleteStatement): void + { + } + + /** + * Sets or overrides a query component for a given dql alias. + * + * @psalm-param QueryComponent $queryComponent + */ + protected function setQueryComponent(string $dqlAlias, array $queryComponent): void + { + $requiredKeys = ['metadata', 'parent', 'relation', 'map', 'nestingLevel', 'token']; + + if (array_diff($requiredKeys, array_keys($queryComponent))) { + throw QueryException::invalidQueryComponent($dqlAlias); + } + + $this->queryComponents[$dqlAlias] = $queryComponent; + } + + /** + * Retrieves the Query Instance responsible for the current walkers execution. + */ + protected function _getQuery(): AbstractQuery + { + return $this->query; + } + + /** + * Retrieves the ParserResult. + */ + protected function _getParserResult(): ParserResult + { + return $this->parserResult; + } + + protected function getMetadataForDqlAlias(string $dqlAlias): ClassMetadata + { + return $this->queryComponents[$dqlAlias]['metadata'] + ?? throw new LogicException(sprintf('No metadata for DQL alias: %s', $dqlAlias)); + } +} 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 @@ +> + */ + private array $walkers = []; + + /** + * {@inheritDoc} + */ + public function __construct( + private readonly AbstractQuery $query, + private readonly ParserResult $parserResult, + private array $queryComponents, + ) { + } + + /** + * Returns the internal queryComponents array. + * + * {@inheritDoc} + */ + public function getQueryComponents(): array + { + return $this->queryComponents; + } + + /** + * Adds a tree walker to the chain. + * + * @param string $walkerClass The class of the walker to instantiate. + * @psalm-param class-string $walkerClass + */ + public function addTreeWalker(string $walkerClass): void + { + $this->walkers[] = $walkerClass; + } + + public function walkSelectStatement(AST\SelectStatement $selectStatement): void + { + foreach ($this->getWalkers() as $walker) { + $walker->walkSelectStatement($selectStatement); + + $this->queryComponents = $walker->getQueryComponents(); + } + } + + public function walkUpdateStatement(AST\UpdateStatement $updateStatement): void + { + foreach ($this->getWalkers() as $walker) { + $walker->walkUpdateStatement($updateStatement); + } + } + + public function walkDeleteStatement(AST\DeleteStatement $deleteStatement): void + { + foreach ($this->getWalkers() as $walker) { + $walker->walkDeleteStatement($deleteStatement); + } + } + + /** @psalm-return Generator */ + private function getWalkers(): Generator + { + foreach ($this->walkers as $walkerClass) { + yield new $walkerClass($this->query, $this->parserResult, $this->queryComponents); + } + } +} 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 @@ + + */ + private array $dqlParts = [ + 'distinct' => false, + 'select' => [], + 'from' => [], + 'join' => [], + 'set' => [], + 'where' => null, + 'groupBy' => [], + 'having' => null, + 'orderBy' => [], + ]; + + private QueryType $type = QueryType::Select; + + /** + * The complete DQL string for this query. + */ + private string|null $dql = null; + + /** + * The query parameters. + * + * @psalm-var ArrayCollection + */ + private ArrayCollection $parameters; + + /** + * The index of the first result to retrieve. + */ + private int $firstResult = 0; + + /** + * The maximum number of results to retrieve. + */ + private int|null $maxResults = null; + + /** + * Keeps root entity alias names for join entities. + * + * @psalm-var array + */ + private array $joinRootAliases = []; + + /** + * Whether to use second level cache, if available. + */ + protected bool $cacheable = false; + + /** + * Second level cache region name. + */ + protected string|null $cacheRegion = null; + + /** + * Second level query cache mode. + * + * @psalm-var Cache::MODE_*|null + */ + protected int|null $cacheMode = null; + + protected int $lifetime = 0; + + /** + * Initializes a new QueryBuilder that uses the given EntityManager. + * + * @param EntityManagerInterface $em The EntityManager to use. + */ + public function __construct( + private readonly EntityManagerInterface $em, + ) { + $this->parameters = new ArrayCollection(); + } + + /** + * Gets an ExpressionBuilder used for object-oriented construction of query expressions. + * This producer method is intended for convenient inline usage. Example: + * + * + * $qb = $em->createQueryBuilder(); + * $qb + * ->select('u') + * ->from('User', 'u') + * ->where($qb->expr()->eq('u.id', 1)); + * + * + * For more complex expression construction, consider storing the expression + * builder object in a local variable. + */ + public function expr(): Expr + { + return $this->em->getExpressionBuilder(); + } + + /** + * Enable/disable second level query (result) caching for this query. + * + * @return $this + */ + public function setCacheable(bool $cacheable): static + { + $this->cacheable = $cacheable; + + return $this; + } + + /** + * Are the query results enabled for second level cache? + */ + public function isCacheable(): bool + { + return $this->cacheable; + } + + /** @return $this */ + public function setCacheRegion(string $cacheRegion): static + { + $this->cacheRegion = $cacheRegion; + + return $this; + } + + /** + * Obtain the name of the second level query cache region in which query results will be stored + * + * @return string|null The cache region name; NULL indicates the default region. + */ + public function getCacheRegion(): string|null + { + return $this->cacheRegion; + } + + public function getLifetime(): int + { + return $this->lifetime; + } + + /** + * Sets the life-time for this query into second level cache. + * + * @return $this + */ + public function setLifetime(int $lifetime): static + { + $this->lifetime = $lifetime; + + return $this; + } + + /** @psalm-return Cache::MODE_*|null */ + public function getCacheMode(): int|null + { + return $this->cacheMode; + } + + /** + * @psalm-param Cache::MODE_* $cacheMode + * + * @return $this + */ + public function setCacheMode(int $cacheMode): static + { + $this->cacheMode = $cacheMode; + + return $this; + } + + /** + * Gets the associated EntityManager for this query builder. + */ + public function getEntityManager(): EntityManagerInterface + { + return $this->em; + } + + /** + * Gets the complete DQL string formed by the current specifications of this QueryBuilder. + * + * + * $qb = $em->createQueryBuilder() + * ->select('u') + * ->from('User', 'u'); + * echo $qb->getDql(); // SELECT u FROM User u + * + */ + public function getDQL(): string + { + return $this->dql ??= match ($this->type) { + QueryType::Select => $this->getDQLForSelect(), + QueryType::Delete => $this->getDQLForDelete(), + QueryType::Update => $this->getDQLForUpdate(), + }; + } + + /** + * Constructs a Query instance from the current specifications of the builder. + * + * + * $qb = $em->createQueryBuilder() + * ->select('u') + * ->from('User', 'u'); + * $q = $qb->getQuery(); + * $results = $q->execute(); + * + */ + public function getQuery(): Query + { + $parameters = clone $this->parameters; + $query = $this->em->createQuery($this->getDQL()) + ->setParameters($parameters) + ->setFirstResult($this->firstResult) + ->setMaxResults($this->maxResults); + + if ($this->lifetime) { + $query->setLifetime($this->lifetime); + } + + if ($this->cacheMode) { + $query->setCacheMode($this->cacheMode); + } + + if ($this->cacheable) { + $query->setCacheable($this->cacheable); + } + + if ($this->cacheRegion) { + $query->setCacheRegion($this->cacheRegion); + } + + return $query; + } + + /** + * Finds the root entity alias of the joined entity. + * + * @param string $alias The alias of the new join entity + * @param string $parentAlias The parent entity alias of the join relationship + */ + private function findRootAlias(string $alias, string $parentAlias): string + { + if (in_array($parentAlias, $this->getRootAliases(), true)) { + $rootAlias = $parentAlias; + } elseif (isset($this->joinRootAliases[$parentAlias])) { + $rootAlias = $this->joinRootAliases[$parentAlias]; + } else { + // Should never happen with correct joining order. Might be + // thoughtful to throw exception instead. + $rootAlias = $this->getRootAlias(); + } + + $this->joinRootAliases[$alias] = $rootAlias; + + return $rootAlias; + } + + /** + * Gets the FIRST root alias of the query. This is the first entity alias involved + * in the construction of the query. + * + * + * $qb = $em->createQueryBuilder() + * ->select('u') + * ->from('User', 'u'); + * + * echo $qb->getRootAlias(); // u + * + * + * @deprecated Please use $qb->getRootAliases() instead. + * + * @throws RuntimeException + */ + public function getRootAlias(): string + { + $aliases = $this->getRootAliases(); + + if (! isset($aliases[0])) { + throw new RuntimeException('No alias was set before invoking getRootAlias().'); + } + + return $aliases[0]; + } + + /** + * Gets the root aliases of the query. This is the entity aliases involved + * in the construction of the query. + * + * + * $qb = $em->createQueryBuilder() + * ->select('u') + * ->from('User', 'u'); + * + * $qb->getRootAliases(); // array('u') + * + * + * @return string[] + * @psalm-return list + */ + public function getRootAliases(): array + { + $aliases = []; + + foreach ($this->dqlParts['from'] as &$fromClause) { + if (is_string($fromClause)) { + $spacePos = strrpos($fromClause, ' '); + + /** @psalm-var class-string $from */ + $from = substr($fromClause, 0, $spacePos); + $alias = substr($fromClause, $spacePos + 1); + + $fromClause = new Query\Expr\From($from, $alias); + } + + $aliases[] = $fromClause->getAlias(); + } + + return $aliases; + } + + /** + * Gets all the aliases that have been used in the query. + * Including all select root aliases and join aliases + * + * + * $qb = $em->createQueryBuilder() + * ->select('u') + * ->from('User', 'u') + * ->join('u.articles','a'); + * + * $qb->getAllAliases(); // array('u','a') + * + * + * @return string[] + * @psalm-return list + */ + public function getAllAliases(): array + { + return [...$this->getRootAliases(), ...array_keys($this->joinRootAliases)]; + } + + /** + * Gets the root entities of the query. This is the entity classes involved + * in the construction of the query. + * + * + * $qb = $em->createQueryBuilder() + * ->select('u') + * ->from('User', 'u'); + * + * $qb->getRootEntities(); // array('User') + * + * + * @return string[] + * @psalm-return list + */ + public function getRootEntities(): array + { + $entities = []; + + foreach ($this->dqlParts['from'] as &$fromClause) { + if (is_string($fromClause)) { + $spacePos = strrpos($fromClause, ' '); + + /** @psalm-var class-string $from */ + $from = substr($fromClause, 0, $spacePos); + $alias = substr($fromClause, $spacePos + 1); + + $fromClause = new Query\Expr\From($from, $alias); + } + + $entities[] = $fromClause->getFrom(); + } + + return $entities; + } + + /** + * Sets a query parameter for the query being constructed. + * + * + * $qb = $em->createQueryBuilder() + * ->select('u') + * ->from('User', 'u') + * ->where('u.id = :user_id') + * ->setParameter('user_id', 1); + * + * + * @param string|int $key The parameter position or name. + * @param ParameterType|ArrayParameterType|string|int|null $type ParameterType::*, ArrayParameterType::* or \Doctrine\DBAL\Types\Type::* constant + * + * @return $this + */ + public function setParameter(string|int $key, mixed $value, ParameterType|ArrayParameterType|string|int|null $type = null): static + { + $existingParameter = $this->getParameter($key); + + if ($existingParameter !== null) { + $existingParameter->setValue($value, $type); + + return $this; + } + + $this->parameters->add(new Parameter($key, $value, $type)); + + return $this; + } + + /** + * Sets a collection of query parameters for the query being constructed. + * + * + * $qb = $em->createQueryBuilder() + * ->select('u') + * ->from('User', 'u') + * ->where('u.id = :user_id1 OR u.id = :user_id2') + * ->setParameters(new ArrayCollection(array( + * new Parameter('user_id1', 1), + * new Parameter('user_id2', 2) + * ))); + * + * + * @psalm-param ArrayCollection $parameters + * + * @return $this + */ + public function setParameters(ArrayCollection $parameters): static + { + $this->parameters = $parameters; + + return $this; + } + + /** + * Gets all defined query parameters for the query being constructed. + * + * @psalm-return ArrayCollection + */ + public function getParameters(): ArrayCollection + { + return $this->parameters; + } + + /** + * Gets a (previously set) query parameter of the query being constructed. + */ + public function getParameter(string|int $key): Parameter|null + { + $key = Parameter::normalizeName($key); + + $filteredParameters = $this->parameters->filter( + static fn (Parameter $parameter): bool => $key === $parameter->getName() + ); + + return ! $filteredParameters->isEmpty() ? $filteredParameters->first() : null; + } + + /** + * Sets the position of the first result to retrieve (the "offset"). + * + * @return $this + */ + public function setFirstResult(int|null $firstResult): static + { + $this->firstResult = (int) $firstResult; + + return $this; + } + + /** + * Gets the position of the first result the query object was set to retrieve (the "offset"). + */ + public function getFirstResult(): int + { + return $this->firstResult; + } + + /** + * Sets the maximum number of results to retrieve (the "limit"). + * + * @return $this + */ + public function setMaxResults(int|null $maxResults): static + { + $this->maxResults = $maxResults; + + return $this; + } + + /** + * Gets the maximum number of results the query object was set to retrieve (the "limit"). + * Returns NULL if {@link setMaxResults} was not applied to this query builder. + */ + public function getMaxResults(): int|null + { + return $this->maxResults; + } + + /** + * Either appends to or replaces a single, generic query part. + * + * The available parts are: 'select', 'from', 'join', 'set', 'where', + * 'groupBy', 'having' and 'orderBy'. + * + * @psalm-param string|object|list|array{join: array} $dqlPart + * + * @return $this + */ + public function add(string $dqlPartName, string|object|array $dqlPart, bool $append = false): static + { + if ($append && ($dqlPartName === 'where' || $dqlPartName === 'having')) { + throw new InvalidArgumentException( + "Using \$append = true does not have an effect with 'where' or 'having' " . + 'parts. See QueryBuilder#andWhere() for an example for correct usage.', + ); + } + + $isMultiple = is_array($this->dqlParts[$dqlPartName]) + && ! ($dqlPartName === 'join' && ! $append); + + // Allow adding any part retrieved from self::getDQLParts(). + if (is_array($dqlPart) && $dqlPartName !== 'join') { + $dqlPart = reset($dqlPart); + } + + // This is introduced for backwards compatibility reasons. + // TODO: Remove for 3.0 + if ($dqlPartName === 'join') { + $newDqlPart = []; + + foreach ($dqlPart as $k => $v) { + $k = is_numeric($k) ? $this->getRootAlias() : $k; + + $newDqlPart[$k] = $v; + } + + $dqlPart = $newDqlPart; + } + + if ($append && $isMultiple) { + if (is_array($dqlPart)) { + $key = key($dqlPart); + + $this->dqlParts[$dqlPartName][$key][] = $dqlPart[$key]; + } else { + $this->dqlParts[$dqlPartName][] = $dqlPart; + } + } else { + $this->dqlParts[$dqlPartName] = $isMultiple ? [$dqlPart] : $dqlPart; + } + + $this->dql = null; + + return $this; + } + + /** + * Specifies an item that is to be returned in the query result. + * Replaces any previously specified selections, if any. + * + * + * $qb = $em->createQueryBuilder() + * ->select('u', 'p') + * ->from('User', 'u') + * ->leftJoin('u.Phonenumbers', 'p'); + * + * + * @return $this + */ + public function select(mixed ...$select): static + { + self::validateVariadicParameter($select); + + $this->type = QueryType::Select; + + if ($select === []) { + return $this; + } + + return $this->add('select', new Expr\Select($select), false); + } + + /** + * Adds a DISTINCT flag to this query. + * + * + * $qb = $em->createQueryBuilder() + * ->select('u') + * ->distinct() + * ->from('User', 'u'); + * + * + * @return $this + */ + public function distinct(bool $flag = true): static + { + if ($this->dqlParts['distinct'] !== $flag) { + $this->dqlParts['distinct'] = $flag; + $this->dql = null; + } + + return $this; + } + + /** + * Adds an item that is to be returned in the query result. + * + * + * $qb = $em->createQueryBuilder() + * ->select('u') + * ->addSelect('p') + * ->from('User', 'u') + * ->leftJoin('u.Phonenumbers', 'p'); + * + * + * @return $this + */ + public function addSelect(mixed ...$select): static + { + self::validateVariadicParameter($select); + + $this->type = QueryType::Select; + + if ($select === []) { + return $this; + } + + return $this->add('select', new Expr\Select($select), true); + } + + /** + * Turns the query being built into a bulk delete query that ranges over + * a certain entity type. + * + * + * $qb = $em->createQueryBuilder() + * ->delete('User', 'u') + * ->where('u.id = :user_id') + * ->setParameter('user_id', 1); + * + * + * @param class-string|null $delete The class/type whose instances are subject to the deletion. + * @param string|null $alias The class/type alias used in the constructed query. + * + * @return $this + */ + public function delete(string|null $delete = null, string|null $alias = null): static + { + $this->type = QueryType::Delete; + + if (! $delete) { + return $this; + } + + if (! $alias) { + throw new InvalidArgumentException(sprintf( + '%s(): The alias for entity %s must not be omitted.', + __METHOD__, + $delete, + )); + } + + return $this->add('from', new Expr\From($delete, $alias)); + } + + /** + * Turns the query being built into a bulk update query that ranges over + * a certain entity type. + * + * + * $qb = $em->createQueryBuilder() + * ->update('User', 'u') + * ->set('u.password', '?1') + * ->where('u.id = ?2'); + * + * + * @param class-string|null $update The class/type whose instances are subject to the update. + * @param string|null $alias The class/type alias used in the constructed query. + * + * @return $this + */ + public function update(string|null $update = null, string|null $alias = null): static + { + $this->type = QueryType::Update; + + if (! $update) { + return $this; + } + + if (! $alias) { + throw new InvalidArgumentException(sprintf( + '%s(): The alias for entity %s must not be omitted.', + __METHOD__, + $update, + )); + } + + return $this->add('from', new Expr\From($update, $alias)); + } + + /** + * Creates and adds a query root corresponding to the entity identified by the given alias, + * forming a cartesian product with any existing query roots. + * + * + * $qb = $em->createQueryBuilder() + * ->select('u') + * ->from('User', 'u'); + * + * + * @param class-string $from The class name. + * @param string $alias The alias of the class. + * @param string|null $indexBy The index for the from. + * + * @return $this + */ + public function from(string $from, string $alias, string|null $indexBy = null): static + { + return $this->add('from', new Expr\From($from, $alias, $indexBy), true); + } + + /** + * Updates a query root corresponding to an entity setting its index by. This method is intended to be used with + * EntityRepository->createQueryBuilder(), which creates the initial FROM clause and do not allow you to update it + * setting an index by. + * + * + * $qb = $userRepository->createQueryBuilder('u') + * ->indexBy('u', 'u.id'); + * + * // Is equivalent to... + * + * $qb = $em->createQueryBuilder() + * ->select('u') + * ->from('User', 'u', 'u.id'); + * + * + * @return $this + * + * @throws Query\QueryException + */ + public function indexBy(string $alias, string $indexBy): static + { + $rootAliases = $this->getRootAliases(); + + if (! in_array($alias, $rootAliases, true)) { + throw new Query\QueryException( + sprintf('Specified root alias %s must be set before invoking indexBy().', $alias), + ); + } + + foreach ($this->dqlParts['from'] as &$fromClause) { + assert($fromClause instanceof Expr\From); + if ($fromClause->getAlias() !== $alias) { + continue; + } + + $fromClause = new Expr\From($fromClause->getFrom(), $fromClause->getAlias(), $indexBy); + } + + return $this; + } + + /** + * Creates and adds a join over an entity association to the query. + * + * The entities in the joined association will be fetched as part of the query + * result if the alias used for the joined association is placed in the select + * expressions. + * + * + * $qb = $em->createQueryBuilder() + * ->select('u') + * ->from('User', 'u') + * ->join('u.Phonenumbers', 'p', Expr\Join::WITH, 'p.is_primary = 1'); + * + * + * @psalm-param Expr\Join::ON|Expr\Join::WITH|null $conditionType + * + * @return $this + */ + public function join( + string $join, + string $alias, + string|null $conditionType = null, + string|Expr\Composite|Expr\Comparison|Expr\Func|null $condition = null, + string|null $indexBy = null, + ): static { + return $this->innerJoin($join, $alias, $conditionType, $condition, $indexBy); + } + + /** + * Creates and adds a join over an entity association to the query. + * + * The entities in the joined association will be fetched as part of the query + * result if the alias used for the joined association is placed in the select + * expressions. + * + * [php] + * $qb = $em->createQueryBuilder() + * ->select('u') + * ->from('User', 'u') + * ->innerJoin('u.Phonenumbers', 'p', Expr\Join::WITH, 'p.is_primary = 1'); + * + * @psalm-param Expr\Join::ON|Expr\Join::WITH|null $conditionType + * + * @return $this + */ + public function innerJoin( + string $join, + string $alias, + string|null $conditionType = null, + string|Expr\Composite|Expr\Comparison|Expr\Func|null $condition = null, + string|null $indexBy = null, + ): static { + $parentAlias = substr($join, 0, (int) strpos($join, '.')); + + $rootAlias = $this->findRootAlias($alias, $parentAlias); + + $join = new Expr\Join( + Expr\Join::INNER_JOIN, + $join, + $alias, + $conditionType, + $condition, + $indexBy, + ); + + return $this->add('join', [$rootAlias => $join], true); + } + + /** + * Creates and adds a left join over an entity association to the query. + * + * The entities in the joined association will be fetched as part of the query + * result if the alias used for the joined association is placed in the select + * expressions. + * + * + * $qb = $em->createQueryBuilder() + * ->select('u') + * ->from('User', 'u') + * ->leftJoin('u.Phonenumbers', 'p', Expr\Join::WITH, 'p.is_primary = 1'); + * + * + * @psalm-param Expr\Join::ON|Expr\Join::WITH|null $conditionType + * + * @return $this + */ + public function leftJoin( + string $join, + string $alias, + string|null $conditionType = null, + string|Expr\Composite|Expr\Comparison|Expr\Func|null $condition = null, + string|null $indexBy = null, + ): static { + $parentAlias = substr($join, 0, (int) strpos($join, '.')); + + $rootAlias = $this->findRootAlias($alias, $parentAlias); + + $join = new Expr\Join( + Expr\Join::LEFT_JOIN, + $join, + $alias, + $conditionType, + $condition, + $indexBy, + ); + + return $this->add('join', [$rootAlias => $join], true); + } + + /** + * Sets a new value for a field in a bulk update query. + * + * + * $qb = $em->createQueryBuilder() + * ->update('User', 'u') + * ->set('u.password', '?1') + * ->where('u.id = ?2'); + * + * + * @return $this + */ + public function set(string $key, mixed $value): static + { + return $this->add('set', new Expr\Comparison($key, Expr\Comparison::EQ, $value), true); + } + + /** + * Specifies one or more restrictions to the query result. + * Replaces any previously specified restrictions, if any. + * + * + * $qb = $em->createQueryBuilder() + * ->select('u') + * ->from('User', 'u') + * ->where('u.id = ?'); + * + * // You can optionally programmatically build and/or expressions + * $qb = $em->createQueryBuilder(); + * + * $or = $qb->expr()->orX(); + * $or->add($qb->expr()->eq('u.id', 1)); + * $or->add($qb->expr()->eq('u.id', 2)); + * + * $qb->update('User', 'u') + * ->set('u.password', '?') + * ->where($or); + * + * + * @return $this + */ + public function where(mixed ...$predicates): static + { + self::validateVariadicParameter($predicates); + + if (! (count($predicates) === 1 && $predicates[0] instanceof Expr\Composite)) { + $predicates = new Expr\Andx($predicates); + } + + return $this->add('where', $predicates); + } + + /** + * Adds one or more restrictions to the query results, forming a logical + * conjunction with any previously specified restrictions. + * + * + * $qb = $em->createQueryBuilder() + * ->select('u') + * ->from('User', 'u') + * ->where('u.username LIKE ?') + * ->andWhere('u.is_active = 1'); + * + * + * @see where() + * + * @return $this + */ + public function andWhere(mixed ...$where): static + { + self::validateVariadicParameter($where); + + $dql = $this->getDQLPart('where'); + + if ($dql instanceof Expr\Andx) { + $dql->addMultiple($where); + } else { + array_unshift($where, $dql); + $dql = new Expr\Andx($where); + } + + return $this->add('where', $dql); + } + + /** + * Adds one or more restrictions to the query results, forming a logical + * disjunction with any previously specified restrictions. + * + * + * $qb = $em->createQueryBuilder() + * ->select('u') + * ->from('User', 'u') + * ->where('u.id = 1') + * ->orWhere('u.id = 2'); + * + * + * @see where() + * + * @return $this + */ + public function orWhere(mixed ...$where): static + { + self::validateVariadicParameter($where); + + $dql = $this->getDQLPart('where'); + + if ($dql instanceof Expr\Orx) { + $dql->addMultiple($where); + } else { + array_unshift($where, $dql); + $dql = new Expr\Orx($where); + } + + return $this->add('where', $dql); + } + + /** + * Specifies a grouping over the results of the query. + * Replaces any previously specified groupings, if any. + * + * + * $qb = $em->createQueryBuilder() + * ->select('u') + * ->from('User', 'u') + * ->groupBy('u.id'); + * + * + * @return $this + */ + public function groupBy(string ...$groupBy): static + { + self::validateVariadicParameter($groupBy); + + return $this->add('groupBy', new Expr\GroupBy($groupBy)); + } + + /** + * Adds a grouping expression to the query. + * + * + * $qb = $em->createQueryBuilder() + * ->select('u') + * ->from('User', 'u') + * ->groupBy('u.lastLogin') + * ->addGroupBy('u.createdAt'); + * + * + * @return $this + */ + public function addGroupBy(string ...$groupBy): static + { + self::validateVariadicParameter($groupBy); + + return $this->add('groupBy', new Expr\GroupBy($groupBy), true); + } + + /** + * Specifies a restriction over the groups of the query. + * Replaces any previous having restrictions, if any. + * + * @return $this + */ + public function having(mixed ...$having): static + { + self::validateVariadicParameter($having); + + if (! (count($having) === 1 && ($having[0] instanceof Expr\Andx || $having[0] instanceof Expr\Orx))) { + $having = new Expr\Andx($having); + } + + return $this->add('having', $having); + } + + /** + * Adds a restriction over the groups of the query, forming a logical + * conjunction with any existing having restrictions. + * + * @return $this + */ + public function andHaving(mixed ...$having): static + { + self::validateVariadicParameter($having); + + $dql = $this->getDQLPart('having'); + + if ($dql instanceof Expr\Andx) { + $dql->addMultiple($having); + } else { + array_unshift($having, $dql); + $dql = new Expr\Andx($having); + } + + return $this->add('having', $dql); + } + + /** + * Adds a restriction over the groups of the query, forming a logical + * disjunction with any existing having restrictions. + * + * @return $this + */ + public function orHaving(mixed ...$having): static + { + self::validateVariadicParameter($having); + + $dql = $this->getDQLPart('having'); + + if ($dql instanceof Expr\Orx) { + $dql->addMultiple($having); + } else { + array_unshift($having, $dql); + $dql = new Expr\Orx($having); + } + + return $this->add('having', $dql); + } + + /** + * Specifies an ordering for the query results. + * Replaces any previously specified orderings, if any. + * + * @return $this + */ + public function orderBy(string|Expr\OrderBy $sort, string|null $order = null): static + { + $orderBy = $sort instanceof Expr\OrderBy ? $sort : new Expr\OrderBy($sort, $order); + + return $this->add('orderBy', $orderBy); + } + + /** + * Adds an ordering to the query results. + * + * @return $this + */ + public function addOrderBy(string|Expr\OrderBy $sort, string|null $order = null): static + { + $orderBy = $sort instanceof Expr\OrderBy ? $sort : new Expr\OrderBy($sort, $order); + + return $this->add('orderBy', $orderBy, true); + } + + /** + * Adds criteria to the query. + * + * Adds where expressions with AND operator. + * Adds orderings. + * Overrides firstResult and maxResults if they're set. + * + * @return $this + * + * @throws Query\QueryException + */ + public function addCriteria(Criteria $criteria): static + { + $allAliases = $this->getAllAliases(); + if (! isset($allAliases[0])) { + throw new Query\QueryException('No aliases are set before invoking addCriteria().'); + } + + $visitor = new QueryExpressionVisitor($this->getAllAliases()); + + $whereExpression = $criteria->getWhereExpression(); + if ($whereExpression) { + $this->andWhere($visitor->dispatch($whereExpression)); + foreach ($visitor->getParameters() as $parameter) { + $this->parameters->add($parameter); + } + } + + foreach ($criteria->orderings() as $sort => $order) { + $hasValidAlias = false; + foreach ($allAliases as $alias) { + if (str_starts_with($sort . '.', $alias . '.')) { + $hasValidAlias = true; + break; + } + } + + if (! $hasValidAlias) { + $sort = $allAliases[0] . '.' . $sort; + } + + $this->addOrderBy($sort, $order->value); + } + + // Overwrite limits only if they was set in criteria + $firstResult = $criteria->getFirstResult(); + if ($firstResult > 0) { + $this->setFirstResult($firstResult); + } + + $maxResults = $criteria->getMaxResults(); + if ($maxResults !== null) { + $this->setMaxResults($maxResults); + } + + return $this; + } + + /** + * Gets a query part by its name. + */ + public function getDQLPart(string $queryPartName): mixed + { + return $this->dqlParts[$queryPartName]; + } + + /** + * Gets all query parts. + * + * @psalm-return array $dqlParts + */ + public function getDQLParts(): array + { + return $this->dqlParts; + } + + private function getDQLForDelete(): string + { + return 'DELETE' + . $this->getReducedDQLQueryPart('from', ['pre' => ' ', 'separator' => ', ']) + . $this->getReducedDQLQueryPart('where', ['pre' => ' WHERE ']) + . $this->getReducedDQLQueryPart('orderBy', ['pre' => ' ORDER BY ', 'separator' => ', ']); + } + + private function getDQLForUpdate(): string + { + return 'UPDATE' + . $this->getReducedDQLQueryPart('from', ['pre' => ' ', 'separator' => ', ']) + . $this->getReducedDQLQueryPart('set', ['pre' => ' SET ', 'separator' => ', ']) + . $this->getReducedDQLQueryPart('where', ['pre' => ' WHERE ']) + . $this->getReducedDQLQueryPart('orderBy', ['pre' => ' ORDER BY ', 'separator' => ', ']); + } + + private function getDQLForSelect(): string + { + $dql = 'SELECT' + . ($this->dqlParts['distinct'] === true ? ' DISTINCT' : '') + . $this->getReducedDQLQueryPart('select', ['pre' => ' ', 'separator' => ', ']); + + $fromParts = $this->getDQLPart('from'); + $joinParts = $this->getDQLPart('join'); + $fromClauses = []; + + // Loop through all FROM clauses + if (! empty($fromParts)) { + $dql .= ' FROM '; + + foreach ($fromParts as $from) { + $fromClause = (string) $from; + + if ($from instanceof Expr\From && isset($joinParts[$from->getAlias()])) { + foreach ($joinParts[$from->getAlias()] as $join) { + $fromClause .= ' ' . ((string) $join); + } + } + + $fromClauses[] = $fromClause; + } + } + + $dql .= implode(', ', $fromClauses) + . $this->getReducedDQLQueryPart('where', ['pre' => ' WHERE ']) + . $this->getReducedDQLQueryPart('groupBy', ['pre' => ' GROUP BY ', 'separator' => ', ']) + . $this->getReducedDQLQueryPart('having', ['pre' => ' HAVING ']) + . $this->getReducedDQLQueryPart('orderBy', ['pre' => ' ORDER BY ', 'separator' => ', ']); + + return $dql; + } + + /** @psalm-param array $options */ + private function getReducedDQLQueryPart(string $queryPartName, array $options = []): string + { + $queryPart = $this->getDQLPart($queryPartName); + + if (empty($queryPart)) { + return $options['empty'] ?? ''; + } + + return ($options['pre'] ?? '') + . (is_array($queryPart) ? implode($options['separator'], $queryPart) : $queryPart) + . ($options['post'] ?? ''); + } + + /** + * Resets DQL parts. + * + * @param string[]|null $parts + * @psalm-param list|null $parts + * + * @return $this + */ + public function resetDQLParts(array|null $parts = null): static + { + if ($parts === null) { + $parts = array_keys($this->dqlParts); + } + + foreach ($parts as $part) { + $this->resetDQLPart($part); + } + + return $this; + } + + /** + * Resets single DQL part. + * + * @return $this + */ + public function resetDQLPart(string $part): static + { + $this->dqlParts[$part] = is_array($this->dqlParts[$part]) ? [] : null; + $this->dql = null; + + return $this; + } + + /** + * Gets a string representation of this QueryBuilder which corresponds to + * the final DQL query being constructed. + */ + public function __toString(): string + { + return $this->getDQL(); + } + + /** + * Deep clones all expression objects in the DQL parts. + * + * @return void + */ + public function __clone() + { + foreach ($this->dqlParts as $part => $elements) { + if (is_array($this->dqlParts[$part])) { + foreach ($this->dqlParts[$part] as $idx => $element) { + if (is_object($element)) { + $this->dqlParts[$part][$idx] = clone $element; + } + } + } elseif (is_object($elements)) { + $this->dqlParts[$part] = clone $elements; + } + } + + $parameters = []; + + foreach ($this->parameters as $parameter) { + $parameters[] = clone $parameter; + } + + $this->parameters = new ArrayCollection($parameters); + } +} 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 @@ + + */ + private array $repositoryList = []; + + public function getRepository(EntityManagerInterface $entityManager, string $entityName): EntityRepository + { + $repositoryHash = $entityManager->getClassMetadata($entityName)->getName() . spl_object_id($entityManager); + + return $this->repositoryList[$repositoryHash] ??= $this->createRepository($entityManager, $entityName); + } + + /** + * Create a new repository instance for an entity class. + * + * @param EntityManagerInterface $entityManager The EntityManager instance. + * @param string $entityName The name of the entity. + */ + private function createRepository( + EntityManagerInterface $entityManager, + string $entityName, + ): EntityRepository { + $metadata = $entityManager->getClassMetadata($entityName); + $repositoryClassName = $metadata->customRepositoryClassName + ?: $entityManager->getConfiguration()->getDefaultRepositoryClassName(); + + return new $repositoryClassName($entityManager, $metadata); + } +} 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 @@ + $entityName The name of the entity. + * + * @return EntityRepository + * + * @template T of object + */ + public function getRepository(EntityManagerInterface $entityManager, string $entityName): EntityRepository; +} 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 @@ +> + */ + private array $entityListeners = []; + + /** + * Adds an entity listener for a specific entity. + * + * @param class-string $entityClass The entity to attach the listener. + * @param class-string $listenerClass The listener class. + * @param Events::*|null $eventName The entity lifecycle event. + * @param non-falsy-string|null $listenerCallback The listener callback method or NULL to use $eventName. + */ + public function addEntityListener( + string $entityClass, + string $listenerClass, + string|null $eventName = null, + string|null $listenerCallback = null, + ): void { + $this->entityListeners[ltrim($entityClass, '\\')][] = [ + 'event' => $eventName, + 'class' => $listenerClass, + 'method' => $listenerCallback ?? $eventName, + ]; + } + + /** + * Processes event and attach the entity listener. + */ + public function loadClassMetadata(LoadClassMetadataEventArgs $event): void + { + $metadata = $event->getClassMetadata(); + + if (! isset($this->entityListeners[$metadata->name])) { + return; + } + + foreach ($this->entityListeners[$metadata->name] as $listener) { + if ($listener['event'] === null) { + EntityListenerBuilder::bindEntityListener($metadata, $listener['class']); + } else { + assert($listener['method'] !== null); + $metadata->addEntityListener($listener['event'], $listener['class'], $listener['method']); + } + } + } +} 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 @@ +getOption('em') === null + ? $this->entityManagerProvider->getDefaultManager() + : $this->entityManagerProvider->getManager($input->getOption('em')); + } +} 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 @@ +setName('orm:clear-cache:region:collection') + ->setDescription('Clear a second-level cache collection region') + ->addArgument('owner-class', InputArgument::OPTIONAL, 'The owner entity name.') + ->addArgument('association', InputArgument::OPTIONAL, 'The association collection name.') + ->addArgument('owner-id', InputArgument::OPTIONAL, 'The owner identifier.') + ->addOption('em', null, InputOption::VALUE_REQUIRED, 'Name of the entity manager to operate on') + ->addOption('all', null, InputOption::VALUE_NONE, 'If defined, all entity regions will be deleted/invalidated.') + ->addOption('flush', null, InputOption::VALUE_NONE, 'If defined, all cache entries will be flushed.') + ->setHelp(<<<'EOT' +The %command.name% command is meant to clear a second-level cache collection regions for an associated Entity Manager. +It is possible to delete/invalidate all collection region, a specific collection region or flushes the cache provider. + +The execution type differ on how you execute the command. +If you want to invalidate all entries for an collection region this command would do the work: + +%command.name% 'Entities\MyEntity' 'collectionName' + +To invalidate a specific entry you should use : + +%command.name% 'Entities\MyEntity' 'collectionName' 1 + +If you want to invalidate all entries for the all collection regions: + +%command.name% --all + +Alternatively, if you want to flush the configured cache provider for an collection region use this command: + +%command.name% 'Entities\MyEntity' 'collectionName' --flush + +Finally, be aware that if --flush option is passed, +not all cache providers are able to flush entries, because of a limitation of its execution nature. +EOT); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $ui = (new SymfonyStyle($input, $output))->getErrorStyle(); + + $em = $this->getEntityManager($input); + $ownerClass = $input->getArgument('owner-class'); + $assoc = $input->getArgument('association'); + $ownerId = $input->getArgument('owner-id'); + $cache = $em->getCache(); + + if (! $cache instanceof Cache) { + throw new InvalidArgumentException('No second-level cache is configured on the given EntityManager.'); + } + + if (( ! $ownerClass || ! $assoc) && ! $input->getOption('all')) { + throw new InvalidArgumentException('Missing arguments "--owner-class" "--association"'); + } + + if ($input->getOption('flush')) { + $cache->getCollectionCacheRegion($ownerClass, $assoc) + ->evictAll(); + + $ui->comment( + sprintf( + 'Flushing cache provider configured for "%s#%s"', + $ownerClass, + $assoc, + ), + ); + + return 0; + } + + if ($input->getOption('all')) { + $ui->comment('Clearing all second-level cache collection regions'); + + $cache->evictEntityRegions(); + + return 0; + } + + if ($ownerId) { + $ui->comment( + sprintf( + 'Clearing second-level cache entry for collection "%s#%s" owner entity identified by "%s"', + $ownerClass, + $assoc, + $ownerId, + ), + ); + $cache->evictCollection($ownerClass, $assoc, $ownerId); + + return 0; + } + + $ui->comment(sprintf('Clearing second-level cache for collection "%s#%s"', $ownerClass, $assoc)); + $cache->evictCollectionRegion($ownerClass, $assoc); + + return 0; + } +} 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 @@ +setName('orm:clear-cache:region:entity') + ->setDescription('Clear a second-level cache entity region') + ->addArgument('entity-class', InputArgument::OPTIONAL, 'The entity name.') + ->addArgument('entity-id', InputArgument::OPTIONAL, 'The entity identifier.') + ->addOption('em', null, InputOption::VALUE_REQUIRED, 'Name of the entity manager to operate on') + ->addOption('all', null, InputOption::VALUE_NONE, 'If defined, all entity regions will be deleted/invalidated.') + ->addOption('flush', null, InputOption::VALUE_NONE, 'If defined, all cache entries will be flushed.') + ->setHelp(<<<'EOT' +The %command.name% command is meant to clear a second-level cache entity region for an associated Entity Manager. +It is possible to delete/invalidate all entity region, a specific entity region or flushes the cache provider. + +The execution type differ on how you execute the command. +If you want to invalidate all entries for an entity region this command would do the work: + +%command.name% 'Entities\MyEntity' + +To invalidate a specific entry you should use : + +%command.name% 'Entities\MyEntity' 1 + +If you want to invalidate all entries for the all entity regions: + +%command.name% --all + +Alternatively, if you want to flush the configured cache provider for an entity region use this command: + +%command.name% 'Entities\MyEntity' --flush + +Finally, be aware that if --flush option is passed, +not all cache providers are able to flush entries, because of a limitation of its execution nature. +EOT); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $ui = (new SymfonyStyle($input, $output))->getErrorStyle(); + + $em = $this->getEntityManager($input); + $entityClass = $input->getArgument('entity-class'); + $entityId = $input->getArgument('entity-id'); + $cache = $em->getCache(); + + if (! $cache instanceof Cache) { + throw new InvalidArgumentException('No second-level cache is configured on the given EntityManager.'); + } + + if (! $entityClass && ! $input->getOption('all')) { + throw new InvalidArgumentException('Invalid argument "--entity-class"'); + } + + if ($input->getOption('flush')) { + $cache->getEntityCacheRegion($entityClass) + ->evictAll(); + + $ui->comment(sprintf('Flushing cache provider configured for entity named "%s"', $entityClass)); + + return 0; + } + + if ($input->getOption('all')) { + $ui->comment('Clearing all second-level cache entity regions'); + + $cache->evictEntityRegions(); + + return 0; + } + + if ($entityId) { + $ui->comment( + sprintf( + 'Clearing second-level cache entry for entity "%s" identified by "%s"', + $entityClass, + $entityId, + ), + ); + $cache->evictEntity($entityClass, $entityId); + + return 0; + } + + $ui->comment(sprintf('Clearing second-level cache for entity "%s"', $entityClass)); + $cache->evictEntityRegion($entityClass); + + return 0; + } +} 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 @@ +setName('orm:clear-cache:metadata') + ->setDescription('Clear all metadata cache of the various cache drivers') + ->addOption('em', null, InputOption::VALUE_REQUIRED, 'Name of the entity manager to operate on') + ->addOption('flush', null, InputOption::VALUE_NONE, 'If defined, cache entries will be flushed instead of deleted/invalidated.') + ->setHelp(<<<'EOT' +The %command.name% command is meant to clear the metadata cache of associated Entity Manager. +EOT); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $ui = (new SymfonyStyle($input, $output))->getErrorStyle(); + + $em = $this->getEntityManager($input); + $cacheDriver = $em->getConfiguration()->getMetadataCache(); + + if (! $cacheDriver) { + throw new InvalidArgumentException('No Metadata cache driver is configured on given EntityManager.'); + } + + $ui->comment('Clearing all Metadata cache entries'); + + $result = $cacheDriver->clear(); + $message = $result ? 'Successfully deleted cache entries.' : 'No cache entries were deleted.'; + + $ui->success($message); + + return 0; + } +} 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 @@ +setName('orm:clear-cache:query') + ->setDescription('Clear all query cache of the various cache drivers') + ->addOption('em', null, InputOption::VALUE_REQUIRED, 'Name of the entity manager to operate on') + ->setHelp('The %command.name% command is meant to clear the query cache of associated Entity Manager.'); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $ui = (new SymfonyStyle($input, $output))->getErrorStyle(); + + $em = $this->getEntityManager($input); + $cache = $em->getConfiguration()->getQueryCache(); + + if (! $cache) { + throw new InvalidArgumentException('No Query cache driver is configured on given EntityManager.'); + } + + if ($cache instanceof ApcuAdapter) { + throw new LogicException('Cannot clear APCu Cache from Console, it\'s shared in the Webserver memory and not accessible from the CLI.'); + } + + $ui->comment('Clearing all Query cache entries'); + + $message = $cache->clear() ? 'Successfully deleted cache entries.' : 'No cache entries were deleted.'; + + $ui->success($message); + + return 0; + } +} 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 @@ +setName('orm:clear-cache:region:query') + ->setDescription('Clear a second-level cache query region') + ->addArgument('region-name', InputArgument::OPTIONAL, 'The query region to clear.') + ->addOption('em', null, InputOption::VALUE_REQUIRED, 'Name of the entity manager to operate on') + ->addOption('all', null, InputOption::VALUE_NONE, 'If defined, all query regions will be deleted/invalidated.') + ->addOption('flush', null, InputOption::VALUE_NONE, 'If defined, all cache entries will be flushed.') + ->setHelp(<<<'EOT' +The %command.name% command is meant to clear a second-level cache query region for an associated Entity Manager. +It is possible to delete/invalidate all query region, a specific query region or flushes the cache provider. + +The execution type differ on how you execute the command. +If you want to invalidate all entries for the default query region this command would do the work: + +%command.name% + +To invalidate entries for a specific query region you should use : + +%command.name% my_region_name + +If you want to invalidate all entries for the all query region: + +%command.name% --all + +Alternatively, if you want to flush the configured cache provider use this command: + +%command.name% my_region_name --flush + +Finally, be aware that if --flush option is passed, +not all cache providers are able to flush entries, because of a limitation of its execution nature. +EOT); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $ui = (new SymfonyStyle($input, $output))->getErrorStyle(); + + $em = $this->getEntityManager($input); + $name = $input->getArgument('region-name'); + $cache = $em->getCache(); + + if ($name === null) { + $name = Cache::DEFAULT_QUERY_REGION_NAME; + } + + if (! $cache instanceof Cache) { + throw new InvalidArgumentException('No second-level cache is configured on the given EntityManager.'); + } + + if ($input->getOption('flush')) { + $cache->getQueryCache($name) + ->getRegion() + ->evictAll(); + + $ui->comment( + sprintf( + 'Flushing cache provider configured for second-level cache query region named "%s"', + $name, + ), + ); + + return 0; + } + + if ($input->getOption('all')) { + $ui->comment('Clearing all second-level cache query regions'); + + $cache->evictQueryRegions(); + + return 0; + } + + $ui->comment(sprintf('Clearing second-level cache query region named "%s"', $name)); + $cache->evictQueryRegion($name); + + return 0; + } +} 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 @@ +setName('orm:clear-cache:result') + ->setDescription('Clear all result cache of the various cache drivers') + ->addOption('em', null, InputOption::VALUE_REQUIRED, 'Name of the entity manager to operate on') + ->addOption('flush', null, InputOption::VALUE_NONE, 'If defined, cache entries will be flushed instead of deleted/invalidated.') + ->setHelp(<<<'EOT' +The %command.name% command is meant to clear the result cache of associated Entity Manager. +It is possible to invalidate all cache entries at once - called delete -, or flushes the cache provider +instance completely. + +The execution type differ on how you execute the command. +If you want to invalidate the entries (and not delete from cache instance), this command would do the work: + +%command.name% + +Alternatively, if you want to flush the cache provider using this command: + +%command.name% --flush + +Finally, be aware that if --flush option is passed, not all cache providers are able to flush entries, +because of a limitation of its execution nature. +EOT); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $ui = (new SymfonyStyle($input, $output))->getErrorStyle(); + + $em = $this->getEntityManager($input); + $cache = $em->getConfiguration()->getResultCache(); + + if (! $cache) { + throw new InvalidArgumentException('No Result cache driver is configured on given EntityManager.'); + } + + $ui->comment('Clearing all Result cache entries'); + + $message = $cache->clear() ? 'Successfully deleted cache entries.' : 'No cache entries were deleted.'; + + $ui->success($message); + + return 0; + } +} 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 @@ +setName('orm:generate-proxies') + ->setAliases(['orm:generate:proxies']) + ->setDescription('Generates proxy classes for entity classes') + ->addArgument('dest-path', InputArgument::OPTIONAL, 'The path to generate your proxy classes. If none is provided, it will attempt to grab from configuration.') + ->addOption('em', null, InputOption::VALUE_REQUIRED, 'Name of the entity manager to operate on') + ->addOption('filter', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'A string pattern used to match entities that should be processed.') + ->setHelp('Generates proxy classes for entity classes.'); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $ui = (new SymfonyStyle($input, $output))->getErrorStyle(); + + $em = $this->getEntityManager($input); + + $metadatas = $em->getMetadataFactory()->getAllMetadata(); + $metadatas = MetadataFilter::filter($metadatas, $input->getOption('filter')); + + // Process destination directory + $destPath = $input->getArgument('dest-path'); + if ($destPath === null) { + $destPath = $em->getConfiguration()->getProxyDir(); + + if ($destPath === null) { + throw new InvalidArgumentException('Proxy directory cannot be null'); + } + } + + if (! is_dir($destPath)) { + mkdir($destPath, 0775, true); + } + + $destPath = realpath($destPath); + + if (! file_exists($destPath)) { + throw new InvalidArgumentException( + sprintf("Proxies destination directory '%s' does not exist.", $em->getConfiguration()->getProxyDir()), + ); + } + + if (! is_writable($destPath)) { + throw new InvalidArgumentException( + sprintf("Proxies destination directory '%s' does not have write permissions.", $destPath), + ); + } + + if (empty($metadatas)) { + $ui->success('No Metadata Classes to process.'); + + return 0; + } + + foreach ($metadatas as $metadata) { + $ui->text(sprintf('Processing entity "%s"', $metadata->name)); + } + + // Generating Proxies + $em->getProxyFactory()->generateProxyClasses($metadatas, $destPath); + + // Outputting information message + $ui->newLine(); + $ui->text(sprintf('Proxy classes generated to "%s"', $destPath)); + + return 0; + } +} 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 @@ +setName('orm:info') + ->setDescription('Show basic information about all mapped entities') + ->addOption('em', null, InputOption::VALUE_REQUIRED, 'Name of the entity manager to operate on') + ->setHelp(<<<'EOT' +The %command.name% shows basic information about which +entities exist and possibly if their mapping information contains errors or +not. +EOT); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $ui = (new SymfonyStyle($input, $output))->getErrorStyle(); + + $entityManager = $this->getEntityManager($input); + + $entityClassNames = $entityManager->getConfiguration() + ->getMetadataDriverImpl() + ->getAllClassNames(); + + if (! $entityClassNames) { + $ui->caution( + [ + 'You do not have any mapped Doctrine ORM entities according to the current configuration.', + 'If you have entities or mapping files you should check your mapping configuration for errors.', + ], + ); + + return 1; + } + + $ui->text(sprintf('Found %d mapped entities:', count($entityClassNames))); + $ui->newLine(); + + $failure = false; + + foreach ($entityClassNames as $entityClassName) { + try { + $entityManager->getClassMetadata($entityClassName); + $ui->text(sprintf('[OK] %s', $entityClassName)); + } catch (MappingException $e) { + $ui->text( + [ + sprintf('[FAIL] %s', $entityClassName), + sprintf('%s', $e->getMessage()), + '', + ], + ); + + $failure = true; + } + } + + return $failure ? 1 : 0; + } +} 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 @@ +setName('orm:mapping:describe') + ->addArgument('entityName', InputArgument::REQUIRED, 'Full or partial name of entity') + ->setDescription('Display information about mapped objects') + ->addOption('em', null, InputOption::VALUE_REQUIRED, 'Name of the entity manager to operate on') + ->setHelp(<<<'EOT' +The %command.full_name% command describes the metadata for the given full or partial entity class name. + + %command.full_name% My\Namespace\Entity\MyEntity + +Or: + + %command.full_name% MyEntity +EOT); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $ui = (new SymfonyStyle($input, $output))->getErrorStyle(); + + $entityManager = $this->getEntityManager($input); + + $this->displayEntity($input->getArgument('entityName'), $entityManager, $ui); + + return 0; + } + + /** + * Display all the mapping information for a single Entity. + * + * @param string $entityName Full or partial entity class name + */ + private function displayEntity( + string $entityName, + EntityManagerInterface $entityManager, + SymfonyStyle $ui, + ): void { + $metadata = $this->getClassMetadata($entityName, $entityManager); + + $ui->table( + ['Field', 'Value'], + array_merge( + [ + $this->formatField('Name', $metadata->name), + $this->formatField('Root entity name', $metadata->rootEntityName), + $this->formatField('Custom generator definition', $metadata->customGeneratorDefinition), + $this->formatField('Custom repository class', $metadata->customRepositoryClassName), + $this->formatField('Mapped super class?', $metadata->isMappedSuperclass), + $this->formatField('Embedded class?', $metadata->isEmbeddedClass), + $this->formatField('Parent classes', $metadata->parentClasses), + $this->formatField('Sub classes', $metadata->subClasses), + $this->formatField('Embedded classes', $metadata->subClasses), + $this->formatField('Identifier', $metadata->identifier), + $this->formatField('Inheritance type', $metadata->inheritanceType), + $this->formatField('Discriminator column', $metadata->discriminatorColumn), + $this->formatField('Discriminator value', $metadata->discriminatorValue), + $this->formatField('Discriminator map', $metadata->discriminatorMap), + $this->formatField('Generator type', $metadata->generatorType), + $this->formatField('Table', $metadata->table), + $this->formatField('Composite identifier?', $metadata->isIdentifierComposite), + $this->formatField('Foreign identifier?', $metadata->containsForeignIdentifier), + $this->formatField('Enum identifier?', $metadata->containsEnumIdentifier), + $this->formatField('Sequence generator definition', $metadata->sequenceGeneratorDefinition), + $this->formatField('Change tracking policy', $metadata->changeTrackingPolicy), + $this->formatField('Versioned?', $metadata->isVersioned), + $this->formatField('Version field', $metadata->versionField), + $this->formatField('Read only?', $metadata->isReadOnly), + + $this->formatEntityListeners($metadata->entityListeners), + ], + [$this->formatField('Association mappings:', '')], + $this->formatMappings($metadata->associationMappings), + [$this->formatField('Field mappings:', '')], + $this->formatMappings($metadata->fieldMappings), + ), + ); + } + + /** + * Return all mapped entity class names + * + * @return string[] + * @psalm-return class-string[] + */ + private function getMappedEntities(EntityManagerInterface $entityManager): array + { + $entityClassNames = $entityManager->getConfiguration() + ->getMetadataDriverImpl() + ->getAllClassNames(); + + if (! $entityClassNames) { + throw new InvalidArgumentException( + 'You do not have any mapped Doctrine ORM entities according to the current configuration. ' . + 'If you have entities or mapping files you should check your mapping configuration for errors.', + ); + } + + return $entityClassNames; + } + + /** + * Return the class metadata for the given entity + * name + * + * @param string $entityName Full or partial entity name + */ + private function getClassMetadata( + string $entityName, + EntityManagerInterface $entityManager, + ): ClassMetadata { + try { + return $entityManager->getClassMetadata($entityName); + } catch (MappingException) { + } + + $matches = array_filter( + $this->getMappedEntities($entityManager), + static fn ($mappedEntity) => preg_match('{' . preg_quote($entityName) . '}', $mappedEntity) + ); + + if (! $matches) { + throw new InvalidArgumentException(sprintf( + 'Could not find any mapped Entity classes matching "%s"', + $entityName, + )); + } + + if (count($matches) > 1) { + throw new InvalidArgumentException(sprintf( + 'Entity name "%s" is ambiguous, possible matches: "%s"', + $entityName, + implode(', ', $matches), + )); + } + + return $entityManager->getClassMetadata(current($matches)); + } + + /** + * Format the given value for console output + */ + private function formatValue(mixed $value): string + { + if ($value === '') { + return ''; + } + + if ($value === null) { + return 'Null'; + } + + if (is_bool($value)) { + return '' . ($value ? 'True' : 'False') . ''; + } + + if (empty($value)) { + return 'Empty'; + } + + if (is_array($value)) { + return json_encode( + $value, + JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR, + ); + } + + if (is_object($value)) { + return sprintf('<%s>', get_debug_type($value)); + } + + if (is_scalar($value)) { + return (string) $value; + } + + throw new InvalidArgumentException(sprintf('Do not know how to format value "%s"', print_r($value, true))); + } + + /** + * Add the given label and value to the two column table output + * + * @param string $label Label for the value + * @param mixed $value A Value to show + * + * @return string[] + * @psalm-return array{0: string, 1: string} + */ + private function formatField(string $label, mixed $value): array + { + if ($value === null) { + $value = 'None'; + } + + return [sprintf('%s', $label), $this->formatValue($value)]; + } + + /** + * Format the association mappings + * + * @psalm-param array $propertyMappings + * + * @return string[][] + * @psalm-return list + */ + private function formatMappings(array $propertyMappings): array + { + $output = []; + + foreach ($propertyMappings as $propertyName => $mapping) { + $output[] = $this->formatField(sprintf(' %s', $propertyName), ''); + + foreach ((array) $mapping as $field => $value) { + $output[] = $this->formatField(sprintf(' %s', $field), $this->formatValue($value)); + } + } + + return $output; + } + + /** + * Format the entity listeners + * + * @psalm-param list $entityListeners + * + * @return string[] + * @psalm-return array{0: string, 1: string} + */ + private function formatEntityListeners(array $entityListeners): array + { + return $this->formatField('Entity listeners', array_map('get_class', $entityListeners)); + } +} 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 @@ +setName('orm:run-dql') + ->setDescription('Executes arbitrary DQL directly from the command line') + ->addArgument('dql', InputArgument::REQUIRED, 'The DQL to execute.') + ->addOption('em', null, InputOption::VALUE_REQUIRED, 'Name of the entity manager to operate on') + ->addOption('hydrate', null, InputOption::VALUE_REQUIRED, 'Hydration mode of result set. Should be either: object, array, scalar or single-scalar.', 'object') + ->addOption('first-result', null, InputOption::VALUE_REQUIRED, 'The first result in the result set.') + ->addOption('max-result', null, InputOption::VALUE_REQUIRED, 'The maximum number of results in the result set.') + ->addOption('depth', null, InputOption::VALUE_REQUIRED, 'Dumping depth of Entity graph.', 7) + ->addOption('show-sql', null, InputOption::VALUE_NONE, 'Dump generated SQL instead of executing query') + ->setHelp(<<<'EOT' + The %command.name% command executes the given DQL query and + outputs the results: + + php %command.full_name% "SELECT u FROM App\Entity\User u" + + You can also optionally specify some additional options like what type of + hydration to use when executing the query: + + php %command.full_name% "SELECT u FROM App\Entity\User u" --hydrate=array + + Additionally you can specify the first result and maximum amount of results to + show: + + php %command.full_name% "SELECT u FROM App\Entity\User u" --first-result=0 --max-result=30 + EOT); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $ui = new SymfonyStyle($input, $output); + + $em = $this->getEntityManager($input); + + $dql = $input->getArgument('dql'); + if ($dql === null) { + throw new RuntimeException("Argument 'dql' is required in order to execute this command correctly."); + } + + $depth = $input->getOption('depth'); + + if (! is_numeric($depth)) { + throw new LogicException("Option 'depth' must contain an integer value"); + } + + $hydrationModeName = (string) $input->getOption('hydrate'); + $hydrationMode = 'Doctrine\ORM\Query::HYDRATE_' . strtoupper(str_replace('-', '_', $hydrationModeName)); + + if (! defined($hydrationMode)) { + throw new RuntimeException(sprintf( + "Hydration mode '%s' does not exist. It should be either: object. array, scalar or single-scalar.", + $hydrationModeName, + )); + } + + $query = $em->createQuery($dql); + + $firstResult = $input->getOption('first-result'); + if ($firstResult !== null) { + if (! is_numeric($firstResult)) { + throw new LogicException("Option 'first-result' must contain an integer value"); + } + + $query->setFirstResult((int) $firstResult); + } + + $maxResult = $input->getOption('max-result'); + if ($maxResult !== null) { + if (! is_numeric($maxResult)) { + throw new LogicException("Option 'max-result' must contain an integer value"); + } + + $query->setMaxResults((int) $maxResult); + } + + if ($input->getOption('show-sql')) { + $ui->text($query->getSQL()); + + return 0; + } + + $resultSet = $query->execute([], constant($hydrationMode)); + + $ui->text(Debug::dump($resultSet, (int) $input->getOption('depth'))); + + return 0; + } +} 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 @@ +getEntityManager($input); + + $metadatas = $em->getMetadataFactory()->getAllMetadata(); + + if (empty($metadatas)) { + $ui->getErrorStyle()->success('No Metadata Classes to process.'); + + return 0; + } + + return $this->executeSchemaCommand($input, $output, new SchemaTool($em), $metadatas, $ui); + } +} 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 @@ +setName('orm:schema-tool:create') + ->setDescription('Processes the schema and either create it directly on EntityManager Storage Connection or generate the SQL output') + ->addOption('em', null, InputOption::VALUE_REQUIRED, 'Name of the entity manager to operate on') + ->addOption('dump-sql', null, InputOption::VALUE_NONE, 'Instead of trying to apply generated SQLs into EntityManager Storage Connection, output them.') + ->setHelp(<<<'EOT' +Processes the schema and either create it directly on EntityManager Storage Connection or generate the SQL output. + +Hint: If you have a database with tables that should not be managed +by the ORM, you can use a DBAL functionality to filter the tables and sequences down +on a global level: + + $config->setSchemaAssetsFilter(function (string|AbstractAsset $assetName): bool { + if ($assetName instanceof AbstractAsset) { + $assetName = $assetName->getName(); + } + + return !str_starts_with($assetName, 'audit_'); + }); +EOT); + } + + /** + * {@inheritDoc} + */ + protected function executeSchemaCommand(InputInterface $input, OutputInterface $output, SchemaTool $schemaTool, array $metadatas, SymfonyStyle $ui): int + { + $dumpSql = $input->getOption('dump-sql') === true; + + if ($dumpSql) { + $sqls = $schemaTool->getCreateSchemaSql($metadatas); + + foreach ($sqls as $sql) { + $ui->writeln(sprintf('%s;', $sql)); + } + + return 0; + } + + $notificationUi = $ui->getErrorStyle(); + + $notificationUi->caution('This operation should not be executed in a production environment!'); + + $notificationUi->text('Creating database schema...'); + $notificationUi->newLine(); + + $schemaTool->createSchema($metadatas); + + $notificationUi->success('Database schema created successfully!'); + + return 0; + } +} 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 @@ +setName('orm:schema-tool:drop') + ->setDescription('Drop the complete database schema of EntityManager Storage Connection or generate the corresponding SQL output') + ->addOption('em', null, InputOption::VALUE_REQUIRED, 'Name of the entity manager to operate on') + ->addOption('dump-sql', null, InputOption::VALUE_NONE, 'Instead of trying to apply generated SQLs into EntityManager Storage Connection, output them.') + ->addOption('force', 'f', InputOption::VALUE_NONE, "Don't ask for the deletion of the database, but force the operation to run.") + ->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.') + ->setHelp(<<<'EOT' +Processes the schema and either drop the database schema of EntityManager Storage Connection or generate the SQL output. +Beware that the complete database is dropped by this command, even tables that are not relevant to your metadata model. + +Hint: If you have a database with tables that should not be managed +by the ORM, you can use a DBAL functionality to filter the tables and sequences down +on a global level: + + $config->setSchemaAssetsFilter(function (string|AbstractAsset $assetName): bool { + if ($assetName instanceof AbstractAsset) { + $assetName = $assetName->getName(); + } + + return !str_starts_with($assetName, 'audit_'); + }); +EOT); + } + + /** + * {@inheritDoc} + */ + protected function executeSchemaCommand(InputInterface $input, OutputInterface $output, SchemaTool $schemaTool, array $metadatas, SymfonyStyle $ui): int + { + $isFullDatabaseDrop = $input->getOption('full-database'); + $dumpSql = $input->getOption('dump-sql') === true; + $force = $input->getOption('force') === true; + + if ($dumpSql) { + if ($isFullDatabaseDrop) { + $sqls = $schemaTool->getDropDatabaseSQL(); + } else { + $sqls = $schemaTool->getDropSchemaSQL($metadatas); + } + + foreach ($sqls as $sql) { + $ui->writeln(sprintf('%s;', $sql)); + } + + return 0; + } + + $notificationUi = $ui->getErrorStyle(); + + if ($force) { + $notificationUi->text('Dropping database schema...'); + $notificationUi->newLine(); + + if ($isFullDatabaseDrop) { + $schemaTool->dropDatabase(); + } else { + $schemaTool->dropSchema($metadatas); + } + + $notificationUi->success('Database schema dropped successfully!'); + + return 0; + } + + $notificationUi->caution('This operation should not be executed in a production environment!'); + + if ($isFullDatabaseDrop) { + $sqls = $schemaTool->getDropDatabaseSQL(); + } else { + $sqls = $schemaTool->getDropSchemaSQL($metadatas); + } + + if (empty($sqls)) { + $notificationUi->success('Nothing to drop. The database is empty!'); + + return 0; + } + + $notificationUi->text( + [ + sprintf('The Schema-Tool would execute "%s" queries to update the database.', count($sqls)), + '', + 'Please run the operation by passing one - or both - of the following options:', + '', + sprintf(' %s --force to execute the command', $this->getName()), + sprintf(' %s --dump-sql to dump the SQL statements to the screen', $this->getName()), + ], + ); + + return 1; + } +} 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 @@ +setName($this->name) + ->setDescription('Executes (or dumps) the SQL needed to update the database schema to match the current mapping metadata') + ->addOption('em', null, InputOption::VALUE_REQUIRED, 'Name of the entity manager to operate on') + ->addOption('complete', null, InputOption::VALUE_NONE, 'This option is a no-op, is deprecated and will be removed in 4.0') + ->addOption('dump-sql', null, InputOption::VALUE_NONE, 'Dumps the generated SQL statements to the screen (does not execute them).') + ->addOption('force', 'f', InputOption::VALUE_NONE, 'Causes the generated SQL statements to be physically executed against your database.') + ->setHelp(<<<'EOT' +The %command.name% command generates the SQL needed to +synchronize the database schema with the current mapping metadata of the +default entity manager. + +For example, if you add metadata for a new column to an entity, this command +would generate and output the SQL needed to add the new column to the database: + +%command.name% --dump-sql + +Alternatively, you can execute the generated queries: + +%command.name% --force + +If both options are specified, the queries are output and then executed: + +%command.name% --dump-sql --force + +Finally, be aware that this task will drop all database assets (e.g. tables, +etc) that are *not* described by the current metadata. In other words, without +this option, this task leaves untouched any "extra" tables that exist in the +database, but which aren't described by any metadata. + +Hint: If you have a database with tables that should not be managed +by the ORM, you can use a DBAL functionality to filter the tables and sequences down +on a global level: + + $config->setSchemaAssetsFilter(function (string|AbstractAsset $assetName): bool { + if ($assetName instanceof AbstractAsset) { + $assetName = $assetName->getName(); + } + + return !str_starts_with($assetName, 'audit_'); + }); +EOT); + } + + /** + * {@inheritDoc} + */ + protected function executeSchemaCommand(InputInterface $input, OutputInterface $output, SchemaTool $schemaTool, array $metadatas, SymfonyStyle $ui): int + { + $notificationUi = $ui->getErrorStyle(); + + if ($input->getOption('complete') === true) { + Deprecation::trigger( + 'doctrine/orm', + 'https://github.com/doctrine/orm/pull/11354', + 'The --complete option is a no-op, is deprecated and will be removed in Doctrine ORM 4.0.', + ); + $notificationUi->warning('The --complete option is a no-op, is deprecated and will be removed in Doctrine ORM 4.0.'); + } + + $sqls = $schemaTool->getUpdateSchemaSql($metadatas); + + if (empty($sqls)) { + $notificationUi->success('Nothing to update - your database is already in sync with the current entity metadata.'); + + return 0; + } + + $dumpSql = $input->getOption('dump-sql') === true; + $force = $input->getOption('force') === true; + + if ($dumpSql) { + foreach ($sqls as $sql) { + $ui->writeln(sprintf('%s;', $sql)); + } + } + + if ($force) { + if ($dumpSql) { + $notificationUi->newLine(); + } + + $notificationUi->text('Updating database schema...'); + $notificationUi->newLine(); + + $schemaTool->updateSchema($metadatas); + + $pluralization = count($sqls) === 1 ? 'query was' : 'queries were'; + + $notificationUi->text(sprintf(' %s %s executed', count($sqls), $pluralization)); + $notificationUi->success('Database schema updated successfully!'); + } + + if ($dumpSql || $force) { + return 0; + } + + $notificationUi->caution( + [ + 'This operation should not be executed in a production environment!', + '', + 'Use the incremental update to detect changes during development and use', + 'the SQL DDL provided to manually update your database in production.', + ], + ); + + $notificationUi->text( + [ + sprintf('The Schema-Tool would execute "%s" queries to update the database.', count($sqls)), + '', + 'Please run the operation by passing one - or both - of the following options:', + '', + sprintf(' %s --force to execute the command', $this->getName()), + sprintf(' %s --dump-sql to dump the SQL statements to the screen', $this->getName()), + ], + ); + + return 1; + } +} 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 @@ +setName('orm:validate-schema') + ->setDescription('Validate the mapping files') + ->addOption('em', null, InputOption::VALUE_REQUIRED, 'Name of the entity manager to operate on') + ->addOption('skip-mapping', null, InputOption::VALUE_NONE, 'Skip the mapping validation check') + ->addOption('skip-sync', null, InputOption::VALUE_NONE, 'Skip checking if the mapping is in sync with the database') + ->addOption('skip-property-types', null, InputOption::VALUE_NONE, 'Skip checking if property types match the Doctrine types') + ->setHelp('Validate that the mapping files are correct and in sync with the database.'); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $ui = (new SymfonyStyle($input, $output))->getErrorStyle(); + + $em = $this->getEntityManager($input); + $validator = new SchemaValidator($em, ! $input->getOption('skip-property-types')); + $exit = 0; + + $ui->section('Mapping'); + + if ($input->getOption('skip-mapping')) { + $ui->text('[SKIPPED] The mapping was not checked.'); + } else { + $errors = $validator->validateMapping(); + if ($errors) { + foreach ($errors as $className => $errorMessages) { + $ui->text( + sprintf( + '[FAIL] The entity-class %s mapping is invalid:', + $className, + ), + ); + + $ui->listing($errorMessages); + $ui->newLine(); + } + + ++$exit; + } else { + $ui->success('The mapping files are correct.'); + } + } + + $ui->section('Database'); + + if ($input->getOption('skip-sync')) { + $ui->text('[SKIPPED] The database was not checked for synchronicity.'); + } elseif (! $validator->schemaInSyncWithMetadata()) { + $ui->error('The database schema is not in sync with the current mapping file.'); + + if ($output->getVerbosity() >= OutputInterface::VERBOSITY_VERBOSE) { + $sqls = $validator->getUpdateSchemaList(); + $ui->comment(sprintf('%d schema diff(s) detected:', count($sqls))); + foreach ($sqls as $sql) { + $ui->text(sprintf(' %s;', $sql)); + } + } + + $exit += 2; + } else { + $ui->success('The database schema is in sync with the mapping files.'); + } + + return $exit; + } +} 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 @@ +run(); + } + + /** + * Creates a console application with the given helperset and + * optional commands. + * + * @param SymfonyCommand[] $commands + * + * @throws OutOfBoundsException + */ + public static function createApplication( + EntityManagerProvider $entityManagerProvider, + array $commands = [], + ): Application { + $version = InstalledVersions::getVersion('doctrine/orm'); + assert($version !== null); + + $cli = new Application('Doctrine Command Line Interface', $version); + $cli->setCatchExceptions(true); + + self::addCommands($cli, $entityManagerProvider); + $cli->addCommands($commands); + + return $cli; + } + + public static function addCommands(Application $cli, EntityManagerProvider $entityManagerProvider): void + { + $connectionProvider = new ConnectionFromManagerProvider($entityManagerProvider); + + if (class_exists(DBALConsole\Command\ReservedWordsCommand::class)) { + $cli->add(new DBALConsole\Command\ReservedWordsCommand($connectionProvider)); + } + + $cli->addCommands( + [ + // DBAL Commands + new DBALConsole\Command\RunSqlCommand($connectionProvider), + + // ORM Commands + new Command\ClearCache\CollectionRegionCommand($entityManagerProvider), + new Command\ClearCache\EntityRegionCommand($entityManagerProvider), + new Command\ClearCache\MetadataCommand($entityManagerProvider), + new Command\ClearCache\QueryCommand($entityManagerProvider), + new Command\ClearCache\QueryRegionCommand($entityManagerProvider), + new Command\ClearCache\ResultCommand($entityManagerProvider), + new Command\SchemaTool\CreateCommand($entityManagerProvider), + new Command\SchemaTool\UpdateCommand($entityManagerProvider), + new Command\SchemaTool\DropCommand($entityManagerProvider), + new Command\GenerateProxiesCommand($entityManagerProvider), + new Command\RunDqlCommand($entityManagerProvider), + new Command\ValidateSchemaCommand($entityManagerProvider), + new Command\InfoCommand($entityManagerProvider), + new Command\MappingDescribeCommand($entityManagerProvider), + ], + ); + } +} 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 @@ +entityManagerProvider->getDefaultManager()->getConnection(); + } + + public function getConnection(string $name): Connection + { + return $this->entityManagerProvider->getManager($name)->getConnection(); + } +} 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 @@ +entityManager; + } + + public function getManager(string $name): EntityManagerInterface + { + if ($name !== $this->defaultManagerName) { + throw UnknownManagerException::unknownManager($name, [$this->defaultManagerName]); + } + + return $this->entityManager; + } +} 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 @@ + $knownManagers */ + public static function unknownManager(string $unknownManager, array $knownManagers = []): self + { + return new self(sprintf( + 'Requested unknown entity manager: %s, known managers: %s', + $unknownManager, + implode(', ', $knownManagers), + )); + } +} 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 @@ +filter = (array) $filter; + + parent::__construct($metadata); + } + + public function accept(): bool + { + if (count($this->filter) === 0) { + return true; + } + + $it = $this->getInnerIterator(); + $metadata = $it->current(); + + foreach ($this->filter as $filter) { + $pregResult = preg_match('/' . $filter . '/', $metadata->getName()); + + if ($pregResult === false) { + throw new RuntimeException( + sprintf("Error while evaluating regex '/%s/'.", $filter), + ); + } + + if ($pregResult) { + return true; + } + } + + return false; + } + + /** @return ArrayIterator */ + public function getInnerIterator(): ArrayIterator + { + $innerIterator = parent::getInnerIterator(); + + assert($innerIterator instanceof ArrayIterator); + + return $innerIterator; + } + + public function count(): int + { + return count($this->getInnerIterator()); + } +} 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 @@ +toArray(); + } + + if (! $maxDepth) { + return is_object($var) ? $var::class + : (is_array($var) ? 'Array(' . count($var) . ')' : $var); + } + + if (is_array($var)) { + $return = []; + + foreach ($var as $k => $v) { + $return[$k] = self::export($v, $maxDepth - 1); + } + + return $return; + } + + if (! is_object($var)) { + return $var; + } + + $return = new stdClass(); + if ($var instanceof DateTimeInterface) { + $return->__CLASS__ = $var::class; + $return->date = $var->format('c'); + $return->timezone = $var->getTimezone()->getName(); + + return $return; + } + + $return->__CLASS__ = DefaultProxyClassNameResolver::getClass($var); + + if ($var instanceof Proxy) { + $return->__IS_PROXY__ = true; + $return->__PROXY_INITIALIZED__ = $var->__isInitialized(); + } + + if ($var instanceof ArrayObject || $var instanceof ArrayIterator) { + $return->__STORAGE__ = self::export($var->getArrayCopy(), $maxDepth - 1); + } + + return self::fillReturnWithClassAttributes($var, $return, $maxDepth); + } + + /** + * Fill the $return variable with class attributes + * Based on obj2array function from {@see https://secure.php.net/manual/en/function.get-object-vars.php#47075} + */ + private static function fillReturnWithClassAttributes(object $var, stdClass $return, int $maxDepth): stdClass + { + $clone = (array) $var; + + foreach (array_keys($clone) as $key) { + $aux = explode("\0", (string) $key); + $name = end($aux); + if ($aux[0] === '') { + $name .= ':' . ($aux[1] === '*' ? 'protected' : $aux[1] . ':private'); + } + + $return->$name = self::export($clone[$key], $maxDepth - 1); + } + + return $return; + } +} 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 @@ +dumpIdentityMap($args->getObjectManager()); + } + + /** + * Dumps the contents of the identity map into a stream. + */ + public function dumpIdentityMap(EntityManagerInterface $em): void + { + $uow = $em->getUnitOfWork(); + $identityMap = $uow->getIdentityMap(); + + $fh = fopen($this->file, 'xb+'); + if (count($identityMap) === 0) { + fwrite($fh, 'Flush Operation [' . $this->context . "] - Empty identity map.\n"); + + return; + } + + fwrite($fh, 'Flush Operation [' . $this->context . "] - Dumping identity map:\n"); + foreach ($identityMap as $className => $map) { + fwrite($fh, 'Class: ' . $className . "\n"); + + foreach ($map as $entity) { + fwrite($fh, ' Entity: ' . $this->getIdString($entity, $uow) . ' ' . spl_object_id($entity) . "\n"); + fwrite($fh, " Associations:\n"); + + $cm = $em->getClassMetadata($className); + + foreach ($cm->associationMappings as $field => $assoc) { + fwrite($fh, ' ' . $field . ' '); + $value = $cm->getFieldValue($entity, $field); + + if ($assoc->isToOne()) { + if ($value === null) { + fwrite($fh, " NULL\n"); + } else { + if ($uow->isUninitializedObject($value)) { + fwrite($fh, '[PROXY] '); + } + + fwrite($fh, $this->getIdString($value, $uow) . ' ' . spl_object_id($value) . "\n"); + } + } else { + $initialized = ! ($value instanceof PersistentCollection) || $value->isInitialized(); + if ($value === null) { + fwrite($fh, " NULL\n"); + } elseif ($initialized) { + fwrite($fh, '[INITIALIZED] ' . $this->getType($value) . ' ' . count($value) . " elements\n"); + + foreach ($value as $obj) { + fwrite($fh, ' ' . $this->getIdString($obj, $uow) . ' ' . spl_object_id($obj) . "\n"); + } + } else { + fwrite($fh, '[PROXY] ' . $this->getType($value) . " unknown element size\n"); + foreach ($value->unwrap() as $obj) { + fwrite($fh, ' ' . $this->getIdString($obj, $uow) . ' ' . spl_object_id($obj) . "\n"); + } + } + } + } + } + } + + fclose($fh); + } + + private function getType(mixed $var): string + { + if (is_object($var)) { + $refl = new ReflectionObject($var); + + return $refl->getShortName(); + } + + return gettype($var); + } + + private function getIdString(object $entity, UnitOfWork $uow): string + { + if ($uow->isInIdentityMap($entity)) { + $ids = $uow->getEntityIdentifier($entity); + $idstring = ''; + + foreach ($ids as $k => $v) { + $idstring .= $k . '=' . $v; + } + } else { + $idstring = 'NEWOBJECT '; + } + + $state = $uow->getEntityState($entity); + + if ($state === UnitOfWork::STATE_NEW) { + $idstring .= ' [NEW]'; + } elseif ($state === UnitOfWork::STATE_REMOVED) { + $idstring .= ' [REMOVED]'; + } elseif ($state === UnitOfWork::STATE_MANAGED) { + $idstring .= ' [MANAGED]'; + } elseif ($state === UnitOfWork::STATE_DETACHED) { + $idstring .= ' [DETACHED]'; + } + + return $idstring; + } +} 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 @@ +em; + } + + public function getSchema(): Schema + { + return $this->schema; + } +} 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 @@ +classMetadata; + } + + public function getSchema(): Schema + { + return $this->schema; + } + + public function getClassTable(): Table + { + return $this->classTable; + } +} 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 @@ + FROM ()) + * + * Works with composite keys but cannot deal with queries that have multiple + * root entities (e.g. `SELECT f, b from Foo, Bar`) + * + * Note that the ORDER BY clause is not removed. Many SQL implementations (e.g. MySQL) + * are able to cache subqueries. By keeping the ORDER BY clause intact, the limitSubQuery + * that will most likely be executed next can be read from the native SQL cache. + * + * @psalm-import-type QueryComponent from Parser + */ +class CountOutputWalker extends SqlWalker +{ + private readonly AbstractPlatform $platform; + private readonly ResultSetMapping $rsm; + + /** + * {@inheritDoc} + */ + public function __construct(Query $query, ParserResult $parserResult, array $queryComponents) + { + $this->platform = $query->getEntityManager()->getConnection()->getDatabasePlatform(); + $this->rsm = $parserResult->getResultSetMapping(); + + parent::__construct($query, $parserResult, $queryComponents); + } + + public function walkSelectStatement(SelectStatement $selectStatement): string + { + if ($this->platform instanceof SQLServerPlatform) { + $selectStatement->orderByClause = null; + } + + $sql = parent::walkSelectStatement($selectStatement); + + if ($selectStatement->groupByClause) { + return sprintf( + 'SELECT COUNT(*) AS dctrn_count FROM (%s) dctrn_table', + $sql, + ); + } + + // Find out the SQL alias of the identifier column of the root entity + // It may be possible to make this work with multiple root entities but that + // would probably require issuing multiple queries or doing a UNION SELECT + // so for now, It's not supported. + + // Get the root entity and alias from the AST fromClause + $from = $selectStatement->fromClause->identificationVariableDeclarations; + if (count($from) > 1) { + throw new RuntimeException('Cannot count query which selects two FROM components, cannot make distinction'); + } + + $fromRoot = reset($from); + $rootAlias = $fromRoot->rangeVariableDeclaration->aliasIdentificationVariable; + $rootClass = $this->getMetadataForDqlAlias($rootAlias); + $rootIdentifier = $rootClass->identifier; + + // For every identifier, find out the SQL alias by combing through the ResultSetMapping + $sqlIdentifier = []; + foreach ($rootIdentifier as $property) { + if (isset($rootClass->fieldMappings[$property])) { + foreach (array_keys($this->rsm->fieldMappings, $property, true) as $alias) { + if ($this->rsm->columnOwnerMap[$alias] === $rootAlias) { + $sqlIdentifier[$property] = $alias; + } + } + } + + if (isset($rootClass->associationMappings[$property])) { + $association = $rootClass->associationMappings[$property]; + assert($association->isToOneOwningSide()); + $joinColumn = $association->joinColumns[0]->name; + + foreach (array_keys($this->rsm->metaMappings, $joinColumn, true) as $alias) { + if ($this->rsm->columnOwnerMap[$alias] === $rootAlias) { + $sqlIdentifier[$property] = $alias; + } + } + } + } + + if (count($rootIdentifier) !== count($sqlIdentifier)) { + throw new RuntimeException(sprintf( + 'Not all identifier properties can be found in the ResultSetMapping: %s', + implode(', ', array_diff($rootIdentifier, array_keys($sqlIdentifier))), + )); + } + + // Build the counter query + return sprintf( + 'SELECT COUNT(*) AS dctrn_count FROM (SELECT DISTINCT %s FROM (%s) dctrn_result) dctrn_table', + implode(', ', $sqlIdentifier), + $sql, + ); + } +} 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 @@ +havingClause) { + throw new RuntimeException('Cannot count query that uses a HAVING clause. Use the output walkers for pagination'); + } + + // Get the root entity and alias from the AST fromClause + $from = $selectStatement->fromClause->identificationVariableDeclarations; + + if (count($from) > 1) { + throw new RuntimeException('Cannot count query which selects two FROM components, cannot make distinction'); + } + + $fromRoot = reset($from); + $rootAlias = $fromRoot->rangeVariableDeclaration->aliasIdentificationVariable; + $rootClass = $this->getMetadataForDqlAlias($rootAlias); + $identifierFieldName = $rootClass->getSingleIdentifierFieldName(); + + $pathType = PathExpression::TYPE_STATE_FIELD; + if (isset($rootClass->associationMappings[$identifierFieldName])) { + $pathType = PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION; + } + + $pathExpression = new PathExpression( + PathExpression::TYPE_STATE_FIELD | PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION, + $rootAlias, + $identifierFieldName, + ); + $pathExpression->type = $pathType; + + $distinct = $this->_getQuery()->getHint(self::HINT_DISTINCT); + $selectStatement->selectClause->selectExpressions = [ + new SelectExpression( + new AggregateExpression('count', $pathExpression, $distinct), + null, + ), + ]; + + // ORDER BY is not needed, only increases query execution through unnecessary sorting. + $selectStatement->orderByClause = null; + } +} 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 @@ + FROM () LIMIT x OFFSET y + * + * Works with composite keys but cannot deal with queries that have multiple + * root entities (e.g. `SELECT f, b from Foo, Bar`) + * + * @psalm-import-type QueryComponent from Parser + */ +class LimitSubqueryOutputWalker extends SqlWalker +{ + private const ORDER_BY_PATH_EXPRESSION = '/(? */ + private array $orderByPathExpressions = []; + + /** + * We don't want to add path expressions from sub-selects into the select clause of the containing query. + * This state flag simply keeps track on whether we are walking on a subquery or not + */ + private bool $inSubSelect = false; + + /** + * Stores various parameters that are otherwise unavailable + * because Doctrine\ORM\Query\SqlWalker keeps everything private without + * accessors. + * + * {@inheritDoc} + */ + public function __construct( + Query $query, + ParserResult $parserResult, + array $queryComponents, + ) { + $this->platform = $query->getEntityManager()->getConnection()->getDatabasePlatform(); + $this->rsm = $parserResult->getResultSetMapping(); + + // Reset limit and offset + $this->firstResult = $query->getFirstResult(); + $this->maxResults = $query->getMaxResults(); + $query->setFirstResult(0)->setMaxResults(null); + + $this->em = $query->getEntityManager(); + $this->quoteStrategy = $this->em->getConfiguration()->getQuoteStrategy(); + + parent::__construct($query, $parserResult, $queryComponents); + } + + /** + * Check if the platform supports the ROW_NUMBER window function. + */ + private function platformSupportsRowNumber(): bool + { + return $this->platform instanceof PostgreSQLPlatform + || $this->platform instanceof SQLServerPlatform + || $this->platform instanceof OraclePlatform + || $this->platform instanceof DB2Platform + || (method_exists($this->platform, 'supportsRowNumberFunction') + && $this->platform->supportsRowNumberFunction()); + } + + /** + * Rebuilds a select statement's order by clause for use in a + * ROW_NUMBER() OVER() expression. + */ + private function rebuildOrderByForRowNumber(SelectStatement $AST): void + { + $orderByClause = $AST->orderByClause; + $selectAliasToExpressionMap = []; + // Get any aliases that are available for select expressions. + foreach ($AST->selectClause->selectExpressions as $selectExpression) { + $selectAliasToExpressionMap[$selectExpression->fieldIdentificationVariable] = $selectExpression->expression; + } + + // Rebuild string orderby expressions to use the select expression they're referencing + foreach ($orderByClause->orderByItems as $orderByItem) { + if (is_string($orderByItem->expression) && isset($selectAliasToExpressionMap[$orderByItem->expression])) { + $orderByItem->expression = $selectAliasToExpressionMap[$orderByItem->expression]; + } + } + + $func = new RowNumberOverFunction('dctrn_rownum'); + $func->orderByClause = $AST->orderByClause; + $AST->selectClause->selectExpressions[] = new SelectExpression($func, 'dctrn_rownum', true); + + // No need for an order by clause, we'll order by rownum in the outer query. + $AST->orderByClause = null; + } + + public function walkSelectStatement(SelectStatement $selectStatement): string + { + if ($this->platformSupportsRowNumber()) { + return $this->walkSelectStatementWithRowNumber($selectStatement); + } + + return $this->walkSelectStatementWithoutRowNumber($selectStatement); + } + + /** + * Walks down a SelectStatement AST node, wrapping it in a SELECT DISTINCT. + * This method is for use with platforms which support ROW_NUMBER. + * + * @throws RuntimeException + */ + public function walkSelectStatementWithRowNumber(SelectStatement $AST): string + { + $hasOrderBy = false; + $outerOrderBy = ' ORDER BY dctrn_minrownum ASC'; + $orderGroupBy = ''; + if ($AST->orderByClause instanceof OrderByClause) { + $hasOrderBy = true; + $this->rebuildOrderByForRowNumber($AST); + } + + $innerSql = $this->getInnerSQL($AST); + + $sqlIdentifier = $this->getSQLIdentifier($AST); + + if ($hasOrderBy) { + $orderGroupBy = ' GROUP BY ' . implode(', ', $sqlIdentifier); + $sqlIdentifier[] = 'MIN(' . $this->walkResultVariable('dctrn_rownum') . ') AS dctrn_minrownum'; + } + + // Build the counter query + $sql = sprintf( + 'SELECT DISTINCT %s FROM (%s) dctrn_result', + implode(', ', $sqlIdentifier), + $innerSql, + ); + + if ($hasOrderBy) { + $sql .= $orderGroupBy . $outerOrderBy; + } + + // Apply the limit and offset. + $sql = $this->platform->modifyLimitQuery( + $sql, + $this->maxResults, + $this->firstResult, + ); + + // Add the columns to the ResultSetMapping. It's not really nice but + // it works. Preferably I'd clear the RSM or simply create a new one + // but that is not possible from inside the output walker, so we dirty + // up the one we have. + foreach ($sqlIdentifier as $property => $alias) { + $this->rsm->addScalarResult($alias, $property); + } + + return $sql; + } + + /** + * Walks down a SelectStatement AST node, wrapping it in a SELECT DISTINCT. + * This method is for platforms which DO NOT support ROW_NUMBER. + * + * @throws RuntimeException + */ + public function walkSelectStatementWithoutRowNumber(SelectStatement $AST, bool $addMissingItemsFromOrderByToSelect = true): string + { + // We don't want to call this recursively! + if ($AST->orderByClause instanceof OrderByClause && $addMissingItemsFromOrderByToSelect) { + // In the case of ordering a query by columns from joined tables, we + // must add those columns to the select clause of the query BEFORE + // the SQL is generated. + $this->addMissingItemsFromOrderByToSelect($AST); + } + + // Remove order by clause from the inner query + // It will be re-appended in the outer select generated by this method + $orderByClause = $AST->orderByClause; + $AST->orderByClause = null; + + $innerSql = $this->getInnerSQL($AST); + + $sqlIdentifier = $this->getSQLIdentifier($AST); + + // Build the counter query + $sql = sprintf( + 'SELECT DISTINCT %s FROM (%s) dctrn_result', + implode(', ', $sqlIdentifier), + $innerSql, + ); + + // https://github.com/doctrine/orm/issues/2630 + $sql = $this->preserveSqlOrdering($sqlIdentifier, $innerSql, $sql, $orderByClause); + + // Apply the limit and offset. + $sql = $this->platform->modifyLimitQuery( + $sql, + $this->maxResults, + $this->firstResult, + ); + + // Add the columns to the ResultSetMapping. It's not really nice but + // it works. Preferably I'd clear the RSM or simply create a new one + // but that is not possible from inside the output walker, so we dirty + // up the one we have. + foreach ($sqlIdentifier as $property => $alias) { + $this->rsm->addScalarResult($alias, $property); + } + + // Restore orderByClause + $AST->orderByClause = $orderByClause; + + return $sql; + } + + /** + * Finds all PathExpressions in an AST's OrderByClause, and ensures that + * the referenced fields are present in the SelectClause of the passed AST. + */ + private function addMissingItemsFromOrderByToSelect(SelectStatement $AST): void + { + $this->orderByPathExpressions = []; + + // We need to do this in another walker because otherwise we'll end up + // polluting the state of this one. + $walker = clone $this; + + // This will populate $orderByPathExpressions via + // LimitSubqueryOutputWalker::walkPathExpression, which will be called + // as the select statement is walked. We'll end up with an array of all + // path expressions referenced in the query. + $walker->walkSelectStatementWithoutRowNumber($AST, false); + $orderByPathExpressions = $walker->getOrderByPathExpressions(); + + // Get a map of referenced identifiers to field names. + $selects = []; + foreach ($orderByPathExpressions as $pathExpression) { + assert($pathExpression->field !== null); + $idVar = $pathExpression->identificationVariable; + $field = $pathExpression->field; + if (! isset($selects[$idVar])) { + $selects[$idVar] = []; + } + + $selects[$idVar][$field] = true; + } + + // Loop the select clause of the AST and exclude items from $select + // that are already being selected in the query. + foreach ($AST->selectClause->selectExpressions as $selectExpression) { + if ($selectExpression instanceof SelectExpression) { + $idVar = $selectExpression->expression; + if (! is_string($idVar)) { + continue; + } + + $field = $selectExpression->fieldIdentificationVariable; + if ($field === null) { + // No need to add this select, as we're already fetching the whole object. + unset($selects[$idVar]); + } else { + unset($selects[$idVar][$field]); + } + } + } + + // Add select items which were not excluded to the AST's select clause. + foreach ($selects as $idVar => $fields) { + $AST->selectClause->selectExpressions[] = new SelectExpression($idVar, null, true); + } + } + + /** + * Generates new SQL for statements with an order by clause + * + * @param mixed[] $sqlIdentifier + */ + private function preserveSqlOrdering( + array $sqlIdentifier, + string $innerSql, + string $sql, + OrderByClause|null $orderByClause, + ): string { + // If the sql statement has an order by clause, we need to wrap it in a new select distinct statement + if (! $orderByClause) { + return $sql; + } + + // now only select distinct identifier + return sprintf( + 'SELECT DISTINCT %s FROM (%s) dctrn_result', + implode(', ', $sqlIdentifier), + $this->recreateInnerSql($orderByClause, $sqlIdentifier, $innerSql), + ); + } + + /** + * Generates a new SQL statement for the inner query to keep the correct sorting + * + * @param mixed[] $identifiers + */ + private function recreateInnerSql( + OrderByClause $orderByClause, + array $identifiers, + string $innerSql, + ): string { + [$searchPatterns, $replacements] = $this->generateSqlAliasReplacements(); + $orderByItems = []; + + foreach ($orderByClause->orderByItems as $orderByItem) { + // Walk order by item to get string representation of it and + // replace path expressions in the order by clause with their column alias + $orderByItemString = preg_replace( + $searchPatterns, + $replacements, + $this->walkOrderByItem($orderByItem), + ); + + $orderByItems[] = $orderByItemString; + $identifier = substr($orderByItemString, 0, strrpos($orderByItemString, ' ')); + + if (! in_array($identifier, $identifiers, true)) { + $identifiers[] = $identifier; + } + } + + return $sql = sprintf( + 'SELECT DISTINCT %s FROM (%s) dctrn_result_inner ORDER BY %s', + implode(', ', $identifiers), + $innerSql, + implode(', ', $orderByItems), + ); + } + + /** + * @return string[][] + * @psalm-return array{0: list, 1: list} + */ + private function generateSqlAliasReplacements(): array + { + $aliasMap = $searchPatterns = $replacements = $metadataList = []; + + // Generate DQL alias -> SQL table alias mapping + foreach (array_keys($this->rsm->aliasMap) as $dqlAlias) { + $metadataList[$dqlAlias] = $class = $this->getMetadataForDqlAlias($dqlAlias); + $aliasMap[$dqlAlias] = $this->getSQLTableAlias($class->getTableName(), $dqlAlias); + } + + // Generate search patterns for each field's path expression in the order by clause + foreach ($this->rsm->fieldMappings as $fieldAlias => $fieldName) { + $dqlAliasForFieldAlias = $this->rsm->columnOwnerMap[$fieldAlias]; + $class = $metadataList[$dqlAliasForFieldAlias]; + + // If the field is from a joined child table, we won't be ordering on it. + if (! isset($class->fieldMappings[$fieldName])) { + continue; + } + + $fieldMapping = $class->fieldMappings[$fieldName]; + + // Get the proper column name as will appear in the select list + $columnName = $this->quoteStrategy->getColumnName( + $fieldName, + $metadataList[$dqlAliasForFieldAlias], + $this->em->getConnection()->getDatabasePlatform(), + ); + + // Get the SQL table alias for the entity and field + $sqlTableAliasForFieldAlias = $aliasMap[$dqlAliasForFieldAlias]; + + if (isset($fieldMapping->declared) && $fieldMapping->declared !== $class->name) { + // Field was declared in a parent class, so we need to get the proper SQL table alias + // for the joined parent table. + $otherClassMetadata = $this->em->getClassMetadata($fieldMapping->declared); + + if (! $otherClassMetadata->isMappedSuperclass) { + $sqlTableAliasForFieldAlias = $this->getSQLTableAlias($otherClassMetadata->getTableName(), $dqlAliasForFieldAlias); + } + } + + // Compose search and replace patterns + $searchPatterns[] = sprintf(self::ORDER_BY_PATH_EXPRESSION, $sqlTableAliasForFieldAlias, $columnName); + $replacements[] = $fieldAlias; + } + + return [$searchPatterns, $replacements]; + } + + /** + * getter for $orderByPathExpressions + * + * @return list + */ + public function getOrderByPathExpressions(): array + { + return $this->orderByPathExpressions; + } + + /** + * @throws OptimisticLockException + * @throws QueryException + */ + private function getInnerSQL(SelectStatement $AST): string + { + // Set every select expression as visible(hidden = false) to + // make $AST have scalar mappings properly - this is relevant for referencing selected + // fields from outside the subquery, for example in the ORDER BY segment + $hiddens = []; + + foreach ($AST->selectClause->selectExpressions as $idx => $expr) { + $hiddens[$idx] = $expr->hiddenAliasResultVariable; + $expr->hiddenAliasResultVariable = false; + } + + $innerSql = parent::walkSelectStatement($AST); + + // Restore hiddens + foreach ($AST->selectClause->selectExpressions as $idx => $expr) { + $expr->hiddenAliasResultVariable = $hiddens[$idx]; + } + + return $innerSql; + } + + /** @return string[] */ + private function getSQLIdentifier(SelectStatement $AST): array + { + // Find out the SQL alias of the identifier column of the root entity. + // It may be possible to make this work with multiple root entities but that + // would probably require issuing multiple queries or doing a UNION SELECT. + // So for now, it's not supported. + + // Get the root entity and alias from the AST fromClause. + $from = $AST->fromClause->identificationVariableDeclarations; + if (count($from) !== 1) { + throw new RuntimeException('Cannot count query which selects two FROM components, cannot make distinction'); + } + + $fromRoot = reset($from); + $rootAlias = $fromRoot->rangeVariableDeclaration->aliasIdentificationVariable; + $rootClass = $this->getMetadataForDqlAlias($rootAlias); + $rootIdentifier = $rootClass->identifier; + + // For every identifier, find out the SQL alias by combing through the ResultSetMapping + $sqlIdentifier = []; + foreach ($rootIdentifier as $property) { + if (isset($rootClass->fieldMappings[$property])) { + foreach (array_keys($this->rsm->fieldMappings, $property, true) as $alias) { + if ($this->rsm->columnOwnerMap[$alias] === $rootAlias) { + $sqlIdentifier[$property] = $alias; + } + } + } + + if (isset($rootClass->associationMappings[$property])) { + $association = $rootClass->associationMappings[$property]; + assert($association->isToOneOwningSide()); + $joinColumn = $association->joinColumns[0]->name; + + foreach (array_keys($this->rsm->metaMappings, $joinColumn, true) as $alias) { + if ($this->rsm->columnOwnerMap[$alias] === $rootAlias) { + $sqlIdentifier[$property] = $alias; + } + } + } + } + + if (count($sqlIdentifier) === 0) { + throw new RuntimeException('The Paginator does not support Queries which only yield ScalarResults.'); + } + + if (count($rootIdentifier) !== count($sqlIdentifier)) { + throw new RuntimeException(sprintf( + 'Not all identifier properties can be found in the ResultSetMapping: %s', + implode(', ', array_diff($rootIdentifier, array_keys($sqlIdentifier))), + )); + } + + return $sqlIdentifier; + } + + public function walkPathExpression(PathExpression $pathExpr): string + { + if (! $this->inSubSelect && ! $this->platformSupportsRowNumber() && ! in_array($pathExpr, $this->orderByPathExpressions, true)) { + $this->orderByPathExpressions[] = $pathExpr; + } + + return parent::walkPathExpression($pathExpr); + } + + public function walkSubSelect(Subselect $subselect): string + { + $this->inSubSelect = true; + + $sql = parent::walkSubselect($subselect); + + $this->inSubSelect = false; + + return $sql; + } +} 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 @@ +fromClause->identificationVariableDeclarations; + $fromRoot = reset($from); + $rootAlias = $fromRoot->rangeVariableDeclaration->aliasIdentificationVariable; + $rootClass = $this->getMetadataForDqlAlias($rootAlias); + + $this->validate($selectStatement); + $identifier = $rootClass->getSingleIdentifierFieldName(); + + if (isset($rootClass->associationMappings[$identifier])) { + 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.'); + } + + $query = $this->_getQuery(); + + $query->setHint( + self::IDENTIFIER_TYPE, + Type::getType($rootClass->fieldMappings[$identifier]->type), + ); + + $query->setHint(self::FORCE_DBAL_TYPE_CONVERSION, true); + + $pathExpression = new PathExpression( + PathExpression::TYPE_STATE_FIELD | PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION, + $rootAlias, + $identifier, + ); + + $pathExpression->type = PathExpression::TYPE_STATE_FIELD; + + $selectStatement->selectClause->selectExpressions = [new SelectExpression($pathExpression, '_dctrn_id')]; + $selectStatement->selectClause->isDistinct = ($query->getHints()[Paginator::HINT_ENABLE_DISTINCT] ?? true) === true; + + if (! isset($selectStatement->orderByClause)) { + return; + } + + $queryComponents = $this->getQueryComponents(); + foreach ($selectStatement->orderByClause->orderByItems as $item) { + if ($item->expression instanceof PathExpression) { + $selectStatement->selectClause->selectExpressions[] = new SelectExpression( + $this->createSelectExpressionItem($item->expression), + '_dctrn_ord' . $this->aliasCounter++, + ); + + continue; + } + + if (is_string($item->expression) && isset($queryComponents[$item->expression])) { + $qComp = $queryComponents[$item->expression]; + + if (isset($qComp['resultVariable'])) { + $selectStatement->selectClause->selectExpressions[] = new SelectExpression( + $qComp['resultVariable'], + $item->expression, + ); + } + } + } + } + + /** + * Validate the AST to ensure that this walker is able to properly manipulate it. + */ + private function validate(SelectStatement $AST): void + { + // Prevent LimitSubqueryWalker from being used with queries that include + // a limit, a fetched to-many join, and an order by condition that + // references a column from the fetch joined table. + $queryComponents = $this->getQueryComponents(); + $query = $this->_getQuery(); + $from = $AST->fromClause->identificationVariableDeclarations; + $fromRoot = reset($from); + + if ( + $query instanceof Query + && $query->getMaxResults() !== null + && $AST->orderByClause + && count($fromRoot->joins) + ) { + // Check each orderby item. + // TODO: check complex orderby items too... + foreach ($AST->orderByClause->orderByItems as $orderByItem) { + $expression = $orderByItem->expression; + if ( + $orderByItem->expression instanceof PathExpression + && isset($queryComponents[$expression->identificationVariable]) + ) { + $queryComponent = $queryComponents[$expression->identificationVariable]; + if ( + isset($queryComponent['parent']) + && isset($queryComponent['relation']) + && $queryComponent['relation']->isToMany() + ) { + 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.'); + } + } + } + } + } + + /** + * Retrieve either an IdentityFunction (IDENTITY(u.assoc)) or a state field (u.name). + * + * @return IdentityFunction|PathExpression + */ + private function createSelectExpressionItem(PathExpression $pathExpression): Node + { + if ($pathExpression->type === PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION) { + $identity = new IdentityFunction('identity'); + + $identity->pathExpression = clone $pathExpression; + + return $identity; + } + + return clone $pathExpression; + } +} 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 @@ + + */ +class Paginator implements Countable, IteratorAggregate +{ + use SQLResultCasing; + + public const HINT_ENABLE_DISTINCT = 'paginator.distinct.enable'; + + private readonly Query $query; + private bool|null $useOutputWalkers = null; + private int|null $count = null; + + /** @param bool $fetchJoinCollection Whether the query joins a collection (true by default). */ + public function __construct( + Query|QueryBuilder $query, + private readonly bool $fetchJoinCollection = true, + ) { + if ($query instanceof QueryBuilder) { + $query = $query->getQuery(); + } + + $this->query = $query; + } + + /** + * Returns the query. + */ + public function getQuery(): Query + { + return $this->query; + } + + /** + * Returns whether the query joins a collection. + * + * @return bool Whether the query joins a collection. + */ + public function getFetchJoinCollection(): bool + { + return $this->fetchJoinCollection; + } + + /** + * Returns whether the paginator will use an output walker. + */ + public function getUseOutputWalkers(): bool|null + { + return $this->useOutputWalkers; + } + + /** + * Sets whether the paginator will use an output walker. + * + * @return $this + */ + public function setUseOutputWalkers(bool|null $useOutputWalkers): static + { + $this->useOutputWalkers = $useOutputWalkers; + + return $this; + } + + public function count(): int + { + if ($this->count === null) { + try { + $this->count = (int) array_sum(array_map('current', $this->getCountQuery()->getScalarResult())); + } catch (NoResultException) { + $this->count = 0; + } + } + + return $this->count; + } + + /** + * {@inheritDoc} + * + * @psalm-return Traversable + */ + public function getIterator(): Traversable + { + $offset = $this->query->getFirstResult(); + $length = $this->query->getMaxResults(); + + if ($this->fetchJoinCollection && $length !== null) { + $subQuery = $this->cloneQuery($this->query); + + if ($this->useOutputWalker($subQuery)) { + $subQuery->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, LimitSubqueryOutputWalker::class); + } else { + $this->appendTreeWalker($subQuery, LimitSubqueryWalker::class); + $this->unbindUnusedQueryParams($subQuery); + } + + $subQuery->setFirstResult($offset)->setMaxResults($length); + + $foundIdRows = $subQuery->getScalarResult(); + + // don't do this for an empty id array + if ($foundIdRows === []) { + return new ArrayIterator([]); + } + + $whereInQuery = $this->cloneQuery($this->query); + $ids = array_map('current', $foundIdRows); + + $this->appendTreeWalker($whereInQuery, WhereInWalker::class); + $whereInQuery->setHint(WhereInWalker::HINT_PAGINATOR_HAS_IDS, true); + $whereInQuery->setFirstResult(0)->setMaxResults(null); + $whereInQuery->setCacheable($this->query->isCacheable()); + + $databaseIds = $this->convertWhereInIdentifiersToDatabaseValues($ids); + $whereInQuery->setParameter(WhereInWalker::PAGINATOR_ID_ALIAS, $databaseIds); + + $result = $whereInQuery->getResult($this->query->getHydrationMode()); + } else { + $result = $this->cloneQuery($this->query) + ->setMaxResults($length) + ->setFirstResult($offset) + ->setCacheable($this->query->isCacheable()) + ->getResult($this->query->getHydrationMode()); + } + + return new ArrayIterator($result); + } + + private function cloneQuery(Query $query): Query + { + $cloneQuery = clone $query; + + $cloneQuery->setParameters(clone $query->getParameters()); + $cloneQuery->setCacheable(false); + + foreach ($query->getHints() as $name => $value) { + $cloneQuery->setHint($name, $value); + } + + return $cloneQuery; + } + + /** + * Determines whether to use an output walker for the query. + */ + private function useOutputWalker(Query $query): bool + { + if ($this->useOutputWalkers === null) { + return (bool) $query->getHint(Query::HINT_CUSTOM_OUTPUT_WALKER) === false; + } + + return $this->useOutputWalkers; + } + + /** + * Appends a custom tree walker to the tree walkers hint. + * + * @psalm-param class-string $walkerClass + */ + private function appendTreeWalker(Query $query, string $walkerClass): void + { + $hints = $query->getHint(Query::HINT_CUSTOM_TREE_WALKERS); + + if ($hints === false) { + $hints = []; + } + + $hints[] = $walkerClass; + $query->setHint(Query::HINT_CUSTOM_TREE_WALKERS, $hints); + } + + /** + * Returns Query prepared to count. + */ + private function getCountQuery(): Query + { + $countQuery = $this->cloneQuery($this->query); + + if (! $countQuery->hasHint(CountWalker::HINT_DISTINCT)) { + $countQuery->setHint(CountWalker::HINT_DISTINCT, true); + } + + if ($this->useOutputWalker($countQuery)) { + $platform = $countQuery->getEntityManager()->getConnection()->getDatabasePlatform(); // law of demeter win + + $rsm = new ResultSetMapping(); + $rsm->addScalarResult($this->getSQLResultCasing($platform, 'dctrn_count'), 'count'); + + $countQuery->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, CountOutputWalker::class); + $countQuery->setResultSetMapping($rsm); + } else { + $this->appendTreeWalker($countQuery, CountWalker::class); + $this->unbindUnusedQueryParams($countQuery); + } + + $countQuery->setFirstResult(0)->setMaxResults(null); + + return $countQuery; + } + + private function unbindUnusedQueryParams(Query $query): void + { + $parser = new Parser($query); + $parameterMappings = $parser->parse()->getParameterMappings(); + /** @var Collection|Parameter[] $parameters */ + $parameters = $query->getParameters(); + + foreach ($parameters as $key => $parameter) { + $parameterName = $parameter->getName(); + + if (! (isset($parameterMappings[$parameterName]) || array_key_exists($parameterName, $parameterMappings))) { + unset($parameters[$key]); + } + } + + $query->setParameters($parameters); + } + + /** + * @param mixed[] $identifiers + * + * @return mixed[] + */ + private function convertWhereInIdentifiersToDatabaseValues(array $identifiers): array + { + $query = $this->cloneQuery($this->query); + $query->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, RootTypeWalker::class); + + $connection = $this->query->getEntityManager()->getConnection(); + $type = $query->getSQL(); + assert(is_string($type)); + + return array_map(static fn ($id): mixed => $connection->convertToDatabaseValue($id, $type), $identifiers); + } +} 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 @@ + root entity id type resolution can be cached in the query cache. + */ +final class RootTypeWalker extends SqlWalker +{ + public function walkSelectStatement(AST\SelectStatement $selectStatement): string + { + // Get the root entity and alias from the AST fromClause + $from = $selectStatement->fromClause->identificationVariableDeclarations; + + if (count($from) > 1) { + throw new RuntimeException('Can only process queries that select only one FROM component'); + } + + $fromRoot = reset($from); + $rootAlias = $fromRoot->rangeVariableDeclaration->aliasIdentificationVariable; + $rootClass = $this->getMetadataForDqlAlias($rootAlias); + $identifierFieldName = $rootClass->getSingleIdentifierFieldName(); + + return PersisterHelper::getTypeOfField( + $identifierFieldName, + $rootClass, + $this->getQuery() + ->getEntityManager(), + )[0]; + } +} 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 @@ +walkOrderByClause( + $this->orderByClause, + )) . ')'; + } + + /** + * @throws RowNumberOverFunctionNotEnabled + * + * @inheritdoc + */ + public function parse(Parser $parser): void + { + throw RowNumberOverFunctionNotEnabled::create(); + } +} 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 @@ +fromClause->identificationVariableDeclarations; + + if (count($from) > 1) { + throw new RuntimeException('Cannot count query which selects two FROM components, cannot make distinction'); + } + + $fromRoot = reset($from); + $rootAlias = $fromRoot->rangeVariableDeclaration->aliasIdentificationVariable; + $rootClass = $this->getMetadataForDqlAlias($rootAlias); + $identifierFieldName = $rootClass->getSingleIdentifierFieldName(); + + $pathType = PathExpression::TYPE_STATE_FIELD; + if (isset($rootClass->associationMappings[$identifierFieldName])) { + $pathType = PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION; + } + + $pathExpression = new PathExpression(PathExpression::TYPE_STATE_FIELD | PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION, $rootAlias, $identifierFieldName); + $pathExpression->type = $pathType; + + $hasIds = $this->_getQuery()->getHint(self::HINT_PAGINATOR_HAS_IDS); + + if ($hasIds) { + $arithmeticExpression = new ArithmeticExpression(); + $arithmeticExpression->simpleArithmeticExpression = new SimpleArithmeticExpression( + [$pathExpression], + ); + $expression = new InListExpression( + $arithmeticExpression, + [new InputParameter(':' . self::PAGINATOR_ID_ALIAS)], + ); + } else { + $expression = new NullComparisonExpression($pathExpression); + } + + $conditionalPrimary = new ConditionalPrimary(); + $conditionalPrimary->simpleConditionalExpression = $expression; + if ($selectStatement->whereClause) { + if ($selectStatement->whereClause->conditionalExpression instanceof ConditionalTerm) { + $selectStatement->whereClause->conditionalExpression->conditionalFactors[] = $conditionalPrimary; + } elseif ($selectStatement->whereClause->conditionalExpression instanceof ConditionalPrimary) { + $selectStatement->whereClause->conditionalExpression = new ConditionalExpression( + [ + new ConditionalTerm( + [ + $selectStatement->whereClause->conditionalExpression, + $conditionalPrimary, + ], + ), + ], + ); + } else { + $tmpPrimary = new ConditionalPrimary(); + $tmpPrimary->conditionalExpression = $selectStatement->whereClause->conditionalExpression; + $selectStatement->whereClause->conditionalExpression = new ConditionalTerm( + [ + $tmpPrimary, + $conditionalPrimary, + ], + ); + } + } else { + $selectStatement->whereClause = new WhereClause( + new ConditionalExpression( + [new ConditionalTerm([$conditionalPrimary])], + ), + ); + } + } +} 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 @@ + $mapping + */ + public function addResolveTargetEntity(string $originalEntity, string $newEntity, array $mapping): void + { + $mapping['targetEntity'] = ltrim($newEntity, '\\'); + $this->resolveTargetEntities[ltrim($originalEntity, '\\')] = $mapping; + } + + /** @internal this is an event callback, and should not be called directly */ + public function onClassMetadataNotFound(OnClassMetadataNotFoundEventArgs $args): void + { + if (array_key_exists($args->getClassName(), $this->resolveTargetEntities)) { + $args->setFoundMetadata( + $args + ->getObjectManager() + ->getClassMetadata($this->resolveTargetEntities[$args->getClassName()]['targetEntity']), + ); + } + } + + /** + * Processes event and resolves new target entity names. + * + * @internal this is an event callback, and should not be called directly + */ + public function loadClassMetadata(LoadClassMetadataEventArgs $args): void + { + $cm = $args->getClassMetadata(); + + foreach ($cm->associationMappings as $mapping) { + if (isset($this->resolveTargetEntities[$mapping->targetEntity])) { + $this->remapAssociation($cm, $mapping); + } + } + + foreach ($this->resolveTargetEntities as $interface => $data) { + if ($data['targetEntity'] === $cm->getName()) { + $args->getEntityManager()->getMetadataFactory()->setMetadataFor($interface, $cm); + } + } + + foreach ($cm->discriminatorMap as $value => $class) { + if (isset($this->resolveTargetEntities[$class])) { + $cm->addDiscriminatorMapClass($value, $this->resolveTargetEntities[$class]['targetEntity']); + } + } + } + + private function remapAssociation(ClassMetadata $classMetadata, AssociationMapping $mapping): void + { + $newMapping = $this->resolveTargetEntities[$mapping->targetEntity]; + $newMapping = array_replace_recursive( + $mapping->toArray(), + $newMapping, + ); + $newMapping['fieldName'] = $mapping->fieldName; + + unset($classMetadata->associationMappings[$mapping->fieldName]); + + switch ($mapping->type()) { + case ClassMetadata::MANY_TO_MANY: + $classMetadata->mapManyToMany($newMapping); + break; + case ClassMetadata::MANY_TO_ONE: + $classMetadata->mapManyToOne($newMapping); + break; + case ClassMetadata::ONE_TO_MANY: + $classMetadata->mapOneToMany($newMapping); + break; + case ClassMetadata::ONE_TO_ONE: + $classMetadata->mapOneToOne($newMapping); + break; + } + } +} 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 @@ +ClassMetadata class descriptors. + * + * @link www.doctrine-project.org + */ +class SchemaTool +{ + private const KNOWN_COLUMN_OPTIONS = ['comment', 'unsigned', 'fixed', 'default']; + + private readonly AbstractPlatform $platform; + private readonly QuoteStrategy $quoteStrategy; + private readonly AbstractSchemaManager $schemaManager; + + /** + * Initializes a new SchemaTool instance that uses the connection of the + * provided EntityManager. + */ + public function __construct(private readonly EntityManagerInterface $em) + { + $this->platform = $em->getConnection()->getDatabasePlatform(); + $this->quoteStrategy = $em->getConfiguration()->getQuoteStrategy(); + $this->schemaManager = $em->getConnection()->createSchemaManager(); + } + + /** + * Creates the database schema for the given array of ClassMetadata instances. + * + * @psalm-param list $classes + * + * @throws ToolsException + */ + public function createSchema(array $classes): void + { + $createSchemaSql = $this->getCreateSchemaSql($classes); + $conn = $this->em->getConnection(); + + foreach ($createSchemaSql as $sql) { + try { + $conn->executeStatement($sql); + } catch (Throwable $e) { + throw ToolsException::schemaToolFailure($sql, $e); + } + } + } + + /** + * Gets the list of DDL statements that are required to create the database schema for + * the given list of ClassMetadata instances. + * + * @psalm-param list $classes + * + * @return list The SQL statements needed to create the schema for the classes. + */ + public function getCreateSchemaSql(array $classes): array + { + $schema = $this->getSchemaFromMetadata($classes); + + return $schema->toSql($this->platform); + } + + /** + * Detects instances of ClassMetadata that don't need to be processed in the SchemaTool context. + * + * @psalm-param array $processedClasses + */ + private function processingNotRequired( + ClassMetadata $class, + array $processedClasses, + ): bool { + return isset($processedClasses[$class->name]) || + $class->isMappedSuperclass || + $class->isEmbeddedClass || + ($class->isInheritanceTypeSingleTable() && $class->name !== $class->rootEntityName) || + in_array($class->name, $this->em->getConfiguration()->getSchemaIgnoreClasses()); + } + + /** + * Resolves fields in index mapping to column names + * + * @param mixed[] $indexData index or unique constraint data + * + * @return list Column names from combined fields and columns mappings + */ + private function getIndexColumns(ClassMetadata $class, array $indexData): array + { + $columns = []; + + if ( + isset($indexData['columns'], $indexData['fields']) + || ( + ! isset($indexData['columns']) + && ! isset($indexData['fields']) + ) + ) { + throw MappingException::invalidIndexConfiguration( + (string) $class, + $indexData['name'] ?? 'unnamed', + ); + } + + if (isset($indexData['columns'])) { + $columns = $indexData['columns']; + } + + if (isset($indexData['fields'])) { + foreach ($indexData['fields'] as $fieldName) { + if ($class->hasField($fieldName)) { + $columns[] = $this->quoteStrategy->getColumnName($fieldName, $class, $this->platform); + } elseif ($class->hasAssociation($fieldName)) { + $assoc = $class->getAssociationMapping($fieldName); + assert($assoc->isToOneOwningSide()); + foreach ($assoc->joinColumns as $joinColumn) { + $columns[] = $this->quoteStrategy->getJoinColumnName($joinColumn, $class, $this->platform); + } + } + } + } + + return $columns; + } + + /** + * Creates a Schema instance from a given set of metadata classes. + * + * @psalm-param list $classes + * + * @throws NotSupported + */ + public function getSchemaFromMetadata(array $classes): Schema + { + // Reminder for processed classes, used for hierarchies + $processedClasses = []; + $eventManager = $this->em->getEventManager(); + $metadataSchemaConfig = $this->schemaManager->createSchemaConfig(); + + $schema = new Schema([], [], $metadataSchemaConfig); + + $addedFks = []; + $blacklistedFks = []; + + foreach ($classes as $class) { + if ($this->processingNotRequired($class, $processedClasses)) { + continue; + } + + $table = $schema->createTable($this->quoteStrategy->getTableName($class, $this->platform)); + + if ($class->isInheritanceTypeSingleTable()) { + $this->gatherColumns($class, $table); + $this->gatherRelationsSql($class, $table, $schema, $addedFks, $blacklistedFks); + + // Add the discriminator column + $this->addDiscriminatorColumnDefinition($class, $table); + + // Aggregate all the information from all classes in the hierarchy + foreach ($class->parentClasses as $parentClassName) { + // Parent class information is already contained in this class + $processedClasses[$parentClassName] = true; + } + + foreach ($class->subClasses as $subClassName) { + $subClass = $this->em->getClassMetadata($subClassName); + $this->gatherColumns($subClass, $table); + $this->gatherRelationsSql($subClass, $table, $schema, $addedFks, $blacklistedFks); + $processedClasses[$subClassName] = true; + } + } elseif ($class->isInheritanceTypeJoined()) { + // Add all non-inherited fields as columns + foreach ($class->fieldMappings as $fieldName => $mapping) { + if (! isset($mapping->inherited)) { + $this->gatherColumn($class, $mapping, $table); + } + } + + $this->gatherRelationsSql($class, $table, $schema, $addedFks, $blacklistedFks); + + // Add the discriminator column only to the root table + if ($class->name === $class->rootEntityName) { + $this->addDiscriminatorColumnDefinition($class, $table); + } else { + // Add an ID FK column to child tables + $pkColumns = []; + $inheritedKeyColumns = []; + + foreach ($class->identifier as $identifierField) { + if (isset($class->fieldMappings[$identifierField]->inherited)) { + $idMapping = $class->fieldMappings[$identifierField]; + $this->gatherColumn($class, $idMapping, $table); + $columnName = $this->quoteStrategy->getColumnName( + $identifierField, + $class, + $this->platform, + ); + // TODO: This seems rather hackish, can we optimize it? + $table->getColumn($columnName)->setAutoincrement(false); + + $pkColumns[] = $columnName; + $inheritedKeyColumns[] = $columnName; + + continue; + } + + if (isset($class->associationMappings[$identifierField]->inherited)) { + $idMapping = $class->associationMappings[$identifierField]; + assert($idMapping->isToOneOwningSide()); + + $targetEntity = current( + array_filter( + $classes, + static fn (ClassMetadata $class): bool => $class->name === $idMapping->targetEntity, + ), + ); + + foreach ($idMapping->joinColumns as $joinColumn) { + if (isset($targetEntity->fieldMappings[$joinColumn->referencedColumnName])) { + $columnName = $this->quoteStrategy->getJoinColumnName( + $joinColumn, + $class, + $this->platform, + ); + + $pkColumns[] = $columnName; + $inheritedKeyColumns[] = $columnName; + } + } + } + } + + if ($inheritedKeyColumns !== []) { + // Add a FK constraint on the ID column + $table->addForeignKeyConstraint( + $this->quoteStrategy->getTableName( + $this->em->getClassMetadata($class->rootEntityName), + $this->platform, + ), + $inheritedKeyColumns, + $inheritedKeyColumns, + ['onDelete' => 'CASCADE'], + ); + } + + if ($pkColumns !== []) { + $table->setPrimaryKey($pkColumns); + } + } + } else { + $this->gatherColumns($class, $table); + $this->gatherRelationsSql($class, $table, $schema, $addedFks, $blacklistedFks); + } + + $pkColumns = []; + + foreach ($class->identifier as $identifierField) { + if (isset($class->fieldMappings[$identifierField])) { + $pkColumns[] = $this->quoteStrategy->getColumnName($identifierField, $class, $this->platform); + } elseif (isset($class->associationMappings[$identifierField])) { + $assoc = $class->associationMappings[$identifierField]; + assert($assoc->isToOneOwningSide()); + + foreach ($assoc->joinColumns as $joinColumn) { + $pkColumns[] = $this->quoteStrategy->getJoinColumnName($joinColumn, $class, $this->platform); + } + } + } + + if (! $table->hasIndex('primary')) { + $table->setPrimaryKey($pkColumns); + } + + // there can be unique indexes automatically created for join column + // if join column is also primary key we should keep only primary key on this column + // so, remove indexes overruled by primary key + $primaryKey = $table->getIndex('primary'); + + foreach ($table->getIndexes() as $idxKey => $existingIndex) { + if ($primaryKey->overrules($existingIndex)) { + $table->dropIndex($idxKey); + } + } + + if (isset($class->table['indexes'])) { + foreach ($class->table['indexes'] as $indexName => $indexData) { + if (! isset($indexData['flags'])) { + $indexData['flags'] = []; + } + + $table->addIndex( + $this->getIndexColumns($class, $indexData), + is_numeric($indexName) ? null : $indexName, + (array) $indexData['flags'], + $indexData['options'] ?? [], + ); + } + } + + if (isset($class->table['uniqueConstraints'])) { + foreach ($class->table['uniqueConstraints'] as $indexName => $indexData) { + $uniqIndex = new Index('tmp__' . $indexName, $this->getIndexColumns($class, $indexData), true, false, [], $indexData['options'] ?? []); + + foreach ($table->getIndexes() as $tableIndexName => $tableIndex) { + if ($tableIndex->isFulfilledBy($uniqIndex)) { + $table->dropIndex($tableIndexName); + break; + } + } + + $table->addUniqueIndex($uniqIndex->getColumns(), is_numeric($indexName) ? null : $indexName, $indexData['options'] ?? []); + } + } + + if (isset($class->table['options'])) { + foreach ($class->table['options'] as $key => $val) { + $table->addOption($key, $val); + } + } + + $processedClasses[$class->name] = true; + + if ($class->isIdGeneratorSequence() && $class->name === $class->rootEntityName) { + $seqDef = $class->sequenceGeneratorDefinition; + $quotedName = $this->quoteStrategy->getSequenceName($seqDef, $class, $this->platform); + if (! $schema->hasSequence($quotedName)) { + $schema->createSequence( + $quotedName, + (int) $seqDef['allocationSize'], + (int) $seqDef['initialValue'], + ); + } + } + + if ($eventManager->hasListeners(ToolEvents::postGenerateSchemaTable)) { + $eventManager->dispatchEvent( + ToolEvents::postGenerateSchemaTable, + new GenerateSchemaTableEventArgs($class, $schema, $table), + ); + } + } + + if ($eventManager->hasListeners(ToolEvents::postGenerateSchema)) { + $eventManager->dispatchEvent( + ToolEvents::postGenerateSchema, + new GenerateSchemaEventArgs($this->em, $schema), + ); + } + + return $schema; + } + + /** + * Gets a portable column definition as required by the DBAL for the discriminator + * column of a class. + */ + private function addDiscriminatorColumnDefinition(ClassMetadata $class, Table $table): void + { + $discrColumn = $class->discriminatorColumn; + assert($discrColumn !== null); + + if (strtolower($discrColumn->type) === 'string' && ! isset($discrColumn->length)) { + $discrColumn->type = 'string'; + $discrColumn->length = 255; + } + + $options = [ + 'length' => $discrColumn->length ?? null, + 'notnull' => true, + ]; + + if (isset($discrColumn->columnDefinition)) { + $options['columnDefinition'] = $discrColumn->columnDefinition; + } + + $options = $this->gatherColumnOptions($discrColumn) + $options; + $table->addColumn($discrColumn->name, $discrColumn->type, $options); + } + + /** + * Gathers the column definitions as required by the DBAL of all field mappings + * found in the given class. + */ + private function gatherColumns(ClassMetadata $class, Table $table): void + { + $pkColumns = []; + + foreach ($class->fieldMappings as $mapping) { + if ($class->isInheritanceTypeSingleTable() && isset($mapping->inherited)) { + continue; + } + + $this->gatherColumn($class, $mapping, $table); + + if ($class->isIdentifier($mapping->fieldName)) { + $pkColumns[] = $this->quoteStrategy->getColumnName($mapping->fieldName, $class, $this->platform); + } + } + } + + /** + * Creates a column definition as required by the DBAL from an ORM field mapping definition. + * + * @param ClassMetadata $class The class that owns the field mapping. + * @psalm-param FieldMapping $mapping The field mapping. + */ + private function gatherColumn( + ClassMetadata $class, + FieldMapping $mapping, + Table $table, + ): void { + $columnName = $this->quoteStrategy->getColumnName($mapping->fieldName, $class, $this->platform); + $columnType = $mapping->type; + + $options = []; + $options['length'] = $mapping->length ?? null; + $options['notnull'] = isset($mapping->nullable) ? ! $mapping->nullable : true; + if ($class->isInheritanceTypeSingleTable() && $class->parentClasses) { + $options['notnull'] = false; + } + + $options['platformOptions'] = []; + $options['platformOptions']['version'] = $class->isVersioned && $class->versionField === $mapping->fieldName; + + if (strtolower($columnType) === 'string' && $options['length'] === null) { + $options['length'] = 255; + } + + if (isset($mapping->precision)) { + $options['precision'] = $mapping->precision; + } + + if (isset($mapping->scale)) { + $options['scale'] = $mapping->scale; + } + + if (isset($mapping->default)) { + $options['default'] = $mapping->default; + } + + if (isset($mapping->columnDefinition)) { + $options['columnDefinition'] = $mapping->columnDefinition; + } + + // the 'default' option can be overwritten here + $options = $this->gatherColumnOptions($mapping) + $options; + + if ($class->isIdGeneratorIdentity() && $class->getIdentifierFieldNames() === [$mapping->fieldName]) { + $options['autoincrement'] = true; + } + + if ($class->isInheritanceTypeJoined() && $class->name !== $class->rootEntityName) { + $options['autoincrement'] = false; + } + + if ($table->hasColumn($columnName)) { + // required in some inheritance scenarios + $table->modifyColumn($columnName, $options); + } else { + $table->addColumn($columnName, $columnType, $options); + } + + $isUnique = $mapping->unique ?? false; + if ($isUnique) { + $table->addUniqueIndex([$columnName]); + } + } + + /** + * Gathers the SQL for properly setting up the relations of the given class. + * This includes the SQL for foreign key constraints and join tables. + * + * @psalm-param array + * }> $addedFks + * @psalm-param array $blacklistedFks + * + * @throws NotSupported + */ + private function gatherRelationsSql( + ClassMetadata $class, + Table $table, + Schema $schema, + array &$addedFks, + array &$blacklistedFks, + ): void { + foreach ($class->associationMappings as $id => $mapping) { + if (isset($mapping->inherited) && ! in_array($id, $class->identifier, true)) { + continue; + } + + $foreignClass = $this->em->getClassMetadata($mapping->targetEntity); + + if ($mapping->isToOneOwningSide()) { + $primaryKeyColumns = []; // PK is unnecessary for this relation-type + + $this->gatherRelationJoinColumns( + $mapping->joinColumns, + $table, + $foreignClass, + $mapping, + $primaryKeyColumns, + $addedFks, + $blacklistedFks, + ); + } elseif ($mapping instanceof ManyToManyOwningSideMapping) { + // create join table + $joinTable = $mapping->joinTable; + + $theJoinTable = $schema->createTable( + $this->quoteStrategy->getJoinTableName($mapping, $foreignClass, $this->platform), + ); + + foreach ($joinTable->options as $key => $val) { + $theJoinTable->addOption($key, $val); + } + + $primaryKeyColumns = []; + + // Build first FK constraint (relation table => source table) + $this->gatherRelationJoinColumns( + $joinTable->joinColumns, + $theJoinTable, + $class, + $mapping, + $primaryKeyColumns, + $addedFks, + $blacklistedFks, + ); + + // Build second FK constraint (relation table => target table) + $this->gatherRelationJoinColumns( + $joinTable->inverseJoinColumns, + $theJoinTable, + $foreignClass, + $mapping, + $primaryKeyColumns, + $addedFks, + $blacklistedFks, + ); + + $theJoinTable->setPrimaryKey($primaryKeyColumns); + } + } + } + + /** + * Gets the class metadata that is responsible for the definition of the referenced column name. + * + * Previously this was a simple task, but with DDC-117 this problem is actually recursive. If its + * not a simple field, go through all identifier field names that are associations recursively and + * find that referenced column name. + * + * TODO: Is there any way to make this code more pleasing? + * + * @psalm-return array{ClassMetadata, string}|null + */ + private function getDefiningClass(ClassMetadata $class, string $referencedColumnName): array|null + { + $referencedFieldName = $class->getFieldName($referencedColumnName); + + if ($class->hasField($referencedFieldName)) { + return [$class, $referencedFieldName]; + } + + if (in_array($referencedColumnName, $class->getIdentifierColumnNames(), true)) { + // it seems to be an entity as foreign key + foreach ($class->getIdentifierFieldNames() as $fieldName) { + if ( + $class->hasAssociation($fieldName) + && $class->getSingleAssociationJoinColumnName($fieldName) === $referencedColumnName + ) { + return $this->getDefiningClass( + $this->em->getClassMetadata($class->associationMappings[$fieldName]->targetEntity), + $class->getSingleAssociationReferencedJoinColumnName($fieldName), + ); + } + } + } + + return null; + } + + /** + * Gathers columns and fk constraints that are required for one part of relationship. + * + * @psalm-param list $joinColumns + * @psalm-param list $primaryKeyColumns + * @psalm-param array + * }> $addedFks + * @psalm-param array $blacklistedFks + * + * @throws MissingColumnException + */ + private function gatherRelationJoinColumns( + array $joinColumns, + Table $theJoinTable, + ClassMetadata $class, + AssociationMapping $mapping, + array &$primaryKeyColumns, + array &$addedFks, + array &$blacklistedFks, + ): void { + $localColumns = []; + $foreignColumns = []; + $fkOptions = []; + $foreignTableName = $this->quoteStrategy->getTableName($class, $this->platform); + $uniqueConstraints = []; + + foreach ($joinColumns as $joinColumn) { + [$definingClass, $referencedFieldName] = $this->getDefiningClass( + $class, + $joinColumn->referencedColumnName, + ); + + if (! $definingClass) { + throw MissingColumnException::fromColumnSourceAndTarget( + $joinColumn->referencedColumnName, + $mapping->sourceEntity, + $mapping->targetEntity, + ); + } + + $quotedColumnName = $this->quoteStrategy->getJoinColumnName($joinColumn, $class, $this->platform); + $quotedRefColumnName = $this->quoteStrategy->getReferencedJoinColumnName( + $joinColumn, + $class, + $this->platform, + ); + + $primaryKeyColumns[] = $quotedColumnName; + $localColumns[] = $quotedColumnName; + $foreignColumns[] = $quotedRefColumnName; + + if (! $theJoinTable->hasColumn($quotedColumnName)) { + // Only add the column to the table if it does not exist already. + // It might exist already if the foreign key is mapped into a regular + // property as well. + + $fieldMapping = $definingClass->getFieldMapping($referencedFieldName); + + $columnOptions = ['notnull' => false]; + + if (isset($joinColumn->columnDefinition)) { + $columnOptions['columnDefinition'] = $joinColumn->columnDefinition; + } elseif (isset($fieldMapping->columnDefinition)) { + $columnOptions['columnDefinition'] = $fieldMapping->columnDefinition; + } + + if (isset($joinColumn->nullable)) { + $columnOptions['notnull'] = ! $joinColumn->nullable; + } + + $columnOptions += $this->gatherColumnOptions($fieldMapping); + + if (isset($fieldMapping->length)) { + $columnOptions['length'] = $fieldMapping->length; + } + + if ($fieldMapping->type === 'decimal') { + $columnOptions['scale'] = $fieldMapping->scale; + $columnOptions['precision'] = $fieldMapping->precision; + } + + $columnOptions = $this->gatherColumnOptions($joinColumn) + $columnOptions; + + $theJoinTable->addColumn($quotedColumnName, $fieldMapping->type, $columnOptions); + } + + if (isset($joinColumn->unique) && $joinColumn->unique === true) { + $uniqueConstraints[] = ['columns' => [$quotedColumnName]]; + } + + if (isset($joinColumn->onDelete)) { + $fkOptions['onDelete'] = $joinColumn->onDelete; + } + } + + // Prefer unique constraints over implicit simple indexes created for foreign keys. + // Also avoids index duplication. + foreach ($uniqueConstraints as $indexName => $unique) { + $theJoinTable->addUniqueIndex($unique['columns'], is_numeric($indexName) ? null : $indexName); + } + + $compositeName = $theJoinTable->getName() . '.' . implode('', $localColumns); + if ( + isset($addedFks[$compositeName]) + && ($foreignTableName !== $addedFks[$compositeName]['foreignTableName'] + || 0 < count(array_diff($foreignColumns, $addedFks[$compositeName]['foreignColumns']))) + ) { + foreach ($theJoinTable->getForeignKeys() as $fkName => $key) { + if ( + count(array_diff($key->getLocalColumns(), $localColumns)) === 0 + && (($key->getForeignTableName() !== $foreignTableName) + || 0 < count(array_diff($key->getForeignColumns(), $foreignColumns))) + ) { + $theJoinTable->removeForeignKey($fkName); + break; + } + } + + $blacklistedFks[$compositeName] = true; + } elseif (! isset($blacklistedFks[$compositeName])) { + $addedFks[$compositeName] = ['foreignTableName' => $foreignTableName, 'foreignColumns' => $foreignColumns]; + $theJoinTable->addForeignKeyConstraint( + $foreignTableName, + $localColumns, + $foreignColumns, + $fkOptions, + ); + } + } + + /** @return mixed[] */ + private function gatherColumnOptions(JoinColumnMapping|FieldMapping|DiscriminatorColumnMapping $mapping): array + { + $mappingOptions = $mapping->options ?? []; + + if (isset($mapping->enumType)) { + $mappingOptions['enumType'] = $mapping->enumType; + } + + if (($mappingOptions['default'] ?? null) instanceof BackedEnum) { + $mappingOptions['default'] = $mappingOptions['default']->value; + } + + if (empty($mappingOptions)) { + return []; + } + + $options = array_intersect_key($mappingOptions, array_flip(self::KNOWN_COLUMN_OPTIONS)); + $options['platformOptions'] = array_diff_key($mappingOptions, $options); + + return $options; + } + + /** + * Drops the database schema for the given classes. + * + * In any way when an exception is thrown it is suppressed since drop was + * issued for all classes of the schema and some probably just don't exist. + * + * @psalm-param list $classes + */ + public function dropSchema(array $classes): void + { + $dropSchemaSql = $this->getDropSchemaSQL($classes); + $conn = $this->em->getConnection(); + + foreach ($dropSchemaSql as $sql) { + try { + $conn->executeStatement($sql); + } catch (Throwable) { + // ignored + } + } + } + + /** + * Drops all elements in the database of the current connection. + */ + public function dropDatabase(): void + { + $dropSchemaSql = $this->getDropDatabaseSQL(); + $conn = $this->em->getConnection(); + + foreach ($dropSchemaSql as $sql) { + $conn->executeStatement($sql); + } + } + + /** + * Gets the SQL needed to drop the database schema for the connections database. + * + * @return list + */ + public function getDropDatabaseSQL(): array + { + return $this->schemaManager + ->introspectSchema() + ->toDropSql($this->platform); + } + + /** + * Gets SQL to drop the tables defined by the passed classes. + * + * @psalm-param list $classes + * + * @return list + */ + public function getDropSchemaSQL(array $classes): array + { + $schema = $this->getSchemaFromMetadata($classes); + + $deployedSchema = $this->schemaManager->introspectSchema(); + + foreach ($schema->getTables() as $table) { + if (! $deployedSchema->hasTable($table->getName())) { + $schema->dropTable($table->getName()); + } + } + + if ($this->platform->supportsSequences()) { + foreach ($schema->getSequences() as $sequence) { + if (! $deployedSchema->hasSequence($sequence->getName())) { + $schema->dropSequence($sequence->getName()); + } + } + + foreach ($schema->getTables() as $table) { + $primaryKey = $table->getPrimaryKey(); + if ($primaryKey === null) { + continue; + } + + $columns = $primaryKey->getColumns(); + if (count($columns) === 1) { + $checkSequence = $table->getName() . '_' . $columns[0] . '_seq'; + if ($deployedSchema->hasSequence($checkSequence) && ! $schema->hasSequence($checkSequence)) { + $schema->createSequence($checkSequence); + } + } + } + } + + return $schema->toDropSql($this->platform); + } + + /** + * Updates the database schema of the given classes by comparing the ClassMetadata + * instances to the current database schema that is inspected. + * + * @param mixed[] $classes + */ + public function updateSchema(array $classes): void + { + $conn = $this->em->getConnection(); + + foreach ($this->getUpdateSchemaSql($classes) as $sql) { + $conn->executeStatement($sql); + } + } + + /** + * Gets the sequence of SQL statements that need to be performed in order + * to bring the given class mappings in-synch with the relational schema. + * + * @param list $classes The classes to consider. + * + * @return list The sequence of SQL statements. + */ + public function getUpdateSchemaSql(array $classes): array + { + $toSchema = $this->getSchemaFromMetadata($classes); + $fromSchema = $this->createSchemaForComparison($toSchema); + $comparator = $this->schemaManager->createComparator(); + $schemaDiff = $comparator->compareSchemas($fromSchema, $toSchema); + + return $this->platform->getAlterSchemaSQL($schemaDiff); + } + + /** + * Creates the schema from the database, ensuring tables from the target schema are whitelisted for comparison. + */ + private function createSchemaForComparison(Schema $toSchema): Schema + { + $connection = $this->em->getConnection(); + + // backup schema assets filter + $config = $connection->getConfiguration(); + $previousFilter = $config->getSchemaAssetsFilter(); + + if ($previousFilter === null) { + return $this->schemaManager->introspectSchema(); + } + + // whitelist assets we already know about in $toSchema, use the existing filter otherwise + $config->setSchemaAssetsFilter(static function ($asset) use ($previousFilter, $toSchema): bool { + $assetName = $asset instanceof AbstractAsset ? $asset->getName() : $asset; + + return $toSchema->hasTable($assetName) || $toSchema->hasSequence($assetName) || $previousFilter($asset); + }); + + try { + return $this->schemaManager->introspectSchema(); + } finally { + // restore schema assets filter + $config->setSchemaAssetsFilter($previousFilter); + } + } +} 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 @@ + ['string'], + BigIntType::class => ['int', 'string'], + BooleanType::class => ['bool'], + DecimalType::class => ['string'], + FloatType::class => ['float'], + GuidType::class => ['string'], + IntegerType::class => ['int'], + JsonType::class => ['array'], + SimpleArrayType::class => ['array'], + SmallIntType::class => ['int'], + StringType::class => ['string'], + TextType::class => ['string'], + ]; + + public function __construct( + private readonly EntityManagerInterface $em, + private readonly bool $validatePropertyTypes = true, + ) { + } + + /** + * Checks the internal consistency of all mapping files. + * + * There are several checks that can't be done at runtime or are too expensive, which can be verified + * with this command. For example: + * + * 1. Check if a relation with "mappedBy" is actually connected to that specified field. + * 2. Check if "mappedBy" and "inversedBy" are consistent to each other. + * 3. Check if "referencedColumnName" attributes are really pointing to primary key columns. + * + * @psalm-return array> + */ + public function validateMapping(): array + { + $errors = []; + $cmf = $this->em->getMetadataFactory(); + $classes = $cmf->getAllMetadata(); + + foreach ($classes as $class) { + $ce = $this->validateClass($class); + if ($ce) { + $errors[$class->name] = $ce; + } + } + + return $errors; + } + + /** + * Validates a single class of the current. + * + * @return string[] + * @psalm-return list + */ + public function validateClass(ClassMetadata $class): array + { + $ce = []; + $cmf = $this->em->getMetadataFactory(); + + foreach ($class->fieldMappings as $fieldName => $mapping) { + if (! Type::hasType($mapping->type)) { + $ce[] = "The field '" . $class->name . '#' . $fieldName . "' uses a non-existent type '" . $mapping->type . "'."; + } + } + + if ($this->validatePropertyTypes) { + array_push($ce, ...$this->validatePropertiesTypes($class)); + } + + foreach ($class->associationMappings as $fieldName => $assoc) { + if (! class_exists($assoc->targetEntity) || $cmf->isTransient($assoc->targetEntity)) { + $ce[] = "The target entity '" . $assoc->targetEntity . "' specified on " . $class->name . '#' . $fieldName . ' is unknown or not an entity.'; + + return $ce; + } + + $targetMetadata = $cmf->getMetadataFor($assoc->targetEntity); + + if ($targetMetadata->isMappedSuperclass) { + $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.'; + + return $ce; + } + + if (isset($assoc->id) && $targetMetadata->containsForeignIdentifier) { + $ce[] = "Cannot map association '" . $class->name . '#' . $fieldName . ' as identifier, because ' . + "the target entity '" . $targetMetadata->name . "' also maps an association as identifier."; + } + + if (! $assoc->isOwningSide()) { + if ($targetMetadata->hasField($assoc->mappedBy)) { + $ce[] = 'The association ' . $class->name . '#' . $fieldName . ' refers to the owning side ' . + 'field ' . $assoc->targetEntity . '#' . $assoc->mappedBy . ' which is not defined as association, but as field.'; + } + + if (! $targetMetadata->hasAssociation($assoc->mappedBy)) { + $ce[] = 'The association ' . $class->name . '#' . $fieldName . ' refers to the owning side ' . + 'field ' . $assoc->targetEntity . '#' . $assoc->mappedBy . ' which does not exist.'; + } elseif ($targetMetadata->associationMappings[$assoc->mappedBy]->inversedBy === null) { + $ce[] = 'The field ' . $class->name . '#' . $fieldName . ' is on the inverse side of a ' . + 'bi-directional relationship, but the specified mappedBy association on the target-entity ' . + $assoc->targetEntity . '#' . $assoc->mappedBy . ' does not contain the required ' . + "'inversedBy=\"" . $fieldName . "\"' attribute."; + } elseif ($targetMetadata->associationMappings[$assoc->mappedBy]->inversedBy !== $fieldName) { + $ce[] = 'The mappings ' . $class->name . '#' . $fieldName . ' and ' . + $assoc->targetEntity . '#' . $assoc->mappedBy . ' are ' . + 'inconsistent with each other.'; + } + } + + if ($assoc->isOwningSide() && $assoc->inversedBy !== null) { + if ($targetMetadata->hasField($assoc->inversedBy)) { + $ce[] = 'The association ' . $class->name . '#' . $fieldName . ' refers to the inverse side ' . + 'field ' . $assoc->targetEntity . '#' . $assoc->inversedBy . ' which is not defined as association.'; + } + + if (! $targetMetadata->hasAssociation($assoc->inversedBy)) { + $ce[] = 'The association ' . $class->name . '#' . $fieldName . ' refers to the inverse side ' . + 'field ' . $assoc->targetEntity . '#' . $assoc->inversedBy . ' which does not exist.'; + } elseif ($targetMetadata->associationMappings[$assoc->inversedBy]->isOwningSide()) { + $ce[] = 'The field ' . $class->name . '#' . $fieldName . ' is on the owning side of a ' . + 'bi-directional relationship, but the specified inversedBy association on the target-entity ' . + $assoc->targetEntity . '#' . $assoc->inversedBy . ' does not contain the required ' . + "'mappedBy=\"" . $fieldName . "\"' attribute."; + } elseif ($targetMetadata->associationMappings[$assoc->inversedBy]->mappedBy !== $fieldName) { + $ce[] = 'The mappings ' . $class->name . '#' . $fieldName . ' and ' . + $assoc->targetEntity . '#' . $assoc->inversedBy . ' are ' . + 'inconsistent with each other.'; + } + + // Verify inverse side/owning side match each other + if (array_key_exists($assoc->inversedBy, $targetMetadata->associationMappings)) { + $targetAssoc = $targetMetadata->associationMappings[$assoc->inversedBy]; + if ($assoc->isOneToOne() && ! $targetAssoc->isOneToOne()) { + $ce[] = 'If association ' . $class->name . '#' . $fieldName . ' is one-to-one, then the inversed ' . + 'side ' . $targetMetadata->name . '#' . $assoc->inversedBy . ' has to be one-to-one as well.'; + } elseif ($assoc->isManyToOne() && ! $targetAssoc->isOneToMany()) { + $ce[] = 'If association ' . $class->name . '#' . $fieldName . ' is many-to-one, then the inversed ' . + 'side ' . $targetMetadata->name . '#' . $assoc->inversedBy . ' has to be one-to-many.'; + } elseif ($assoc->isManyToMany() && ! $targetAssoc->isManyToMany()) { + $ce[] = 'If association ' . $class->name . '#' . $fieldName . ' is many-to-many, then the inversed ' . + 'side ' . $targetMetadata->name . '#' . $assoc->inversedBy . ' has to be many-to-many as well.'; + } + } + } + + if ($assoc->isOwningSide()) { + if ($assoc->isManyToManyOwningSide()) { + $identifierColumns = $class->getIdentifierColumnNames(); + foreach ($assoc->joinTable->joinColumns as $joinColumn) { + if (! in_array($joinColumn->referencedColumnName, $identifierColumns, true)) { + $ce[] = "The referenced column name '" . $joinColumn->referencedColumnName . "' " . + "has to be a primary key column on the target entity class '" . $class->name . "'."; + break; + } + } + + $identifierColumns = $targetMetadata->getIdentifierColumnNames(); + foreach ($assoc->joinTable->inverseJoinColumns as $inverseJoinColumn) { + if (! in_array($inverseJoinColumn->referencedColumnName, $identifierColumns, true)) { + $ce[] = "The referenced column name '" . $inverseJoinColumn->referencedColumnName . "' " . + "has to be a primary key column on the target entity class '" . $targetMetadata->name . "'."; + break; + } + } + + if (count($targetMetadata->getIdentifierColumnNames()) !== count($assoc->joinTable->inverseJoinColumns)) { + $ce[] = "The inverse join columns of the many-to-many table '" . $assoc->joinTable->name . "' " . + "have to contain to ALL identifier columns of the target entity '" . $targetMetadata->name . "', " . + "however '" . implode(', ', array_diff($targetMetadata->getIdentifierColumnNames(), array_values($assoc->relationToTargetKeyColumns))) . + "' are missing."; + } + + if (count($class->getIdentifierColumnNames()) !== count($assoc->joinTable->joinColumns)) { + $ce[] = "The join columns of the many-to-many table '" . $assoc->joinTable->name . "' " . + "have to contain to ALL identifier columns of the source entity '" . $class->name . "', " . + "however '" . implode(', ', array_diff($class->getIdentifierColumnNames(), array_values($assoc->relationToSourceKeyColumns))) . + "' are missing."; + } + } elseif ($assoc->isToOneOwningSide()) { + $identifierColumns = $targetMetadata->getIdentifierColumnNames(); + foreach ($assoc->joinColumns as $joinColumn) { + if (! in_array($joinColumn->referencedColumnName, $identifierColumns, true)) { + $ce[] = "The referenced column name '" . $joinColumn->referencedColumnName . "' " . + "has to be a primary key column on the target entity class '" . $targetMetadata->name . "'."; + } + } + + if (count($identifierColumns) !== count($assoc->joinColumns)) { + $ids = []; + + foreach ($assoc->joinColumns as $joinColumn) { + $ids[] = $joinColumn->name; + } + + $ce[] = "The join columns of the association '" . $assoc->fieldName . "' " . + "have to match to ALL identifier columns of the target entity '" . $targetMetadata->name . "', " . + "however '" . implode(', ', array_diff($targetMetadata->getIdentifierColumnNames(), $ids)) . + "' are missing."; + } + } + } + + if ($assoc->isOrdered()) { + foreach ($assoc->orderBy() as $orderField => $orientation) { + if (! $targetMetadata->hasField($orderField) && ! $targetMetadata->hasAssociation($orderField)) { + $ce[] = 'The association ' . $class->name . '#' . $fieldName . ' is ordered by a foreign field ' . + $orderField . ' that is not a field on the target entity ' . $targetMetadata->name . '.'; + continue; + } + + if ($targetMetadata->isCollectionValuedAssociation($orderField)) { + $ce[] = 'The association ' . $class->name . '#' . $fieldName . ' is ordered by a field ' . + $orderField . ' on ' . $targetMetadata->name . ' that is a collection-valued association.'; + continue; + } + + if ($targetMetadata->isAssociationInverseSide($orderField)) { + $ce[] = 'The association ' . $class->name . '#' . $fieldName . ' is ordered by a field ' . + $orderField . ' on ' . $targetMetadata->name . ' that is the inverse side of an association.'; + continue; + } + } + } + } + + if ( + ! $class->isInheritanceTypeNone() + && ! $class->isRootEntity() + && ($class->reflClass !== null && ! $class->reflClass->isAbstract()) + && ! $class->isMappedSuperclass + && array_search($class->name, $class->discriminatorMap, true) === false + ) { + $ce[] = "Entity class '" . $class->name . "' is part of inheritance hierarchy, but is " . + "not mapped in the root entity '" . $class->rootEntityName . "' discriminator map. " . + 'All subclasses must be listed in the discriminator map.'; + } + + foreach ($class->subClasses as $subClass) { + if (! in_array($class->name, class_parents($subClass), true)) { + $ce[] = "According to the discriminator map class '" . $subClass . "' has to be a child " . + "of '" . $class->name . "' but these entities are not related through inheritance."; + } + } + + return $ce; + } + + /** + * Checks if the Database Schema is in sync with the current metadata state. + */ + public function schemaInSyncWithMetadata(): bool + { + return count($this->getUpdateSchemaList()) === 0; + } + + /** + * Returns the list of missing Database Schema updates. + * + * @return array + */ + public function getUpdateSchemaList(): array + { + $schemaTool = new SchemaTool($this->em); + + $allMetadata = $this->em->getMetadataFactory()->getAllMetadata(); + + return $schemaTool->getUpdateSchemaSql($allMetadata); + } + + /** @return list containing the found issues */ + private function validatePropertiesTypes(ClassMetadata $class): array + { + return array_values( + array_filter( + array_map( + function (FieldMapping $fieldMapping) use ($class): string|null { + $fieldName = $fieldMapping->fieldName; + assert(isset($class->reflFields[$fieldName])); + $propertyType = $class->reflFields[$fieldName]->getType(); + + // If the field type is not a built-in type, we cannot check it + if (! Type::hasType($fieldMapping->type)) { + return null; + } + + // If the property type is not a named type, we cannot check it + if (! ($propertyType instanceof ReflectionNamedType) || $propertyType->getName() === 'mixed') { + return null; + } + + $metadataFieldType = $this->findBuiltInType(Type::getType($fieldMapping->type)); + + //If the metadata field type is not a mapped built-in type, we cannot check it + if ($metadataFieldType === null) { + return null; + } + + $propertyType = $propertyType->getName(); + + // If the property type is the same as the metadata field type, we are ok + if (in_array($propertyType, $metadataFieldType, true)) { + return null; + } + + if (is_a($propertyType, BackedEnum::class, true)) { + $backingType = (string) (new ReflectionEnum($propertyType))->getBackingType(); + + if (! in_array($backingType, $metadataFieldType, true)) { + return sprintf( + "The field '%s#%s' has the property type '%s' with a backing type of '%s' that differs from the metadata field type '%s'.", + $class->name, + $fieldName, + $propertyType, + $backingType, + implode('|', $metadataFieldType), + ); + } + + if (! isset($fieldMapping->enumType) || $propertyType === $fieldMapping->enumType) { + return null; + } + + return sprintf( + "The field '%s#%s' has the property type '%s' that differs from the metadata enumType '%s'.", + $class->name, + $fieldName, + $propertyType, + $fieldMapping->enumType, + ); + } + + if ( + isset($fieldMapping->enumType) + && $propertyType !== $fieldMapping->enumType + && interface_exists($propertyType) + && is_a($fieldMapping->enumType, $propertyType, true) + ) { + $backingType = (string) (new ReflectionEnum($fieldMapping->enumType))->getBackingType(); + + if (in_array($backingType, $metadataFieldType, true)) { + return null; + } + + return sprintf( + "The field '%s#%s' has the metadata enumType '%s' with a backing type of '%s' that differs from the metadata field type '%s'.", + $class->name, + $fieldName, + $fieldMapping->enumType, + $backingType, + implode('|', $metadataFieldType), + ); + } + + if ( + $fieldMapping->type === 'json' + && in_array($propertyType, ['string', 'int', 'float', 'bool', 'true', 'false', 'null'], true) + ) { + return null; + } + + return sprintf( + "The field '%s#%s' has the property type '%s' that differs from the metadata field type '%s' returned by the '%s' DBAL type.", + $class->name, + $fieldName, + $propertyType, + implode('|', $metadataFieldType), + $fieldMapping->type, + ); + }, + $class->fieldMappings, + ), + ), + ); + } + + /** + * The exact DBAL type must be used (no subclasses), since consumers of doctrine/orm may have their own + * customization around field types. + * + * @return list|null + */ + private function findBuiltInType(Type $type): array|null + { + $typeName = $type::class; + + return self::BUILTIN_TYPES_MAP[$typeName] ?? null; + } +} 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 @@ +getMessage() . "' while executing DDL: " . $sql, + 0, + $e, + ); + } +} 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 @@ +> + */ + private array $identityMap = []; + + /** + * Map of all identifiers of managed entities. + * Keys are object ids (spl_object_id). + * + * @psalm-var array> + */ + private array $entityIdentifiers = []; + + /** + * Map of the original entity data of managed entities. + * Keys are object ids (spl_object_id). This is used for calculating changesets + * at commit time. + * + * Internal note: Note that PHPs "copy-on-write" behavior helps a lot with memory usage. + * A value will only really be copied if the value in the entity is modified + * by the user. + * + * @psalm-var array> + */ + private array $originalEntityData = []; + + /** + * Map of entity changes. Keys are object ids (spl_object_id). + * Filled at the beginning of a commit of the UnitOfWork and cleaned at the end. + * + * @psalm-var array> + */ + private array $entityChangeSets = []; + + /** + * The (cached) states of any known entities. + * Keys are object ids (spl_object_id). + * + * @psalm-var array + */ + private array $entityStates = []; + + /** + * Map of entities that are scheduled for dirty checking at commit time. + * This is only used for entities with a change tracking policy of DEFERRED_EXPLICIT. + * Keys are object ids (spl_object_id). + * + * @psalm-var array> + */ + private array $scheduledForSynchronization = []; + + /** + * A list of all pending entity insertions. + * + * @psalm-var array + */ + private array $entityInsertions = []; + + /** + * A list of all pending entity updates. + * + * @psalm-var array + */ + private array $entityUpdates = []; + + /** + * Any pending extra updates that have been scheduled by persisters. + * + * @psalm-var array}> + */ + private array $extraUpdates = []; + + /** + * A list of all pending entity deletions. + * + * @psalm-var array + */ + private array $entityDeletions = []; + + /** + * New entities that were discovered through relationships that were not + * marked as cascade-persist. During flush, this array is populated and + * then pruned of any entities that were discovered through a valid + * cascade-persist path. (Leftovers cause an error.) + * + * Keys are OIDs, payload is a two-item array describing the association + * and the entity. + * + * @var array indexed by respective object spl_object_id() + */ + private array $nonCascadedNewDetectedEntities = []; + + /** + * All pending collection deletions. + * + * @psalm-var array> + */ + private array $collectionDeletions = []; + + /** + * All pending collection updates. + * + * @psalm-var array> + */ + private array $collectionUpdates = []; + + /** + * List of collections visited during changeset calculation on a commit-phase of a UnitOfWork. + * At the end of the UnitOfWork all these collections will make new snapshots + * of their data. + * + * @psalm-var array> + */ + private array $visitedCollections = []; + + /** + * List of collections visited during the changeset calculation that contain to-be-removed + * entities and need to have keys removed post commit. + * + * Indexed by Collection object ID, which also serves as the key in self::$visitedCollections; + * values are the key names that need to be removed. + * + * @psalm-var array> + */ + private array $pendingCollectionElementRemovals = []; + + /** + * The entity persister instances used to persist entity instances. + * + * @psalm-var array + */ + private array $persisters = []; + + /** + * The collection persister instances used to persist collections. + * + * @psalm-var array + */ + private array $collectionPersisters = []; + + /** + * The EventManager used for dispatching events. + */ + private readonly EventManager $evm; + + /** + * The ListenersInvoker used for dispatching events. + */ + private readonly ListenersInvoker $listenersInvoker; + + /** + * The IdentifierFlattener used for manipulating identifiers + */ + private readonly IdentifierFlattener $identifierFlattener; + + /** + * Orphaned entities that are scheduled for removal. + * + * @psalm-var array + */ + private array $orphanRemovals = []; + + /** + * Read-Only objects are never evaluated + * + * @var array + */ + private array $readOnlyObjects = []; + + /** + * Map of Entity Class-Names and corresponding IDs that should eager loaded when requested. + * + * @psalm-var array> + */ + private array $eagerLoadingEntities = []; + + /** @var array> */ + private array $eagerLoadingCollections = []; + + protected bool $hasCache = false; + + /** + * Helper for handling completion of hydration + */ + private readonly HydrationCompleteHandler $hydrationCompleteHandler; + + /** + * Initializes a new UnitOfWork instance, bound to the given EntityManager. + * + * @param EntityManagerInterface $em The EntityManager that "owns" this UnitOfWork instance. + */ + public function __construct( + private readonly EntityManagerInterface $em, + ) { + $this->evm = $em->getEventManager(); + $this->listenersInvoker = new ListenersInvoker($em); + $this->hasCache = $em->getConfiguration()->isSecondLevelCacheEnabled(); + $this->identifierFlattener = new IdentifierFlattener($this, $em->getMetadataFactory()); + $this->hydrationCompleteHandler = new HydrationCompleteHandler($this->listenersInvoker, $em); + } + + /** + * Commits the UnitOfWork, executing all operations that have been postponed + * up to this point. The state of all managed entities will be synchronized with + * the database. + * + * The operations are executed in the following order: + * + * 1) All entity insertions + * 2) All entity updates + * 3) All collection deletions + * 4) All collection updates + * 5) All entity deletions + * + * @throws Exception + */ + public function commit(): void + { + $connection = $this->em->getConnection(); + + if ($connection instanceof PrimaryReadReplicaConnection) { + $connection->ensureConnectedToPrimary(); + } + + // Raise preFlush + if ($this->evm->hasListeners(Events::preFlush)) { + $this->evm->dispatchEvent(Events::preFlush, new PreFlushEventArgs($this->em)); + } + + // Compute changes done since last commit. + $this->computeChangeSets(); + + if ( + ! ($this->entityInsertions || + $this->entityDeletions || + $this->entityUpdates || + $this->collectionUpdates || + $this->collectionDeletions || + $this->orphanRemovals) + ) { + $this->dispatchOnFlushEvent(); + $this->dispatchPostFlushEvent(); + + $this->postCommitCleanup(); + + return; // Nothing to do. + } + + $this->assertThatThereAreNoUnintentionallyNonPersistedAssociations(); + + if ($this->orphanRemovals) { + foreach ($this->orphanRemovals as $orphan) { + $this->remove($orphan); + } + } + + $this->dispatchOnFlushEvent(); + + $conn = $this->em->getConnection(); + $conn->beginTransaction(); + + try { + // Collection deletions (deletions of complete collections) + foreach ($this->collectionDeletions as $collectionToDelete) { + // Deferred explicit tracked collections can be removed only when owning relation was persisted + $owner = $collectionToDelete->getOwner(); + + if ($this->em->getClassMetadata($owner::class)->isChangeTrackingDeferredImplicit() || $this->isScheduledForDirtyCheck($owner)) { + $this->getCollectionPersister($collectionToDelete->getMapping())->delete($collectionToDelete); + } + } + + if ($this->entityInsertions) { + // Perform entity insertions first, so that all new entities have their rows in the database + // and can be referred to by foreign keys. The commit order only needs to take new entities + // into account (new entities referring to other new entities), since all other types (entities + // with updates or scheduled deletions) are currently not a problem, since they are already + // in the database. + $this->executeInserts(); + } + + if ($this->entityUpdates) { + // Updates do not need to follow a particular order + $this->executeUpdates(); + } + + // Extra updates that were requested by persisters. + // This may include foreign keys that could not be set when an entity was inserted, + // which may happen in the case of circular foreign key relationships. + if ($this->extraUpdates) { + $this->executeExtraUpdates(); + } + + // Collection updates (deleteRows, updateRows, insertRows) + // No particular order is necessary, since all entities themselves are already + // in the database + foreach ($this->collectionUpdates as $collectionToUpdate) { + $this->getCollectionPersister($collectionToUpdate->getMapping())->update($collectionToUpdate); + } + + // Entity deletions come last. Their order only needs to take care of other deletions + // (first delete entities depending upon others, before deleting depended-upon entities). + if ($this->entityDeletions) { + $this->executeDeletions(); + } + + $commitFailed = false; + try { + if ($conn->commit() === false) { + $commitFailed = true; + } + } catch (DBAL\Exception $e) { + $commitFailed = true; + } + + if ($commitFailed) { + throw new OptimisticLockException('Commit failed', null, $e ?? null); + } + } catch (Throwable $e) { + $this->em->close(); + + if ($conn->isTransactionActive()) { + $conn->rollBack(); + } + + $this->afterTransactionRolledBack(); + + throw $e; + } + + $this->afterTransactionComplete(); + + // Unset removed entities from collections, and take new snapshots from + // all visited collections. + foreach ($this->visitedCollections as $coid => $coll) { + if (isset($this->pendingCollectionElementRemovals[$coid])) { + foreach ($this->pendingCollectionElementRemovals[$coid] as $key => $valueIgnored) { + unset($coll[$key]); + } + } + + $coll->takeSnapshot(); + } + + $this->dispatchPostFlushEvent(); + + $this->postCommitCleanup(); + } + + private function postCommitCleanup(): void + { + $this->entityInsertions = + $this->entityUpdates = + $this->entityDeletions = + $this->extraUpdates = + $this->collectionUpdates = + $this->nonCascadedNewDetectedEntities = + $this->collectionDeletions = + $this->pendingCollectionElementRemovals = + $this->visitedCollections = + $this->orphanRemovals = + $this->entityChangeSets = + $this->scheduledForSynchronization = []; + } + + /** + * Computes the changesets of all entities scheduled for insertion. + */ + private function computeScheduleInsertsChangeSets(): void + { + foreach ($this->entityInsertions as $entity) { + $class = $this->em->getClassMetadata($entity::class); + + $this->computeChangeSet($class, $entity); + } + } + + /** + * Executes any extra updates that have been scheduled. + */ + private function executeExtraUpdates(): void + { + foreach ($this->extraUpdates as $oid => $update) { + [$entity, $changeset] = $update; + + $this->entityChangeSets[$oid] = $changeset; + $this->getEntityPersister($entity::class)->update($entity); + } + + $this->extraUpdates = []; + } + + /** + * Gets the changeset for an entity. + * + * @return mixed[][] + * @psalm-return array + */ + public function & getEntityChangeSet(object $entity): array + { + $oid = spl_object_id($entity); + $data = []; + + if (! isset($this->entityChangeSets[$oid])) { + return $data; + } + + return $this->entityChangeSets[$oid]; + } + + /** + * Computes the changes that happened to a single entity. + * + * Modifies/populates the following properties: + * + * {@link _originalEntityData} + * If the entity is NEW or MANAGED but not yet fully persisted (only has an id) + * then it was not fetched from the database and therefore we have no original + * entity data yet. All of the current entity data is stored as the original entity data. + * + * {@link _entityChangeSets} + * The changes detected on all properties of the entity are stored there. + * A change is a tuple array where the first entry is the old value and the second + * entry is the new value of the property. Changesets are used by persisters + * to INSERT/UPDATE the persistent entity state. + * + * {@link _entityUpdates} + * If the entity is already fully MANAGED (has been fetched from the database before) + * and any changes to its properties are detected, then a reference to the entity is stored + * there to mark it for an update. + * + * {@link _collectionDeletions} + * If a PersistentCollection has been de-referenced in a fully MANAGED entity, + * then this collection is marked for deletion. + * + * @param ClassMetadata $class The class descriptor of the entity. + * @param object $entity The entity for which to compute the changes. + * @psalm-param ClassMetadata $class + * @psalm-param T $entity + * + * @template T of object + * + * @ignore + */ + public function computeChangeSet(ClassMetadata $class, object $entity): void + { + $oid = spl_object_id($entity); + + if (isset($this->readOnlyObjects[$oid])) { + return; + } + + if (! $class->isInheritanceTypeNone()) { + $class = $this->em->getClassMetadata($entity::class); + } + + $invoke = $this->listenersInvoker->getSubscribedSystems($class, Events::preFlush) & ~ListenersInvoker::INVOKE_MANAGER; + + if ($invoke !== ListenersInvoker::INVOKE_NONE) { + $this->listenersInvoker->invoke($class, Events::preFlush, $entity, new PreFlushEventArgs($this->em), $invoke); + } + + $actualData = []; + + foreach ($class->reflFields as $name => $refProp) { + $value = $refProp->getValue($entity); + + if ($class->isCollectionValuedAssociation($name) && $value !== null) { + if ($value instanceof PersistentCollection) { + if ($value->getOwner() === $entity) { + $actualData[$name] = $value; + continue; + } + + $value = new ArrayCollection($value->getValues()); + } + + // If $value is not a Collection then use an ArrayCollection. + if (! $value instanceof Collection) { + $value = new ArrayCollection($value); + } + + $assoc = $class->associationMappings[$name]; + assert($assoc->isToMany()); + + // Inject PersistentCollection + $value = new PersistentCollection( + $this->em, + $this->em->getClassMetadata($assoc->targetEntity), + $value, + ); + $value->setOwner($entity, $assoc); + $value->setDirty(! $value->isEmpty()); + + $refProp->setValue($entity, $value); + + $actualData[$name] = $value; + + continue; + } + + if (( ! $class->isIdentifier($name) || ! $class->isIdGeneratorIdentity()) && ($name !== $class->versionField)) { + $actualData[$name] = $value; + } + } + + if (! isset($this->originalEntityData[$oid])) { + // Entity is either NEW or MANAGED but not yet fully persisted (only has an id). + // These result in an INSERT. + $this->originalEntityData[$oid] = $actualData; + $changeSet = []; + + foreach ($actualData as $propName => $actualValue) { + if (! isset($class->associationMappings[$propName])) { + $changeSet[$propName] = [null, $actualValue]; + + continue; + } + + $assoc = $class->associationMappings[$propName]; + + if ($assoc->isToOneOwningSide()) { + $changeSet[$propName] = [null, $actualValue]; + } + } + + $this->entityChangeSets[$oid] = $changeSet; + } else { + // Entity is "fully" MANAGED: it was already fully persisted before + // and we have a copy of the original data + $originalData = $this->originalEntityData[$oid]; + $changeSet = []; + + foreach ($actualData as $propName => $actualValue) { + // skip field, its a partially omitted one! + if (! (isset($originalData[$propName]) || array_key_exists($propName, $originalData))) { + continue; + } + + $orgValue = $originalData[$propName]; + + if (! empty($class->fieldMappings[$propName]->enumType)) { + if (is_array($orgValue)) { + foreach ($orgValue as $id => $val) { + if ($val instanceof BackedEnum) { + $orgValue[$id] = $val->value; + } + } + } else { + if ($orgValue instanceof BackedEnum) { + $orgValue = $orgValue->value; + } + } + } + + // skip if value haven't changed + if ($orgValue === $actualValue) { + continue; + } + + // if regular field + if (! isset($class->associationMappings[$propName])) { + $changeSet[$propName] = [$orgValue, $actualValue]; + + continue; + } + + $assoc = $class->associationMappings[$propName]; + + // Persistent collection was exchanged with the "originally" + // created one. This can only mean it was cloned and replaced + // on another entity. + if ($actualValue instanceof PersistentCollection) { + assert($assoc->isToMany()); + $owner = $actualValue->getOwner(); + if ($owner === null) { // cloned + $actualValue->setOwner($entity, $assoc); + } elseif ($owner !== $entity) { // no clone, we have to fix + if (! $actualValue->isInitialized()) { + $actualValue->initialize(); // we have to do this otherwise the cols share state + } + + $newValue = clone $actualValue; + $newValue->setOwner($entity, $assoc); + $class->reflFields[$propName]->setValue($entity, $newValue); + } + } + + if ($orgValue instanceof PersistentCollection) { + // A PersistentCollection was de-referenced, so delete it. + $coid = spl_object_id($orgValue); + + if (isset($this->collectionDeletions[$coid])) { + continue; + } + + $this->collectionDeletions[$coid] = $orgValue; + $changeSet[$propName] = $orgValue; // Signal changeset, to-many assocs will be ignored. + + continue; + } + + if ($assoc->isToOne()) { + if ($assoc->isOwningSide()) { + $changeSet[$propName] = [$orgValue, $actualValue]; + } + + if ($orgValue !== null && $assoc->orphanRemoval) { + assert(is_object($orgValue)); + $this->scheduleOrphanRemoval($orgValue); + } + } + } + + if ($changeSet) { + $this->entityChangeSets[$oid] = $changeSet; + $this->originalEntityData[$oid] = $actualData; + $this->entityUpdates[$oid] = $entity; + } + } + + // Look for changes in associations of the entity + foreach ($class->associationMappings as $field => $assoc) { + $val = $class->reflFields[$field]->getValue($entity); + if ($val === null) { + continue; + } + + $this->computeAssociationChanges($assoc, $val); + + if ( + ! isset($this->entityChangeSets[$oid]) && + $assoc->isManyToManyOwningSide() && + $val instanceof PersistentCollection && + $val->isDirty() + ) { + $this->entityChangeSets[$oid] = []; + $this->originalEntityData[$oid] = $actualData; + $this->entityUpdates[$oid] = $entity; + } + } + } + + /** + * Computes all the changes that have been done to entities and collections + * since the last commit and stores these changes in the _entityChangeSet map + * temporarily for access by the persisters, until the UoW commit is finished. + */ + public function computeChangeSets(): void + { + // Compute changes for INSERTed entities first. This must always happen. + $this->computeScheduleInsertsChangeSets(); + + // Compute changes for other MANAGED entities. Change tracking policies take effect here. + foreach ($this->identityMap as $className => $entities) { + $class = $this->em->getClassMetadata($className); + + // Skip class if instances are read-only + if ($class->isReadOnly) { + continue; + } + + $entitiesToProcess = match (true) { + $class->isChangeTrackingDeferredImplicit() => $entities, + isset($this->scheduledForSynchronization[$className]) => $this->scheduledForSynchronization[$className], + default => [], + }; + + foreach ($entitiesToProcess as $entity) { + // Ignore uninitialized proxy objects + if ($this->isUninitializedObject($entity)) { + continue; + } + + // Only MANAGED entities that are NOT SCHEDULED FOR INSERTION OR DELETION are processed here. + $oid = spl_object_id($entity); + + if (! isset($this->entityInsertions[$oid]) && ! isset($this->entityDeletions[$oid]) && isset($this->entityStates[$oid])) { + $this->computeChangeSet($class, $entity); + } + } + } + } + + /** + * Computes the changes of an association. + * + * @param mixed $value The value of the association. + * + * @throws ORMInvalidArgumentException + * @throws ORMException + */ + private function computeAssociationChanges(AssociationMapping $assoc, mixed $value): void + { + if ($this->isUninitializedObject($value)) { + return; + } + + // If this collection is dirty, schedule it for updates + if ($value instanceof PersistentCollection && $value->isDirty()) { + $coid = spl_object_id($value); + + $this->collectionUpdates[$coid] = $value; + $this->visitedCollections[$coid] = $value; + } + + // Look through the entities, and in any of their associations, + // for transient (new) entities, recursively. ("Persistence by reachability") + // Unwrap. Uninitialized collections will simply be empty. + $unwrappedValue = $assoc->isToOne() ? [$value] : $value->unwrap(); + $targetClass = $this->em->getClassMetadata($assoc->targetEntity); + + foreach ($unwrappedValue as $key => $entry) { + if (! ($entry instanceof $targetClass->name)) { + throw ORMInvalidArgumentException::invalidAssociation($targetClass, $assoc, $entry); + } + + $state = $this->getEntityState($entry, self::STATE_NEW); + + if (! ($entry instanceof $assoc->targetEntity)) { + throw UnexpectedAssociationValue::create( + $assoc->sourceEntity, + $assoc->fieldName, + get_debug_type($entry), + $assoc->targetEntity, + ); + } + + switch ($state) { + case self::STATE_NEW: + if (! $assoc->isCascadePersist()) { + /* + * For now just record the details, because this may + * not be an issue if we later discover another pathway + * through the object-graph where cascade-persistence + * is enabled for this object. + */ + $this->nonCascadedNewDetectedEntities[spl_object_id($entry)] = [$assoc, $entry]; + + break; + } + + $this->persistNew($targetClass, $entry); + $this->computeChangeSet($targetClass, $entry); + + break; + + case self::STATE_REMOVED: + // Consume the $value as array (it's either an array or an ArrayAccess) + // and remove the element from Collection. + if (! $assoc->isToMany()) { + break; + } + + $coid = spl_object_id($value); + $this->visitedCollections[$coid] = $value; + + if (! isset($this->pendingCollectionElementRemovals[$coid])) { + $this->pendingCollectionElementRemovals[$coid] = []; + } + + $this->pendingCollectionElementRemovals[$coid][$key] = true; + break; + + case self::STATE_DETACHED: + // Can actually not happen right now as we assume STATE_NEW, + // so the exception will be raised from the DBAL layer (constraint violation). + throw ORMInvalidArgumentException::detachedEntityFoundThroughRelationship($assoc, $entry); + + default: + // MANAGED associated entities are already taken into account + // during changeset calculation anyway, since they are in the identity map. + } + } + } + + /** + * @psalm-param ClassMetadata $class + * @psalm-param T $entity + * + * @template T of object + */ + private function persistNew(ClassMetadata $class, object $entity): void + { + $oid = spl_object_id($entity); + $invoke = $this->listenersInvoker->getSubscribedSystems($class, Events::prePersist); + + if ($invoke !== ListenersInvoker::INVOKE_NONE) { + $this->listenersInvoker->invoke($class, Events::prePersist, $entity, new PrePersistEventArgs($entity, $this->em), $invoke); + } + + $idGen = $class->idGenerator; + + if (! $idGen->isPostInsertGenerator()) { + $idValue = $idGen->generateId($this->em, $entity); + + if (! $idGen instanceof AssignedGenerator) { + $idValue = [$class->getSingleIdentifierFieldName() => $this->convertSingleFieldIdentifierToPHPValue($class, $idValue)]; + + $class->setIdentifierValues($entity, $idValue); + } + + // Some identifiers may be foreign keys to new entities. + // In this case, we don't have the value yet and should treat it as if we have a post-insert generator + if (! $this->hasMissingIdsWhichAreForeignKeys($class, $idValue)) { + $this->entityIdentifiers[$oid] = $idValue; + } + } + + $this->entityStates[$oid] = self::STATE_MANAGED; + + $this->scheduleForInsert($entity); + } + + /** @param mixed[] $idValue */ + private function hasMissingIdsWhichAreForeignKeys(ClassMetadata $class, array $idValue): bool + { + foreach ($idValue as $idField => $idFieldValue) { + if ($idFieldValue === null && isset($class->associationMappings[$idField])) { + return true; + } + } + + return false; + } + + /** + * INTERNAL: + * Computes the changeset of an individual entity, independently of the + * computeChangeSets() routine that is used at the beginning of a UnitOfWork#commit(). + * + * The passed entity must be a managed entity. If the entity already has a change set + * because this method is invoked during a commit cycle then the change sets are added. + * whereby changes detected in this method prevail. + * + * @param ClassMetadata $class The class descriptor of the entity. + * @param object $entity The entity for which to (re)calculate the change set. + * @psalm-param ClassMetadata $class + * @psalm-param T $entity + * + * @throws ORMInvalidArgumentException If the passed entity is not MANAGED. + * + * @template T of object + * @ignore + */ + public function recomputeSingleEntityChangeSet(ClassMetadata $class, object $entity): void + { + $oid = spl_object_id($entity); + + if (! isset($this->entityStates[$oid]) || $this->entityStates[$oid] !== self::STATE_MANAGED) { + throw ORMInvalidArgumentException::entityNotManaged($entity); + } + + if (! $class->isInheritanceTypeNone()) { + $class = $this->em->getClassMetadata($entity::class); + } + + $actualData = []; + + foreach ($class->reflFields as $name => $refProp) { + if ( + ( ! $class->isIdentifier($name) || ! $class->isIdGeneratorIdentity()) + && ($name !== $class->versionField) + && ! $class->isCollectionValuedAssociation($name) + ) { + $actualData[$name] = $refProp->getValue($entity); + } + } + + if (! isset($this->originalEntityData[$oid])) { + throw new RuntimeException('Cannot call recomputeSingleEntityChangeSet before computeChangeSet on an entity.'); + } + + $originalData = $this->originalEntityData[$oid]; + $changeSet = []; + + foreach ($actualData as $propName => $actualValue) { + $orgValue = $originalData[$propName] ?? null; + + if (isset($class->fieldMappings[$propName]->enumType)) { + if (is_array($orgValue)) { + foreach ($orgValue as $id => $val) { + if ($val instanceof BackedEnum) { + $orgValue[$id] = $val->value; + } + } + } else { + if ($orgValue instanceof BackedEnum) { + $orgValue = $orgValue->value; + } + } + } + + if ($orgValue !== $actualValue) { + $changeSet[$propName] = [$orgValue, $actualValue]; + } + } + + if ($changeSet) { + if (isset($this->entityChangeSets[$oid])) { + $this->entityChangeSets[$oid] = [...$this->entityChangeSets[$oid], ...$changeSet]; + } elseif (! isset($this->entityInsertions[$oid])) { + $this->entityChangeSets[$oid] = $changeSet; + $this->entityUpdates[$oid] = $entity; + } + + $this->originalEntityData[$oid] = $actualData; + } + } + + /** + * Executes entity insertions + */ + private function executeInserts(): void + { + $entities = $this->computeInsertExecutionOrder(); + $eventsToDispatch = []; + + foreach ($entities as $entity) { + $oid = spl_object_id($entity); + $class = $this->em->getClassMetadata($entity::class); + $persister = $this->getEntityPersister($class->name); + + $persister->addInsert($entity); + + unset($this->entityInsertions[$oid]); + + $persister->executeInserts(); + + if (! isset($this->entityIdentifiers[$oid])) { + //entity was not added to identity map because some identifiers are foreign keys to new entities. + //add it now + $this->addToEntityIdentifiersAndEntityMap($class, $oid, $entity); + } + + $invoke = $this->listenersInvoker->getSubscribedSystems($class, Events::postPersist); + + if ($invoke !== ListenersInvoker::INVOKE_NONE) { + $eventsToDispatch[] = ['class' => $class, 'entity' => $entity, 'invoke' => $invoke]; + } + } + + // Defer dispatching `postPersist` events to until all entities have been inserted and post-insert + // IDs have been assigned. + foreach ($eventsToDispatch as $event) { + $this->listenersInvoker->invoke( + $event['class'], + Events::postPersist, + $event['entity'], + new PostPersistEventArgs($event['entity'], $this->em), + $event['invoke'], + ); + } + } + + /** + * @psalm-param ClassMetadata $class + * @psalm-param T $entity + * + * @template T of object + */ + private function addToEntityIdentifiersAndEntityMap( + ClassMetadata $class, + int $oid, + object $entity, + ): void { + $identifier = []; + + foreach ($class->getIdentifierFieldNames() as $idField) { + $origValue = $class->getFieldValue($entity, $idField); + + $value = null; + if (isset($class->associationMappings[$idField])) { + // NOTE: Single Columns as associated identifiers only allowed - this constraint it is enforced. + $value = $this->getSingleIdentifierValue($origValue); + } + + $identifier[$idField] = $value ?? $origValue; + $this->originalEntityData[$oid][$idField] = $origValue; + } + + $this->entityStates[$oid] = self::STATE_MANAGED; + $this->entityIdentifiers[$oid] = $identifier; + + $this->addToIdentityMap($entity); + } + + /** + * Executes all entity updates + */ + private function executeUpdates(): void + { + foreach ($this->entityUpdates as $oid => $entity) { + $class = $this->em->getClassMetadata($entity::class); + $persister = $this->getEntityPersister($class->name); + $preUpdateInvoke = $this->listenersInvoker->getSubscribedSystems($class, Events::preUpdate); + $postUpdateInvoke = $this->listenersInvoker->getSubscribedSystems($class, Events::postUpdate); + + if ($preUpdateInvoke !== ListenersInvoker::INVOKE_NONE) { + $this->listenersInvoker->invoke($class, Events::preUpdate, $entity, new PreUpdateEventArgs($entity, $this->em, $this->getEntityChangeSet($entity)), $preUpdateInvoke); + + $this->recomputeSingleEntityChangeSet($class, $entity); + } + + if (! empty($this->entityChangeSets[$oid])) { + $persister->update($entity); + } + + unset($this->entityUpdates[$oid]); + + if ($postUpdateInvoke !== ListenersInvoker::INVOKE_NONE) { + $this->listenersInvoker->invoke($class, Events::postUpdate, $entity, new PostUpdateEventArgs($entity, $this->em), $postUpdateInvoke); + } + } + } + + /** + * Executes all entity deletions + */ + private function executeDeletions(): void + { + $entities = $this->computeDeleteExecutionOrder(); + $eventsToDispatch = []; + + foreach ($entities as $entity) { + $this->removeFromIdentityMap($entity); + + $oid = spl_object_id($entity); + $class = $this->em->getClassMetadata($entity::class); + $persister = $this->getEntityPersister($class->name); + $invoke = $this->listenersInvoker->getSubscribedSystems($class, Events::postRemove); + + $persister->delete($entity); + + unset( + $this->entityDeletions[$oid], + $this->entityIdentifiers[$oid], + $this->originalEntityData[$oid], + $this->entityStates[$oid], + ); + + // Entity with this $oid after deletion treated as NEW, even if the $oid + // is obtained by a new entity because the old one went out of scope. + //$this->entityStates[$oid] = self::STATE_NEW; + if (! $class->isIdentifierNatural()) { + $class->reflFields[$class->identifier[0]]->setValue($entity, null); + } + + if ($invoke !== ListenersInvoker::INVOKE_NONE) { + $eventsToDispatch[] = ['class' => $class, 'entity' => $entity, 'invoke' => $invoke]; + } + } + + // Defer dispatching `postRemove` events to until all entities have been removed. + foreach ($eventsToDispatch as $event) { + $this->listenersInvoker->invoke( + $event['class'], + Events::postRemove, + $event['entity'], + new PostRemoveEventArgs($event['entity'], $this->em), + $event['invoke'], + ); + } + } + + /** @return list */ + private function computeInsertExecutionOrder(): array + { + $sort = new TopologicalSort(); + + // First make sure we have all the nodes + foreach ($this->entityInsertions as $entity) { + $sort->addNode($entity); + } + + // Now add edges + foreach ($this->entityInsertions as $entity) { + $class = $this->em->getClassMetadata($entity::class); + + foreach ($class->associationMappings as $assoc) { + // We only need to consider the owning sides of to-one associations, + // since many-to-many associations are persisted at a later step and + // have no insertion order problems (all entities already in the database + // at that time). + if (! $assoc->isToOneOwningSide()) { + continue; + } + + $targetEntity = $class->getFieldValue($entity, $assoc->fieldName); + + // If there is no entity that we need to refer to, or it is already in the + // database (i. e. does not have to be inserted), no need to consider it. + if ($targetEntity === null || ! $sort->hasNode($targetEntity)) { + continue; + } + + // An entity that references back to itself _and_ uses an application-provided ID + // (the "NONE" generator strategy) can be exempted from commit order computation. + // See https://github.com/doctrine/orm/pull/10735/ for more details on this edge case. + // A non-NULLable self-reference would be a cycle in the graph. + if ($targetEntity === $entity && $class->isIdentifierNatural()) { + continue; + } + + // According to https://www.doctrine-project.org/projects/doctrine-orm/en/2.14/reference/annotations-reference.html#annref_joincolumn, + // the default for "nullable" is true. Unfortunately, it seems this default is not applied at the metadata driver, factory or other + // level, but in fact we may have an undefined 'nullable' key here, so we must assume that default here as well. + // + // Same in \Doctrine\ORM\Tools\EntityGenerator::isAssociationIsNullable or \Doctrine\ORM\Persisters\Entity\BasicEntityPersister::getJoinSQLForJoinColumns, + // to give two examples. + $joinColumns = reset($assoc->joinColumns); + $isNullable = ! isset($joinColumns->nullable) || $joinColumns->nullable; + + // Add dependency. The dependency direction implies that "$entity depends on $targetEntity". The + // topological sort result will output the depended-upon nodes first, which means we can insert + // entities in that order. + $sort->addEdge($entity, $targetEntity, $isNullable); + } + } + + return $sort->sort(); + } + + /** @return list */ + private function computeDeleteExecutionOrder(): array + { + $stronglyConnectedComponents = new StronglyConnectedComponents(); + $sort = new TopologicalSort(); + + foreach ($this->entityDeletions as $entity) { + $stronglyConnectedComponents->addNode($entity); + $sort->addNode($entity); + } + + // First, consider only "on delete cascade" associations between entities + // and find strongly connected groups. Once we delete any one of the entities + // in such a group, _all_ of the other entities will be removed as well. So, + // we need to treat those groups like a single entity when performing delete + // order topological sorting. + foreach ($this->entityDeletions as $entity) { + $class = $this->em->getClassMetadata($entity::class); + + foreach ($class->associationMappings as $assoc) { + // We only need to consider the owning sides of to-one associations, + // since many-to-many associations can always be (and have already been) + // deleted in a preceding step. + if (! $assoc->isToOneOwningSide()) { + continue; + } + + $joinColumns = reset($assoc->joinColumns); + if (! isset($joinColumns->onDelete)) { + continue; + } + + $onDeleteOption = strtolower($joinColumns->onDelete); + if ($onDeleteOption !== 'cascade') { + continue; + } + + $targetEntity = $class->getFieldValue($entity, $assoc->fieldName); + + // If the association does not refer to another entity or that entity + // is not to be deleted, there is no ordering problem and we can + // skip this particular association. + if ($targetEntity === null || ! $stronglyConnectedComponents->hasNode($targetEntity)) { + continue; + } + + $stronglyConnectedComponents->addEdge($entity, $targetEntity); + } + } + + $stronglyConnectedComponents->findStronglyConnectedComponents(); + + // Now do the actual topological sorting to find the delete order. + foreach ($this->entityDeletions as $entity) { + $class = $this->em->getClassMetadata($entity::class); + + // Get the entities representing the SCC + $entityComponent = $stronglyConnectedComponents->getNodeRepresentingStronglyConnectedComponent($entity); + + // When $entity is part of a non-trivial strongly connected component group + // (a group containing not only those entities alone), make sure we process it _after_ the + // entity representing the group. + // The dependency direction implies that "$entity depends on $entityComponent + // being deleted first". The topological sort will output the depended-upon nodes first. + if ($entityComponent !== $entity) { + $sort->addEdge($entity, $entityComponent, false); + } + + foreach ($class->associationMappings as $assoc) { + // We only need to consider the owning sides of to-one associations, + // since many-to-many associations can always be (and have already been) + // deleted in a preceding step. + if (! $assoc->isToOneOwningSide()) { + continue; + } + + // For associations that implement a database-level set null operation, + // we do not have to follow a particular order: If the referred-to entity is + // deleted first, the DBMS will temporarily set the foreign key to NULL (SET NULL). + // So, we can skip it in the computation. + $joinColumns = reset($assoc->joinColumns); + if (isset($joinColumns->onDelete)) { + $onDeleteOption = strtolower($joinColumns->onDelete); + if ($onDeleteOption === 'set null') { + continue; + } + } + + $targetEntity = $class->getFieldValue($entity, $assoc->fieldName); + + // If the association does not refer to another entity or that entity + // is not to be deleted, there is no ordering problem and we can + // skip this particular association. + if ($targetEntity === null || ! $sort->hasNode($targetEntity)) { + continue; + } + + // Get the entities representing the SCC + $targetEntityComponent = $stronglyConnectedComponents->getNodeRepresentingStronglyConnectedComponent($targetEntity); + + // When we have a dependency between two different groups of strongly connected nodes, + // add it to the computation. + // The dependency direction implies that "$targetEntityComponent depends on $entityComponent + // being deleted first". The topological sort will output the depended-upon nodes first, + // so we can work through the result in the returned order. + if ($targetEntityComponent !== $entityComponent) { + $sort->addEdge($targetEntityComponent, $entityComponent, false); + } + } + } + + return $sort->sort(); + } + + /** + * Schedules an entity for insertion into the database. + * If the entity already has an identifier, it will be added to the identity map. + * + * @throws ORMInvalidArgumentException + * @throws InvalidArgumentException + */ + public function scheduleForInsert(object $entity): void + { + $oid = spl_object_id($entity); + + if (isset($this->entityUpdates[$oid])) { + throw new InvalidArgumentException('Dirty entity can not be scheduled for insertion.'); + } + + if (isset($this->entityDeletions[$oid])) { + throw ORMInvalidArgumentException::scheduleInsertForRemovedEntity($entity); + } + + if (isset($this->originalEntityData[$oid]) && ! isset($this->entityInsertions[$oid])) { + throw ORMInvalidArgumentException::scheduleInsertForManagedEntity($entity); + } + + if (isset($this->entityInsertions[$oid])) { + throw ORMInvalidArgumentException::scheduleInsertTwice($entity); + } + + $this->entityInsertions[$oid] = $entity; + + if (isset($this->entityIdentifiers[$oid])) { + $this->addToIdentityMap($entity); + } + } + + /** + * Checks whether an entity is scheduled for insertion. + */ + public function isScheduledForInsert(object $entity): bool + { + return isset($this->entityInsertions[spl_object_id($entity)]); + } + + /** + * Schedules an entity for being updated. + * + * @throws ORMInvalidArgumentException + */ + public function scheduleForUpdate(object $entity): void + { + $oid = spl_object_id($entity); + + if (! isset($this->entityIdentifiers[$oid])) { + throw ORMInvalidArgumentException::entityHasNoIdentity($entity, 'scheduling for update'); + } + + if (isset($this->entityDeletions[$oid])) { + throw ORMInvalidArgumentException::entityIsRemoved($entity, 'schedule for update'); + } + + if (! isset($this->entityUpdates[$oid]) && ! isset($this->entityInsertions[$oid])) { + $this->entityUpdates[$oid] = $entity; + } + } + + /** + * INTERNAL: + * Schedules an extra update that will be executed immediately after the + * regular entity updates within the currently running commit cycle. + * + * Extra updates for entities are stored as (entity, changeset) tuples. + * + * @psalm-param array $changeset The changeset of the entity (what to update). + * + * @ignore + */ + public function scheduleExtraUpdate(object $entity, array $changeset): void + { + $oid = spl_object_id($entity); + $extraUpdate = [$entity, $changeset]; + + if (isset($this->extraUpdates[$oid])) { + [, $changeset2] = $this->extraUpdates[$oid]; + + $extraUpdate = [$entity, $changeset + $changeset2]; + } + + $this->extraUpdates[$oid] = $extraUpdate; + } + + /** + * Checks whether an entity is registered as dirty in the unit of work. + * Note: Is not very useful currently as dirty entities are only registered + * at commit time. + */ + public function isScheduledForUpdate(object $entity): bool + { + return isset($this->entityUpdates[spl_object_id($entity)]); + } + + /** + * Checks whether an entity is registered to be checked in the unit of work. + */ + public function isScheduledForDirtyCheck(object $entity): bool + { + $rootEntityName = $this->em->getClassMetadata($entity::class)->rootEntityName; + + return isset($this->scheduledForSynchronization[$rootEntityName][spl_object_id($entity)]); + } + + /** + * INTERNAL: + * Schedules an entity for deletion. + */ + public function scheduleForDelete(object $entity): void + { + $oid = spl_object_id($entity); + + if (isset($this->entityInsertions[$oid])) { + if ($this->isInIdentityMap($entity)) { + $this->removeFromIdentityMap($entity); + } + + unset($this->entityInsertions[$oid], $this->entityStates[$oid]); + + return; // entity has not been persisted yet, so nothing more to do. + } + + if (! $this->isInIdentityMap($entity)) { + return; + } + + unset($this->entityUpdates[$oid]); + + if (! isset($this->entityDeletions[$oid])) { + $this->entityDeletions[$oid] = $entity; + $this->entityStates[$oid] = self::STATE_REMOVED; + } + } + + /** + * Checks whether an entity is registered as removed/deleted with the unit + * of work. + */ + public function isScheduledForDelete(object $entity): bool + { + return isset($this->entityDeletions[spl_object_id($entity)]); + } + + /** + * Checks whether an entity is scheduled for insertion, update or deletion. + */ + public function isEntityScheduled(object $entity): bool + { + $oid = spl_object_id($entity); + + return isset($this->entityInsertions[$oid]) + || isset($this->entityUpdates[$oid]) + || isset($this->entityDeletions[$oid]); + } + + /** + * INTERNAL: + * Registers an entity in the identity map. + * Note that entities in a hierarchy are registered with the class name of + * the root entity. + * + * @return bool TRUE if the registration was successful, FALSE if the identity of + * the entity in question is already managed. + * + * @throws ORMInvalidArgumentException + * @throws EntityIdentityCollisionException + * + * @ignore + */ + public function addToIdentityMap(object $entity): bool + { + $classMetadata = $this->em->getClassMetadata($entity::class); + $idHash = $this->getIdHashByEntity($entity); + $className = $classMetadata->rootEntityName; + + if (isset($this->identityMap[$className][$idHash])) { + if ($this->identityMap[$className][$idHash] !== $entity) { + throw EntityIdentityCollisionException::create($this->identityMap[$className][$idHash], $entity, $idHash); + } + + return false; + } + + $this->identityMap[$className][$idHash] = $entity; + + return true; + } + + /** + * Gets the id hash of an entity by its identifier. + * + * @param array $identifier The identifier of an entity + * + * @return string The entity id hash. + */ + final public static function getIdHashByIdentifier(array $identifier): string + { + foreach ($identifier as $k => $value) { + if ($value instanceof BackedEnum) { + $identifier[$k] = $value->value; + } + } + + return implode( + ' ', + $identifier, + ); + } + + /** + * Gets the id hash of an entity. + * + * @param object $entity The entity managed by Unit Of Work + * + * @return string The entity id hash. + */ + public function getIdHashByEntity(object $entity): string + { + $identifier = $this->entityIdentifiers[spl_object_id($entity)]; + + if (empty($identifier) || in_array(null, $identifier, true)) { + $classMetadata = $this->em->getClassMetadata($entity::class); + + throw ORMInvalidArgumentException::entityWithoutIdentity($classMetadata->name, $entity); + } + + return self::getIdHashByIdentifier($identifier); + } + + /** + * Gets the state of an entity with regard to the current unit of work. + * + * @param int|null $assume The state to assume if the state is not yet known (not MANAGED or REMOVED). + * This parameter can be set to improve performance of entity state detection + * by potentially avoiding a database lookup if the distinction between NEW and DETACHED + * is either known or does not matter for the caller of the method. + * @psalm-param self::STATE_*|null $assume + * + * @psalm-return self::STATE_* + */ + public function getEntityState(object $entity, int|null $assume = null): int + { + $oid = spl_object_id($entity); + + if (isset($this->entityStates[$oid])) { + return $this->entityStates[$oid]; + } + + if ($assume !== null) { + return $assume; + } + + // State can only be NEW or DETACHED, because MANAGED/REMOVED states are known. + // Note that you can not remember the NEW or DETACHED state in _entityStates since + // the UoW does not hold references to such objects and the object hash can be reused. + // More generally because the state may "change" between NEW/DETACHED without the UoW being aware of it. + $class = $this->em->getClassMetadata($entity::class); + $id = $class->getIdentifierValues($entity); + + if (! $id) { + return self::STATE_NEW; + } + + if ($class->containsForeignIdentifier || $class->containsEnumIdentifier) { + $id = $this->identifierFlattener->flattenIdentifier($class, $id); + } + + switch (true) { + case $class->isIdentifierNatural(): + // Check for a version field, if available, to avoid a db lookup. + if ($class->isVersioned) { + assert($class->versionField !== null); + + return $class->getFieldValue($entity, $class->versionField) + ? self::STATE_DETACHED + : self::STATE_NEW; + } + + // Last try before db lookup: check the identity map. + if ($this->tryGetById($id, $class->rootEntityName)) { + return self::STATE_DETACHED; + } + + // db lookup + if ($this->getEntityPersister($class->name)->exists($entity)) { + return self::STATE_DETACHED; + } + + return self::STATE_NEW; + + case ! $class->idGenerator->isPostInsertGenerator(): + // if we have a pre insert generator we can't be sure that having an id + // really means that the entity exists. We have to verify this through + // the last resort: a db lookup + + // Last try before db lookup: check the identity map. + if ($this->tryGetById($id, $class->rootEntityName)) { + return self::STATE_DETACHED; + } + + // db lookup + if ($this->getEntityPersister($class->name)->exists($entity)) { + return self::STATE_DETACHED; + } + + return self::STATE_NEW; + + default: + return self::STATE_DETACHED; + } + } + + /** + * INTERNAL: + * Removes an entity from the identity map. This effectively detaches the + * entity from the persistence management of Doctrine. + * + * @throws ORMInvalidArgumentException + * + * @ignore + */ + public function removeFromIdentityMap(object $entity): bool + { + $oid = spl_object_id($entity); + $classMetadata = $this->em->getClassMetadata($entity::class); + $idHash = self::getIdHashByIdentifier($this->entityIdentifiers[$oid]); + + if ($idHash === '') { + throw ORMInvalidArgumentException::entityHasNoIdentity($entity, 'remove from identity map'); + } + + $className = $classMetadata->rootEntityName; + + if (isset($this->identityMap[$className][$idHash])) { + unset($this->identityMap[$className][$idHash], $this->readOnlyObjects[$oid]); + + //$this->entityStates[$oid] = self::STATE_DETACHED; + + return true; + } + + return false; + } + + /** + * INTERNAL: + * Gets an entity in the identity map by its identifier hash. + * + * @ignore + */ + public function getByIdHash(string $idHash, string $rootClassName): object|null + { + return $this->identityMap[$rootClassName][$idHash]; + } + + /** + * INTERNAL: + * Tries to get an entity by its identifier hash. If no entity is found for + * the given hash, FALSE is returned. + * + * @param mixed $idHash (must be possible to cast it to string) + * + * @return false|object The found entity or FALSE. + * + * @ignore + */ + public function tryGetByIdHash(mixed $idHash, string $rootClassName): object|false + { + $stringIdHash = (string) $idHash; + + return $this->identityMap[$rootClassName][$stringIdHash] ?? false; + } + + /** + * Checks whether an entity is registered in the identity map of this UnitOfWork. + */ + public function isInIdentityMap(object $entity): bool + { + $oid = spl_object_id($entity); + + if (empty($this->entityIdentifiers[$oid])) { + return false; + } + + $classMetadata = $this->em->getClassMetadata($entity::class); + $idHash = self::getIdHashByIdentifier($this->entityIdentifiers[$oid]); + + return isset($this->identityMap[$classMetadata->rootEntityName][$idHash]); + } + + /** + * Persists an entity as part of the current unit of work. + */ + public function persist(object $entity): void + { + $visited = []; + + $this->doPersist($entity, $visited); + } + + /** + * Persists an entity as part of the current unit of work. + * + * This method is internally called during persist() cascades as it tracks + * the already visited entities to prevent infinite recursions. + * + * @psalm-param array $visited The already visited entities. + * + * @throws ORMInvalidArgumentException + * @throws UnexpectedValueException + */ + private function doPersist(object $entity, array &$visited): void + { + $oid = spl_object_id($entity); + + if (isset($visited[$oid])) { + return; // Prevent infinite recursion + } + + $visited[$oid] = $entity; // Mark visited + + $class = $this->em->getClassMetadata($entity::class); + + // We assume NEW, so DETACHED entities result in an exception on flush (constraint violation). + // If we would detect DETACHED here we would throw an exception anyway with the same + // consequences (not recoverable/programming error), so just assuming NEW here + // lets us avoid some database lookups for entities with natural identifiers. + $entityState = $this->getEntityState($entity, self::STATE_NEW); + + switch ($entityState) { + case self::STATE_MANAGED: + // Nothing to do, except if policy is "deferred explicit" + if ($class->isChangeTrackingDeferredExplicit()) { + $this->scheduleForDirtyCheck($entity); + } + + break; + + case self::STATE_NEW: + $this->persistNew($class, $entity); + break; + + case self::STATE_REMOVED: + // Entity becomes managed again + unset($this->entityDeletions[$oid]); + $this->addToIdentityMap($entity); + + $this->entityStates[$oid] = self::STATE_MANAGED; + + if ($class->isChangeTrackingDeferredExplicit()) { + $this->scheduleForDirtyCheck($entity); + } + + break; + + case self::STATE_DETACHED: + // Can actually not happen right now since we assume STATE_NEW. + throw ORMInvalidArgumentException::detachedEntityCannot($entity, 'persisted'); + + default: + throw new UnexpectedValueException(sprintf( + 'Unexpected entity state: %s. %s', + $entityState, + self::objToStr($entity), + )); + } + + $this->cascadePersist($entity, $visited); + } + + /** + * Deletes an entity as part of the current unit of work. + */ + public function remove(object $entity): void + { + $visited = []; + + $this->doRemove($entity, $visited); + } + + /** + * Deletes an entity as part of the current unit of work. + * + * This method is internally called during delete() cascades as it tracks + * the already visited entities to prevent infinite recursions. + * + * @psalm-param array $visited The map of the already visited entities. + * + * @throws ORMInvalidArgumentException If the instance is a detached entity. + * @throws UnexpectedValueException + */ + private function doRemove(object $entity, array &$visited): void + { + $oid = spl_object_id($entity); + + if (isset($visited[$oid])) { + return; // Prevent infinite recursion + } + + $visited[$oid] = $entity; // mark visited + + // Cascade first, because scheduleForDelete() removes the entity from the identity map, which + // can cause problems when a lazy proxy has to be initialized for the cascade operation. + $this->cascadeRemove($entity, $visited); + + $class = $this->em->getClassMetadata($entity::class); + $entityState = $this->getEntityState($entity); + + switch ($entityState) { + case self::STATE_NEW: + case self::STATE_REMOVED: + // nothing to do + break; + + case self::STATE_MANAGED: + $invoke = $this->listenersInvoker->getSubscribedSystems($class, Events::preRemove); + + if ($invoke !== ListenersInvoker::INVOKE_NONE) { + $this->listenersInvoker->invoke($class, Events::preRemove, $entity, new PreRemoveEventArgs($entity, $this->em), $invoke); + } + + $this->scheduleForDelete($entity); + break; + + case self::STATE_DETACHED: + throw ORMInvalidArgumentException::detachedEntityCannot($entity, 'removed'); + + default: + throw new UnexpectedValueException(sprintf( + 'Unexpected entity state: %s. %s', + $entityState, + self::objToStr($entity), + )); + } + } + + /** + * Detaches an entity from the persistence management. It's persistence will + * no longer be managed by Doctrine. + */ + public function detach(object $entity): void + { + $visited = []; + + $this->doDetach($entity, $visited); + } + + /** + * Executes a detach operation on the given entity. + * + * @param mixed[] $visited + * @param bool $noCascade if true, don't cascade detach operation. + */ + private function doDetach( + object $entity, + array &$visited, + bool $noCascade = false, + ): void { + $oid = spl_object_id($entity); + + if (isset($visited[$oid])) { + return; // Prevent infinite recursion + } + + $visited[$oid] = $entity; // mark visited + + switch ($this->getEntityState($entity, self::STATE_DETACHED)) { + case self::STATE_MANAGED: + if ($this->isInIdentityMap($entity)) { + $this->removeFromIdentityMap($entity); + } + + unset( + $this->entityInsertions[$oid], + $this->entityUpdates[$oid], + $this->entityDeletions[$oid], + $this->entityIdentifiers[$oid], + $this->entityStates[$oid], + $this->originalEntityData[$oid], + ); + break; + case self::STATE_NEW: + case self::STATE_DETACHED: + return; + } + + if (! $noCascade) { + $this->cascadeDetach($entity, $visited); + } + } + + /** + * Refreshes the state of the given entity from the database, overwriting + * any local, unpersisted changes. + * + * @psalm-param LockMode::*|null $lockMode + * + * @throws InvalidArgumentException If the entity is not MANAGED. + * @throws TransactionRequiredException + */ + public function refresh(object $entity, LockMode|int|null $lockMode = null): void + { + $visited = []; + + $this->doRefresh($entity, $visited, $lockMode); + } + + /** + * Executes a refresh operation on an entity. + * + * @psalm-param array $visited The already visited entities during cascades. + * @psalm-param LockMode::*|null $lockMode + * + * @throws ORMInvalidArgumentException If the entity is not MANAGED. + * @throws TransactionRequiredException + */ + private function doRefresh(object $entity, array &$visited, LockMode|int|null $lockMode = null): void + { + switch (true) { + case $lockMode === LockMode::PESSIMISTIC_READ: + case $lockMode === LockMode::PESSIMISTIC_WRITE: + if (! $this->em->getConnection()->isTransactionActive()) { + throw TransactionRequiredException::transactionRequired(); + } + } + + $oid = spl_object_id($entity); + + if (isset($visited[$oid])) { + return; // Prevent infinite recursion + } + + $visited[$oid] = $entity; // mark visited + + $class = $this->em->getClassMetadata($entity::class); + + if ($this->getEntityState($entity) !== self::STATE_MANAGED) { + throw ORMInvalidArgumentException::entityNotManaged($entity); + } + + $this->getEntityPersister($class->name)->refresh( + array_combine($class->getIdentifierFieldNames(), $this->entityIdentifiers[$oid]), + $entity, + $lockMode, + ); + + $this->cascadeRefresh($entity, $visited, $lockMode); + } + + /** + * Cascades a refresh operation to associated entities. + * + * @psalm-param array $visited + * @psalm-param LockMode::*|null $lockMode + */ + private function cascadeRefresh(object $entity, array &$visited, LockMode|int|null $lockMode = null): void + { + $class = $this->em->getClassMetadata($entity::class); + + $associationMappings = array_filter( + $class->associationMappings, + static fn (AssociationMapping $assoc): bool => $assoc->isCascadeRefresh() + ); + + foreach ($associationMappings as $assoc) { + $relatedEntities = $class->reflFields[$assoc->fieldName]->getValue($entity); + + switch (true) { + case $relatedEntities instanceof PersistentCollection: + // Unwrap so that foreach() does not initialize + $relatedEntities = $relatedEntities->unwrap(); + // break; is commented intentionally! + + case $relatedEntities instanceof Collection: + case is_array($relatedEntities): + foreach ($relatedEntities as $relatedEntity) { + $this->doRefresh($relatedEntity, $visited, $lockMode); + } + + break; + + case $relatedEntities !== null: + $this->doRefresh($relatedEntities, $visited, $lockMode); + break; + + default: + // Do nothing + } + } + } + + /** + * Cascades a detach operation to associated entities. + * + * @param array $visited + */ + private function cascadeDetach(object $entity, array &$visited): void + { + $class = $this->em->getClassMetadata($entity::class); + + $associationMappings = array_filter( + $class->associationMappings, + static fn (AssociationMapping $assoc): bool => $assoc->isCascadeDetach() + ); + + foreach ($associationMappings as $assoc) { + $relatedEntities = $class->reflFields[$assoc->fieldName]->getValue($entity); + + switch (true) { + case $relatedEntities instanceof PersistentCollection: + // Unwrap so that foreach() does not initialize + $relatedEntities = $relatedEntities->unwrap(); + // break; is commented intentionally! + + case $relatedEntities instanceof Collection: + case is_array($relatedEntities): + foreach ($relatedEntities as $relatedEntity) { + $this->doDetach($relatedEntity, $visited); + } + + break; + + case $relatedEntities !== null: + $this->doDetach($relatedEntities, $visited); + break; + + default: + // Do nothing + } + } + } + + /** + * Cascades the save operation to associated entities. + * + * @psalm-param array $visited + */ + private function cascadePersist(object $entity, array &$visited): void + { + if ($this->isUninitializedObject($entity)) { + // nothing to do - proxy is not initialized, therefore we don't do anything with it + return; + } + + $class = $this->em->getClassMetadata($entity::class); + + $associationMappings = array_filter( + $class->associationMappings, + static fn (AssociationMapping $assoc): bool => $assoc->isCascadePersist() + ); + + foreach ($associationMappings as $assoc) { + $relatedEntities = $class->reflFields[$assoc->fieldName]->getValue($entity); + + switch (true) { + case $relatedEntities instanceof PersistentCollection: + // Unwrap so that foreach() does not initialize + $relatedEntities = $relatedEntities->unwrap(); + // break; is commented intentionally! + + case $relatedEntities instanceof Collection: + case is_array($relatedEntities): + if ($assoc->isToMany() <= 0) { + throw ORMInvalidArgumentException::invalidAssociation( + $this->em->getClassMetadata($assoc->targetEntity), + $assoc, + $relatedEntities, + ); + } + + foreach ($relatedEntities as $relatedEntity) { + $this->doPersist($relatedEntity, $visited); + } + + break; + + case $relatedEntities !== null: + if (! $relatedEntities instanceof $assoc->targetEntity) { + throw ORMInvalidArgumentException::invalidAssociation( + $this->em->getClassMetadata($assoc->targetEntity), + $assoc, + $relatedEntities, + ); + } + + $this->doPersist($relatedEntities, $visited); + break; + + default: + // Do nothing + } + } + } + + /** + * Cascades the delete operation to associated entities. + * + * @psalm-param array $visited + */ + private function cascadeRemove(object $entity, array &$visited): void + { + $class = $this->em->getClassMetadata($entity::class); + + $associationMappings = array_filter( + $class->associationMappings, + static fn (AssociationMapping $assoc): bool => $assoc->isCascadeRemove() + ); + + if ($associationMappings) { + $this->initializeObject($entity); + } + + $entitiesToCascade = []; + + foreach ($associationMappings as $assoc) { + $relatedEntities = $class->reflFields[$assoc->fieldName]->getValue($entity); + + switch (true) { + case $relatedEntities instanceof Collection: + case is_array($relatedEntities): + // If its a PersistentCollection initialization is intended! No unwrap! + foreach ($relatedEntities as $relatedEntity) { + $entitiesToCascade[] = $relatedEntity; + } + + break; + + case $relatedEntities !== null: + $entitiesToCascade[] = $relatedEntities; + break; + + default: + // Do nothing + } + } + + foreach ($entitiesToCascade as $relatedEntity) { + $this->doRemove($relatedEntity, $visited); + } + } + + /** + * Acquire a lock on the given entity. + * + * @psalm-param LockMode::* $lockMode + * + * @throws ORMInvalidArgumentException + * @throws TransactionRequiredException + * @throws OptimisticLockException + */ + public function lock(object $entity, LockMode|int $lockMode, DateTimeInterface|int|null $lockVersion = null): void + { + if ($this->getEntityState($entity, self::STATE_DETACHED) !== self::STATE_MANAGED) { + throw ORMInvalidArgumentException::entityNotManaged($entity); + } + + $class = $this->em->getClassMetadata($entity::class); + + switch (true) { + case $lockMode === LockMode::OPTIMISTIC: + if (! $class->isVersioned) { + throw OptimisticLockException::notVersioned($class->name); + } + + if ($lockVersion === null) { + return; + } + + $this->initializeObject($entity); + + assert($class->versionField !== null); + $entityVersion = $class->reflFields[$class->versionField]->getValue($entity); + + // phpcs:ignore SlevomatCodingStandard.Operators.DisallowEqualOperators.DisallowedNotEqualOperator + if ($entityVersion != $lockVersion) { + throw OptimisticLockException::lockFailedVersionMismatch($entity, $lockVersion, $entityVersion); + } + + break; + + case $lockMode === LockMode::NONE: + case $lockMode === LockMode::PESSIMISTIC_READ: + case $lockMode === LockMode::PESSIMISTIC_WRITE: + if (! $this->em->getConnection()->isTransactionActive()) { + throw TransactionRequiredException::transactionRequired(); + } + + $oid = spl_object_id($entity); + + $this->getEntityPersister($class->name)->lock( + array_combine($class->getIdentifierFieldNames(), $this->entityIdentifiers[$oid]), + $lockMode, + ); + break; + + default: + // Do nothing + } + } + + /** + * Clears the UnitOfWork. + */ + public function clear(): void + { + $this->identityMap = + $this->entityIdentifiers = + $this->originalEntityData = + $this->entityChangeSets = + $this->entityStates = + $this->scheduledForSynchronization = + $this->entityInsertions = + $this->entityUpdates = + $this->entityDeletions = + $this->nonCascadedNewDetectedEntities = + $this->collectionDeletions = + $this->collectionUpdates = + $this->extraUpdates = + $this->readOnlyObjects = + $this->pendingCollectionElementRemovals = + $this->visitedCollections = + $this->eagerLoadingEntities = + $this->eagerLoadingCollections = + $this->orphanRemovals = []; + + if ($this->evm->hasListeners(Events::onClear)) { + $this->evm->dispatchEvent(Events::onClear, new OnClearEventArgs($this->em)); + } + } + + /** + * INTERNAL: + * Schedules an orphaned entity for removal. The remove() operation will be + * invoked on that entity at the beginning of the next commit of this + * UnitOfWork. + * + * @ignore + */ + public function scheduleOrphanRemoval(object $entity): void + { + $this->orphanRemovals[spl_object_id($entity)] = $entity; + } + + /** + * INTERNAL: + * Cancels a previously scheduled orphan removal. + * + * @ignore + */ + public function cancelOrphanRemoval(object $entity): void + { + unset($this->orphanRemovals[spl_object_id($entity)]); + } + + /** + * INTERNAL: + * Schedules a complete collection for removal when this UnitOfWork commits. + */ + public function scheduleCollectionDeletion(PersistentCollection $coll): void + { + $coid = spl_object_id($coll); + + // TODO: if $coll is already scheduled for recreation ... what to do? + // Just remove $coll from the scheduled recreations? + unset($this->collectionUpdates[$coid]); + + $this->collectionDeletions[$coid] = $coll; + } + + public function isCollectionScheduledForDeletion(PersistentCollection $coll): bool + { + return isset($this->collectionDeletions[spl_object_id($coll)]); + } + + /** + * INTERNAL: + * Creates an entity. Used for reconstitution of persistent entities. + * + * Internal note: Highly performance-sensitive method. + * + * @param string $className The name of the entity class. + * @param mixed[] $data The data for the entity. + * @param mixed[] $hints Any hints to account for during reconstitution/lookup of the entity. + * @psalm-param class-string $className + * @psalm-param array $hints + * + * @return object The managed entity instance. + * + * @ignore + * @todo Rename: getOrCreateEntity + */ + public function createEntity(string $className, array $data, array &$hints = []): object + { + $class = $this->em->getClassMetadata($className); + + $id = $this->identifierFlattener->flattenIdentifier($class, $data); + $idHash = self::getIdHashByIdentifier($id); + + if (isset($this->identityMap[$class->rootEntityName][$idHash])) { + $entity = $this->identityMap[$class->rootEntityName][$idHash]; + $oid = spl_object_id($entity); + + if ( + isset($hints[Query::HINT_REFRESH], $hints[Query::HINT_REFRESH_ENTITY]) + ) { + $unmanagedProxy = $hints[Query::HINT_REFRESH_ENTITY]; + if ( + $unmanagedProxy !== $entity + && $this->isIdentifierEquals($unmanagedProxy, $entity) + ) { + // We will hydrate the given un-managed proxy anyway: + // continue work, but consider it the entity from now on + $entity = $unmanagedProxy; + } + } + + if ($this->isUninitializedObject($entity)) { + $entity->__setInitialized(true); + } else { + if ( + ! isset($hints[Query::HINT_REFRESH]) + || (isset($hints[Query::HINT_REFRESH_ENTITY]) && $hints[Query::HINT_REFRESH_ENTITY] !== $entity) + ) { + return $entity; + } + } + + $this->originalEntityData[$oid] = $data; + } else { + $entity = $class->newInstance(); + $oid = spl_object_id($entity); + $this->registerManaged($entity, $id, $data); + + if (isset($hints[Query::HINT_READ_ONLY])) { + $this->readOnlyObjects[$oid] = true; + } + } + + foreach ($data as $field => $value) { + if (isset($class->fieldMappings[$field])) { + $class->reflFields[$field]->setValue($entity, $value); + } + } + + // Loading the entity right here, if its in the eager loading map get rid of it there. + unset($this->eagerLoadingEntities[$class->rootEntityName][$idHash]); + + if (isset($this->eagerLoadingEntities[$class->rootEntityName]) && ! $this->eagerLoadingEntities[$class->rootEntityName]) { + unset($this->eagerLoadingEntities[$class->rootEntityName]); + } + + foreach ($class->associationMappings as $field => $assoc) { + // Check if the association is not among the fetch-joined associations already. + if (isset($hints['fetchAlias'], $hints['fetched'][$hints['fetchAlias']][$field])) { + continue; + } + + if (! isset($hints['fetchMode'][$class->name][$field])) { + $hints['fetchMode'][$class->name][$field] = $assoc->fetch; + } + + $targetClass = $this->em->getClassMetadata($assoc->targetEntity); + + switch (true) { + case $assoc->isToOne(): + if (! $assoc->isOwningSide()) { + // use the given entity association + if (isset($data[$field]) && is_object($data[$field]) && isset($this->entityStates[spl_object_id($data[$field])])) { + $this->originalEntityData[$oid][$field] = $data[$field]; + + $class->reflFields[$field]->setValue($entity, $data[$field]); + $targetClass->reflFields[$assoc->mappedBy]->setValue($data[$field], $entity); + + continue 2; + } + + // Inverse side of x-to-one can never be lazy + $class->reflFields[$field]->setValue($entity, $this->getEntityPersister($assoc->targetEntity)->loadOneToOneEntity($assoc, $entity)); + + continue 2; + } + + // use the entity association + if (isset($data[$field]) && is_object($data[$field]) && isset($this->entityStates[spl_object_id($data[$field])])) { + $class->reflFields[$field]->setValue($entity, $data[$field]); + $this->originalEntityData[$oid][$field] = $data[$field]; + + break; + } + + $associatedId = []; + + assert($assoc->isToOneOwningSide()); + // TODO: Is this even computed right in all cases of composite keys? + foreach ($assoc->targetToSourceKeyColumns as $targetColumn => $srcColumn) { + $joinColumnValue = $data[$srcColumn] ?? null; + + if ($joinColumnValue !== null) { + if ($joinColumnValue instanceof BackedEnum) { + $joinColumnValue = $joinColumnValue->value; + } + + if ($targetClass->containsForeignIdentifier) { + $associatedId[$targetClass->getFieldForColumn($targetColumn)] = $joinColumnValue; + } else { + $associatedId[$targetClass->fieldNames[$targetColumn]] = $joinColumnValue; + } + } elseif (in_array($targetClass->getFieldForColumn($targetColumn), $targetClass->identifier, true)) { + // the missing key is part of target's entity primary key + $associatedId = []; + break; + } + } + + if (! $associatedId) { + // Foreign key is NULL + $class->reflFields[$field]->setValue($entity, null); + $this->originalEntityData[$oid][$field] = null; + + break; + } + + // Foreign key is set + // Check identity map first + // FIXME: Can break easily with composite keys if join column values are in + // wrong order. The correct order is the one in ClassMetadata#identifier. + $relatedIdHash = self::getIdHashByIdentifier($associatedId); + + switch (true) { + case isset($this->identityMap[$targetClass->rootEntityName][$relatedIdHash]): + $newValue = $this->identityMap[$targetClass->rootEntityName][$relatedIdHash]; + + // If this is an uninitialized proxy, we are deferring eager loads, + // this association is marked as eager fetch, and its an uninitialized proxy (wtf!) + // then we can append this entity for eager loading! + if ( + $hints['fetchMode'][$class->name][$field] === ClassMetadata::FETCH_EAGER && + isset($hints[self::HINT_DEFEREAGERLOAD]) && + ! $targetClass->isIdentifierComposite && + $this->isUninitializedObject($newValue) + ) { + $this->eagerLoadingEntities[$targetClass->rootEntityName][$relatedIdHash] = current($associatedId); + } + + break; + + case $targetClass->subClasses: + // If it might be a subtype, it can not be lazy. There isn't even + // a way to solve this with deferred eager loading, which means putting + // an entity with subclasses at a *-to-one location is really bad! (performance-wise) + $newValue = $this->getEntityPersister($assoc->targetEntity)->loadOneToOneEntity($assoc, $entity, $associatedId); + break; + + default: + $normalizedAssociatedId = $this->normalizeIdentifier($targetClass, $associatedId); + + switch (true) { + // We are negating the condition here. Other cases will assume it is valid! + case $hints['fetchMode'][$class->name][$field] !== ClassMetadata::FETCH_EAGER: + $newValue = $this->em->getProxyFactory()->getProxy($assoc->targetEntity, $normalizedAssociatedId); + $this->registerManaged($newValue, $associatedId, []); + break; + + // Deferred eager load only works for single identifier classes + case isset($hints[self::HINT_DEFEREAGERLOAD]) && + $hints[self::HINT_DEFEREAGERLOAD] && + ! $targetClass->isIdentifierComposite: + // TODO: Is there a faster approach? + $this->eagerLoadingEntities[$targetClass->rootEntityName][$relatedIdHash] = current($normalizedAssociatedId); + + $newValue = $this->em->getProxyFactory()->getProxy($assoc->targetEntity, $normalizedAssociatedId); + $this->registerManaged($newValue, $associatedId, []); + break; + + default: + // TODO: This is very imperformant, ignore it? + $newValue = $this->em->find($assoc->targetEntity, $normalizedAssociatedId); + break; + } + } + + $this->originalEntityData[$oid][$field] = $newValue; + $class->reflFields[$field]->setValue($entity, $newValue); + + if ($assoc->inversedBy !== null && $assoc->isOneToOne() && $newValue !== null) { + $inverseAssoc = $targetClass->associationMappings[$assoc->inversedBy]; + $targetClass->reflFields[$inverseAssoc->fieldName]->setValue($newValue, $entity); + } + + break; + + default: + assert($assoc->isToMany()); + // Ignore if its a cached collection + if (isset($hints[Query::HINT_CACHE_ENABLED]) && $class->getFieldValue($entity, $field) instanceof PersistentCollection) { + break; + } + + // use the given collection + if (isset($data[$field]) && $data[$field] instanceof PersistentCollection) { + $data[$field]->setOwner($entity, $assoc); + + $class->reflFields[$field]->setValue($entity, $data[$field]); + $this->originalEntityData[$oid][$field] = $data[$field]; + + break; + } + + // Inject collection + $pColl = new PersistentCollection($this->em, $targetClass, new ArrayCollection()); + $pColl->setOwner($entity, $assoc); + $pColl->setInitialized(false); + + $reflField = $class->reflFields[$field]; + $reflField->setValue($entity, $pColl); + + if ($hints['fetchMode'][$class->name][$field] === ClassMetadata::FETCH_EAGER) { + $isIteration = isset($hints[Query::HINT_INTERNAL_ITERATION]) && $hints[Query::HINT_INTERNAL_ITERATION]; + if (! $isIteration && $assoc->isOneToMany() && ! $targetClass->isIdentifierComposite && ! $assoc->isIndexed()) { + $this->scheduleCollectionForBatchLoading($pColl, $class); + } else { + $this->loadCollection($pColl); + $pColl->takeSnapshot(); + } + } + + $this->originalEntityData[$oid][$field] = $pColl; + break; + } + } + + // defer invoking of postLoad event to hydration complete step + $this->hydrationCompleteHandler->deferPostLoadInvoking($class, $entity); + + return $entity; + } + + public function triggerEagerLoads(): void + { + if (! $this->eagerLoadingEntities && ! $this->eagerLoadingCollections) { + return; + } + + // avoid infinite recursion + $eagerLoadingEntities = $this->eagerLoadingEntities; + $this->eagerLoadingEntities = []; + + foreach ($eagerLoadingEntities as $entityName => $ids) { + if (! $ids) { + continue; + } + + $class = $this->em->getClassMetadata($entityName); + $batches = array_chunk($ids, $this->em->getConfiguration()->getEagerFetchBatchSize()); + + foreach ($batches as $batchedIds) { + $this->getEntityPersister($entityName)->loadAll( + array_combine($class->identifier, [$batchedIds]), + ); + } + } + + $eagerLoadingCollections = $this->eagerLoadingCollections; // avoid recursion + $this->eagerLoadingCollections = []; + + foreach ($eagerLoadingCollections as $group) { + $this->eagerLoadCollections($group['items'], $group['mapping']); + } + } + + /** + * Load all data into the given collections, according to the specified mapping + * + * @param PersistentCollection[] $collections + */ + private function eagerLoadCollections(array $collections, ToManyInverseSideMapping $mapping): void + { + $targetEntity = $mapping->targetEntity; + $class = $this->em->getClassMetadata($mapping->sourceEntity); + $mappedBy = $mapping->mappedBy; + + $batches = array_chunk($collections, $this->em->getConfiguration()->getEagerFetchBatchSize(), true); + + foreach ($batches as $collectionBatch) { + $entities = []; + + foreach ($collectionBatch as $collection) { + $entities[] = $collection->getOwner(); + } + + $found = $this->getEntityPersister($targetEntity)->loadAll([$mappedBy => $entities], $mapping->orderBy); + + $targetClass = $this->em->getClassMetadata($targetEntity); + $targetProperty = $targetClass->getReflectionProperty($mappedBy); + assert($targetProperty !== null); + + foreach ($found as $targetValue) { + $sourceEntity = $targetProperty->getValue($targetValue); + + if ($sourceEntity === null && isset($targetClass->associationMappings[$mappedBy]->joinColumns)) { + // case where the hydration $targetValue itself has not yet fully completed, for example + // in case a bi-directional association is being hydrated and deferring eager loading is + // not possible due to subclassing. + $data = $this->getOriginalEntityData($targetValue); + $id = []; + foreach ($targetClass->associationMappings[$mappedBy]->joinColumns as $joinColumn) { + $id[] = $data[$joinColumn->name]; + } + } else { + $id = $this->identifierFlattener->flattenIdentifier($class, $class->getIdentifierValues($sourceEntity)); + } + + $idHash = implode(' ', $id); + + if ($mapping->indexBy !== null) { + $indexByProperty = $targetClass->getReflectionProperty($mapping->indexBy); + assert($indexByProperty !== null); + $collectionBatch[$idHash]->hydrateSet($indexByProperty->getValue($targetValue), $targetValue); + } else { + $collectionBatch[$idHash]->add($targetValue); + } + } + } + + foreach ($collections as $association) { + $association->setInitialized(true); + $association->takeSnapshot(); + } + } + + /** + * Initializes (loads) an uninitialized persistent collection of an entity. + * + * @todo Maybe later move to EntityManager#initialize($proxyOrCollection). See DDC-733. + */ + public function loadCollection(PersistentCollection $collection): void + { + $assoc = $collection->getMapping(); + $persister = $this->getEntityPersister($assoc->targetEntity); + + switch ($assoc->type()) { + case ClassMetadata::ONE_TO_MANY: + $persister->loadOneToManyCollection($assoc, $collection->getOwner(), $collection); + break; + + case ClassMetadata::MANY_TO_MANY: + $persister->loadManyToManyCollection($assoc, $collection->getOwner(), $collection); + break; + } + + $collection->setInitialized(true); + } + + /** + * Schedule this collection for batch loading at the end of the UnitOfWork + */ + private function scheduleCollectionForBatchLoading(PersistentCollection $collection, ClassMetadata $sourceClass): void + { + $mapping = $collection->getMapping(); + $name = $mapping->sourceEntity . '#' . $mapping->fieldName; + + if (! isset($this->eagerLoadingCollections[$name])) { + $this->eagerLoadingCollections[$name] = [ + 'items' => [], + 'mapping' => $mapping, + ]; + } + + $owner = $collection->getOwner(); + assert($owner !== null); + + $id = $this->identifierFlattener->flattenIdentifier( + $sourceClass, + $sourceClass->getIdentifierValues($owner), + ); + $idHash = implode(' ', $id); + + $this->eagerLoadingCollections[$name]['items'][$idHash] = $collection; + } + + /** + * Gets the identity map of the UnitOfWork. + * + * @psalm-return array> + */ + public function getIdentityMap(): array + { + return $this->identityMap; + } + + /** + * Gets the original data of an entity. The original data is the data that was + * present at the time the entity was reconstituted from the database. + * + * @psalm-return array + */ + public function getOriginalEntityData(object $entity): array + { + $oid = spl_object_id($entity); + + return $this->originalEntityData[$oid] ?? []; + } + + /** + * @param mixed[] $data + * + * @ignore + */ + public function setOriginalEntityData(object $entity, array $data): void + { + $this->originalEntityData[spl_object_id($entity)] = $data; + } + + /** + * INTERNAL: + * Sets a property value of the original data array of an entity. + * + * @ignore + */ + public function setOriginalEntityProperty(int $oid, string $property, mixed $value): void + { + $this->originalEntityData[$oid][$property] = $value; + } + + /** + * Gets the identifier of an entity. + * The returned value is always an array of identifier values. If the entity + * has a composite identifier then the identifier values are in the same + * order as the identifier field names as returned by ClassMetadata#getIdentifierFieldNames(). + * + * @return mixed[] The identifier values. + */ + public function getEntityIdentifier(object $entity): array + { + return $this->entityIdentifiers[spl_object_id($entity)] + ?? throw EntityNotFoundException::noIdentifierFound(get_debug_type($entity)); + } + + /** + * Processes an entity instance to extract their identifier values. + * + * @return mixed A scalar value. + * + * @throws ORMInvalidArgumentException + */ + public function getSingleIdentifierValue(object $entity): mixed + { + $class = $this->em->getClassMetadata($entity::class); + + if ($class->isIdentifierComposite) { + throw ORMInvalidArgumentException::invalidCompositeIdentifier(); + } + + $values = $this->isInIdentityMap($entity) + ? $this->getEntityIdentifier($entity) + : $class->getIdentifierValues($entity); + + return $values[$class->identifier[0]] ?? null; + } + + /** + * Tries to find an entity with the given identifier in the identity map of + * this UnitOfWork. + * + * @param mixed $id The entity identifier to look for. + * @param string $rootClassName The name of the root class of the mapped entity hierarchy. + * @psalm-param class-string $rootClassName + * + * @return object|false Returns the entity with the specified identifier if it exists in + * this UnitOfWork, FALSE otherwise. + */ + public function tryGetById(mixed $id, string $rootClassName): object|false + { + $idHash = self::getIdHashByIdentifier((array) $id); + + return $this->identityMap[$rootClassName][$idHash] ?? false; + } + + /** + * Schedules an entity for dirty-checking at commit-time. + * + * @todo Rename: scheduleForSynchronization + */ + public function scheduleForDirtyCheck(object $entity): void + { + $rootClassName = $this->em->getClassMetadata($entity::class)->rootEntityName; + + $this->scheduledForSynchronization[$rootClassName][spl_object_id($entity)] = $entity; + } + + /** + * Checks whether the UnitOfWork has any pending insertions. + */ + public function hasPendingInsertions(): bool + { + return ! empty($this->entityInsertions); + } + + /** + * Calculates the size of the UnitOfWork. The size of the UnitOfWork is the + * number of entities in the identity map. + */ + public function size(): int + { + return array_sum(array_map('count', $this->identityMap)); + } + + /** + * Gets the EntityPersister for an Entity. + * + * @psalm-param class-string $entityName + */ + public function getEntityPersister(string $entityName): EntityPersister + { + if (isset($this->persisters[$entityName])) { + return $this->persisters[$entityName]; + } + + $class = $this->em->getClassMetadata($entityName); + + $persister = match (true) { + $class->isInheritanceTypeNone() => new BasicEntityPersister($this->em, $class), + $class->isInheritanceTypeSingleTable() => new SingleTablePersister($this->em, $class), + $class->isInheritanceTypeJoined() => new JoinedSubclassPersister($this->em, $class), + default => throw new RuntimeException('No persister found for entity.'), + }; + + if ($this->hasCache && $class->cache !== null) { + $persister = $this->em->getConfiguration() + ->getSecondLevelCacheConfiguration() + ->getCacheFactory() + ->buildCachedEntityPersister($this->em, $persister, $class); + } + + $this->persisters[$entityName] = $persister; + + return $this->persisters[$entityName]; + } + + /** Gets a collection persister for a collection-valued association. */ + public function getCollectionPersister(AssociationMapping $association): CollectionPersister + { + $role = isset($association->cache) + ? $association->sourceEntity . '::' . $association->fieldName + : $association->type(); + + if (isset($this->collectionPersisters[$role])) { + return $this->collectionPersisters[$role]; + } + + $persister = $association->type() === ClassMetadata::ONE_TO_MANY + ? new OneToManyPersister($this->em) + : new ManyToManyPersister($this->em); + + if ($this->hasCache && isset($association->cache)) { + $persister = $this->em->getConfiguration() + ->getSecondLevelCacheConfiguration() + ->getCacheFactory() + ->buildCachedCollectionPersister($this->em, $persister, $association); + } + + $this->collectionPersisters[$role] = $persister; + + return $this->collectionPersisters[$role]; + } + + /** + * INTERNAL: + * Registers an entity as managed. + * + * @param mixed[] $id The identifier values. + * @param mixed[] $data The original entity data. + */ + public function registerManaged(object $entity, array $id, array $data): void + { + $oid = spl_object_id($entity); + + $this->entityIdentifiers[$oid] = $id; + $this->entityStates[$oid] = self::STATE_MANAGED; + $this->originalEntityData[$oid] = $data; + + $this->addToIdentityMap($entity); + } + + /* PropertyChangedListener implementation */ + + /** + * Notifies this UnitOfWork of a property change in an entity. + * + * {@inheritDoc} + */ + public function propertyChanged(object $sender, string $propertyName, mixed $oldValue, mixed $newValue): void + { + $oid = spl_object_id($sender); + $class = $this->em->getClassMetadata($sender::class); + + $isAssocField = isset($class->associationMappings[$propertyName]); + + if (! $isAssocField && ! isset($class->fieldMappings[$propertyName])) { + return; // ignore non-persistent fields + } + + // Update changeset and mark entity for synchronization + $this->entityChangeSets[$oid][$propertyName] = [$oldValue, $newValue]; + + if (! isset($this->scheduledForSynchronization[$class->rootEntityName][$oid])) { + $this->scheduleForDirtyCheck($sender); + } + } + + /** + * Gets the currently scheduled entity insertions in this UnitOfWork. + * + * @psalm-return array + */ + public function getScheduledEntityInsertions(): array + { + return $this->entityInsertions; + } + + /** + * Gets the currently scheduled entity updates in this UnitOfWork. + * + * @psalm-return array + */ + public function getScheduledEntityUpdates(): array + { + return $this->entityUpdates; + } + + /** + * Gets the currently scheduled entity deletions in this UnitOfWork. + * + * @psalm-return array + */ + public function getScheduledEntityDeletions(): array + { + return $this->entityDeletions; + } + + /** + * Gets the currently scheduled complete collection deletions + * + * @psalm-return array> + */ + public function getScheduledCollectionDeletions(): array + { + return $this->collectionDeletions; + } + + /** + * Gets the currently scheduled collection inserts, updates and deletes. + * + * @psalm-return array> + */ + public function getScheduledCollectionUpdates(): array + { + return $this->collectionUpdates; + } + + /** + * Helper method to initialize a lazy loading proxy or persistent collection. + */ + public function initializeObject(object $obj): void + { + if ($obj instanceof InternalProxy) { + $obj->__load(); + + return; + } + + if ($obj instanceof PersistentCollection) { + $obj->initialize(); + } + } + + /** + * Tests if a value is an uninitialized entity. + * + * @psalm-assert-if-true InternalProxy $obj + */ + public function isUninitializedObject(mixed $obj): bool + { + return $obj instanceof InternalProxy && ! $obj->__isInitialized(); + } + + /** + * Helper method to show an object as string. + */ + private static function objToStr(object $obj): string + { + return $obj instanceof Stringable ? (string) $obj : get_debug_type($obj) . '@' . spl_object_id($obj); + } + + /** + * Marks an entity as read-only so that it will not be considered for updates during UnitOfWork#commit(). + * + * This operation cannot be undone as some parts of the UnitOfWork now keep gathering information + * on this object that might be necessary to perform a correct update. + * + * @throws ORMInvalidArgumentException + */ + public function markReadOnly(object $object): void + { + if (! $this->isInIdentityMap($object)) { + throw ORMInvalidArgumentException::readOnlyRequiresManagedEntity($object); + } + + $this->readOnlyObjects[spl_object_id($object)] = true; + } + + /** + * Is this entity read only? + * + * @throws ORMInvalidArgumentException + */ + public function isReadOnly(object $object): bool + { + return isset($this->readOnlyObjects[spl_object_id($object)]); + } + + /** + * Perform whatever processing is encapsulated here after completion of the transaction. + */ + private function afterTransactionComplete(): void + { + $this->performCallbackOnCachedPersister(static function (CachedPersister $persister): void { + $persister->afterTransactionComplete(); + }); + } + + /** + * Perform whatever processing is encapsulated here after completion of the rolled-back. + */ + private function afterTransactionRolledBack(): void + { + $this->performCallbackOnCachedPersister(static function (CachedPersister $persister): void { + $persister->afterTransactionRolledBack(); + }); + } + + /** + * Performs an action after the transaction. + */ + private function performCallbackOnCachedPersister(callable $callback): void + { + if (! $this->hasCache) { + return; + } + + foreach ([...$this->persisters, ...$this->collectionPersisters] as $persister) { + if ($persister instanceof CachedPersister) { + $callback($persister); + } + } + } + + private function dispatchOnFlushEvent(): void + { + if ($this->evm->hasListeners(Events::onFlush)) { + $this->evm->dispatchEvent(Events::onFlush, new OnFlushEventArgs($this->em)); + } + } + + private function dispatchPostFlushEvent(): void + { + if ($this->evm->hasListeners(Events::postFlush)) { + $this->evm->dispatchEvent(Events::postFlush, new PostFlushEventArgs($this->em)); + } + } + + /** + * Verifies if two given entities actually are the same based on identifier comparison + */ + private function isIdentifierEquals(object $entity1, object $entity2): bool + { + if ($entity1 === $entity2) { + return true; + } + + $class = $this->em->getClassMetadata($entity1::class); + + if ($class !== $this->em->getClassMetadata($entity2::class)) { + return false; + } + + $oid1 = spl_object_id($entity1); + $oid2 = spl_object_id($entity2); + + $id1 = $this->entityIdentifiers[$oid1] ?? $this->identifierFlattener->flattenIdentifier($class, $class->getIdentifierValues($entity1)); + $id2 = $this->entityIdentifiers[$oid2] ?? $this->identifierFlattener->flattenIdentifier($class, $class->getIdentifierValues($entity2)); + + return $id1 === $id2 || self::getIdHashByIdentifier($id1) === self::getIdHashByIdentifier($id2); + } + + /** @throws ORMInvalidArgumentException */ + private function assertThatThereAreNoUnintentionallyNonPersistedAssociations(): void + { + $entitiesNeedingCascadePersist = array_diff_key($this->nonCascadedNewDetectedEntities, $this->entityInsertions); + + $this->nonCascadedNewDetectedEntities = []; + + if ($entitiesNeedingCascadePersist) { + throw ORMInvalidArgumentException::newEntitiesFoundThroughRelationships( + array_values($entitiesNeedingCascadePersist), + ); + } + } + + /** + * This method called by hydrators, and indicates that hydrator totally completed current hydration cycle. + * Unit of work able to fire deferred events, related to loading events here. + * + * @internal should be called internally from object hydrators + */ + public function hydrationComplete(): void + { + $this->hydrationCompleteHandler->hydrationComplete(); + } + + /** @throws MappingException if the entity has more than a single identifier. */ + private function convertSingleFieldIdentifierToPHPValue(ClassMetadata $class, mixed $identifierValue): mixed + { + return $this->em->getConnection()->convertToPHPValue( + $identifierValue, + $class->getTypeOfField($class->getSingleIdentifierFieldName()), + ); + } + + /** + * Given a flat identifier, this method will produce another flat identifier, but with all + * association fields that are mapped as identifiers replaced by entity references, recursively. + * + * @param mixed[] $flatIdentifier + * + * @return array + */ + private function normalizeIdentifier(ClassMetadata $targetClass, array $flatIdentifier): array + { + $normalizedAssociatedId = []; + + foreach ($targetClass->getIdentifierFieldNames() as $name) { + if (! array_key_exists($name, $flatIdentifier)) { + continue; + } + + if (! $targetClass->isSingleValuedAssociation($name)) { + $normalizedAssociatedId[$name] = $flatIdentifier[$name]; + continue; + } + + $targetIdMetadata = $this->em->getClassMetadata($targetClass->getAssociationTargetClass($name)); + + // Note: the ORM prevents using an entity with a composite identifier as an identifier association + // therefore, reset($targetIdMetadata->identifier) is always correct + $normalizedAssociatedId[$name] = $this->em->getReference( + $targetIdMetadata->getName(), + $this->normalizeIdentifier( + $targetIdMetadata, + [(string) reset($targetIdMetadata->identifier) => $flatIdentifier[$name]], + ), + ); + } + + return $normalizedAssociatedId; + } + + /** + * Assign a post-insert generated ID to an entity + * + * This is used by EntityPersisters after they inserted entities into the database. + * It will place the assigned ID values in the entity's fields and start tracking + * the entity in the identity map. + */ + final public function assignPostInsertId(object $entity, mixed $generatedId): void + { + $class = $this->em->getClassMetadata($entity::class); + $idField = $class->getSingleIdentifierFieldName(); + $idValue = $this->convertSingleFieldIdentifierToPHPValue($class, $generatedId); + $oid = spl_object_id($entity); + + $class->reflFields[$idField]->setValue($entity, $idValue); + + $this->entityIdentifiers[$oid] = [$idField => $idValue]; + $this->entityStates[$oid] = self::STATE_MANAGED; + $this->originalEntityData[$oid][$idField] = $idValue; + + $this->addToIdentityMap($entity); + } +} 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 @@ + + */ + public static function resolveDiscriminatorsForClass( + ClassMetadata $rootClassMetadata, + EntityManagerInterface $entityManager, + ): array { + $hierarchyClasses = $rootClassMetadata->subClasses; + $hierarchyClasses[] = $rootClassMetadata->name; + + $discriminators = []; + + foreach ($hierarchyClasses as $class) { + $currentMetadata = $entityManager->getClassMetadata($class); + $currentDiscriminator = $currentMetadata->discriminatorValue; + + if ($currentDiscriminator !== null) { + $discriminators[$currentDiscriminator] = null; + } + } + + return $discriminators; + } +} 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 @@ + + */ + public function flattenIdentifier(ClassMetadata $class, array $id): array + { + $flatId = []; + + foreach ($class->identifier as $field) { + if (isset($class->associationMappings[$field]) && isset($id[$field]) && is_a($id[$field], $class->associationMappings[$field]->targetEntity)) { + $targetClassMetadata = $this->metadataFactory->getMetadataFor( + $class->associationMappings[$field]->targetEntity, + ); + assert($targetClassMetadata instanceof ClassMetadata); + + if ($this->unitOfWork->isInIdentityMap($id[$field])) { + $associatedId = $this->flattenIdentifier($targetClassMetadata, $this->unitOfWork->getEntityIdentifier($id[$field])); + } else { + $associatedId = $this->flattenIdentifier($targetClassMetadata, $targetClassMetadata->getIdentifierValues($id[$field])); + } + + $flatId[$field] = implode(' ', $associatedId); + } elseif (isset($class->associationMappings[$field])) { + assert($class->associationMappings[$field]->isToOneOwningSide()); + $associatedId = []; + + foreach ($class->associationMappings[$field]->joinColumns as $joinColumn) { + $associatedId[] = $id[$joinColumn->name]; + } + + $flatId[$field] = implode(' ', $associatedId); + } else { + if ($id[$field] instanceof BackedEnum) { + $flatId[$field] = $id[$field]->value; + } else { + $flatId[$field] = $id[$field]; + } + } + } + + return $flatId; + } +} 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 @@ + 'LOCK IN SHARE MODE', + $platform instanceof PostgreSQLPlatform => 'FOR SHARE', + default => $this->getWriteLockSQL($platform), + }; + } + + private function getWriteLockSQL(AbstractPlatform $platform): string + { + return match (true) { + $platform instanceof DB2Platform => 'WITH RR USE AND KEEP UPDATE LOCKS', + $platform instanceof SQLitePlatform, + $platform instanceof SQLServerPlatform => '', + default => 'FOR UPDATE', + }; + } +} 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 @@ + + * + * @throws QueryException + */ + public static function getTypeOfField(string $fieldName, ClassMetadata $class, EntityManagerInterface $em): array + { + if (isset($class->fieldMappings[$fieldName])) { + return [$class->fieldMappings[$fieldName]->type]; + } + + if (! isset($class->associationMappings[$fieldName])) { + return []; + } + + $assoc = $class->associationMappings[$fieldName]; + + if (! $assoc->isOwningSide()) { + return self::getTypeOfField($assoc->mappedBy, $em->getClassMetadata($assoc->targetEntity), $em); + } + + if ($assoc->isManyToManyOwningSide()) { + $joinData = $assoc->joinTable; + } else { + $joinData = $assoc; + } + + $types = []; + $targetClass = $em->getClassMetadata($assoc->targetEntity); + + foreach ($joinData->joinColumns as $joinColumn) { + $types[] = self::getTypeOfColumn($joinColumn->referencedColumnName, $targetClass, $em); + } + + return $types; + } + + /** @throws RuntimeException */ + public static function getTypeOfColumn(string $columnName, ClassMetadata $class, EntityManagerInterface $em): string + { + if (isset($class->fieldNames[$columnName])) { + $fieldName = $class->fieldNames[$columnName]; + + if (isset($class->fieldMappings[$fieldName])) { + return $class->fieldMappings[$fieldName]->type; + } + } + + // iterate over to-one association mappings + foreach ($class->associationMappings as $assoc) { + if (! $assoc->isToOneOwningSide()) { + continue; + } + + foreach ($assoc->joinColumns as $joinColumn) { + if ($joinColumn->name === $columnName) { + $targetColumnName = $joinColumn->referencedColumnName; + $targetClass = $em->getClassMetadata($assoc->targetEntity); + + return self::getTypeOfColumn($targetColumnName, $targetClass, $em); + } + } + } + + // iterate over to-many association mappings + foreach ($class->associationMappings as $assoc) { + if (! $assoc->isManyToManyOwningSide()) { + continue; + } + + foreach ($assoc->joinTable->joinColumns as $joinColumn) { + if ($joinColumn->name === $columnName) { + $targetColumnName = $joinColumn->referencedColumnName; + $targetClass = $em->getClassMetadata($assoc->targetEntity); + + return self::getTypeOfColumn($targetColumnName, $targetClass, $em); + } + } + } + + throw new RuntimeException(sprintf( + 'Could not resolve type of column "%s" of class "%s"', + $columnName, + $class->getName(), + )); + } +} -- cgit v1.2.3