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/LICENSE | 19 + vendor/doctrine/orm/README.md | 40 + vendor/doctrine/orm/SECURITY.md | 17 + vendor/doctrine/orm/UPGRADE.md | 2303 ++++++++++++++ vendor/doctrine/orm/composer.json | 65 + vendor/doctrine/orm/doctrine-mapping.xsd | 589 ++++ vendor/doctrine/orm/phpstan-dbal3.neon | 29 + 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 + 432 files changed, 54867 insertions(+) create mode 100644 vendor/doctrine/orm/LICENSE create mode 100644 vendor/doctrine/orm/README.md create mode 100644 vendor/doctrine/orm/SECURITY.md create mode 100644 vendor/doctrine/orm/UPGRADE.md create mode 100644 vendor/doctrine/orm/composer.json create mode 100644 vendor/doctrine/orm/doctrine-mapping.xsd create mode 100644 vendor/doctrine/orm/phpstan-dbal3.neon 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') diff --git a/vendor/doctrine/orm/LICENSE b/vendor/doctrine/orm/LICENSE new file mode 100644 index 0000000..f988839 --- /dev/null +++ b/vendor/doctrine/orm/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) Doctrine Project + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/doctrine/orm/README.md b/vendor/doctrine/orm/README.md new file mode 100644 index 0000000..1df322c --- /dev/null +++ b/vendor/doctrine/orm/README.md @@ -0,0 +1,40 @@ +| [4.0.x][4.0] | [3.3.x][3.3] | [3.2.x][3.2] | [2.20.x][2.20] | [2.19.x][2.19] | +|:------------------------------------------------------:|:------------------------------------------------------:|:------------------------------------------------------:|:--------------------------------------------------------:|:--------------------------------------------------------:| +| [![Build status][4.0 image]][4.0] | [![Build status][3.3 image]][3.3] | [![Build status][3.2 image]][3.2] | [![Build status][2.20 image]][2.20] | [![Build status][2.19 image]][2.19] | +| [![Coverage Status][4.0 coverage image]][4.0 coverage] | [![Coverage Status][3.3 coverage image]][3.3 coverage] | [![Coverage Status][3.2 coverage image]][3.2 coverage] | [![Coverage Status][2.20 coverage image]][2.20 coverage] | [![Coverage Status][2.19 coverage image]][2.19 coverage] | + +[

πŸ‡ΊπŸ‡¦ UKRAINE NEEDS YOUR HELP NOW!

](https://www.doctrine-project.org/stop-war.html) + +Doctrine ORM is an object-relational mapper for PHP 8.1+ that provides transparent persistence +for PHP objects. It sits on top of a powerful database abstraction layer (DBAL). One of its key features +is the option to write database queries in a proprietary object oriented SQL dialect called Doctrine Query Language (DQL), +inspired by Hibernate's HQL. This provides developers with a powerful alternative to SQL that maintains flexibility +without requiring unnecessary code duplication. + + +## More resources: + +* [Website](http://www.doctrine-project.org) +* [Documentation](https://www.doctrine-project.org/projects/doctrine-orm/en/stable/index.html) + + + [4.0 image]: https://github.com/doctrine/orm/actions/workflows/continuous-integration.yml/badge.svg?branch=4.0.x + [4.0]: https://github.com/doctrine/orm/tree/4.0.x + [4.0 coverage image]: https://codecov.io/gh/doctrine/orm/branch/4.0.x/graph/badge.svg + [4.0 coverage]: https://codecov.io/gh/doctrine/orm/branch/4.0.x + [3.3 image]: https://github.com/doctrine/orm/actions/workflows/continuous-integration.yml/badge.svg?branch=3.3.x + [3.3]: https://github.com/doctrine/orm/tree/3.3.x + [3.3 coverage image]: https://codecov.io/gh/doctrine/orm/branch/3.3.x/graph/badge.svg + [3.3 coverage]: https://codecov.io/gh/doctrine/orm/branch/3.3.x + [3.2 image]: https://github.com/doctrine/orm/actions/workflows/continuous-integration.yml/badge.svg?branch=3.2.x + [3.2]: https://github.com/doctrine/orm/tree/3.2.x + [3.2 coverage image]: https://codecov.io/gh/doctrine/orm/branch/3.2.x/graph/badge.svg + [3.2 coverage]: https://codecov.io/gh/doctrine/orm/branch/3.2.x + [2.20 image]: https://github.com/doctrine/orm/actions/workflows/continuous-integration.yml/badge.svg?branch=2.20.x + [2.20]: https://github.com/doctrine/orm/tree/2.20.x + [2.20 coverage image]: https://codecov.io/gh/doctrine/orm/branch/2.20.x/graph/badge.svg + [2.20 coverage]: https://codecov.io/gh/doctrine/orm/branch/2.20.x + [2.19 image]: https://github.com/doctrine/orm/actions/workflows/continuous-integration.yml/badge.svg?branch=2.19.x + [2.19]: https://github.com/doctrine/orm/tree/2.19.x + [2.19 coverage image]: https://codecov.io/gh/doctrine/orm/branch/2.19.x/graph/badge.svg + [2.19 coverage]: https://codecov.io/gh/doctrine/orm/branch/2.19.x diff --git a/vendor/doctrine/orm/SECURITY.md b/vendor/doctrine/orm/SECURITY.md new file mode 100644 index 0000000..b0e7293 --- /dev/null +++ b/vendor/doctrine/orm/SECURITY.md @@ -0,0 +1,17 @@ +Security +======== + +The Doctrine library is operating very close to your database and as such needs +to handle and make assumptions about SQL injection vulnerabilities. + +It is vital that you understand how Doctrine approaches security, because +we cannot protect you from SQL injection. + +Please read the documentation chapter on Security in Doctrine DBAL and ORM to +understand the assumptions we make. + +- [DBAL Security Page](https://www.doctrine-project.org/projects/doctrine-dbal/en/stable/reference/security.html) +- [ORM Security Page](https://www.doctrine-project.org/projects/doctrine-orm/en/stable/reference/security.html) + +If you find a Security bug in Doctrine, please follow our +[Security reporting guidelines](https://www.doctrine-project.org/policies/security.html#reporting). diff --git a/vendor/doctrine/orm/UPGRADE.md b/vendor/doctrine/orm/UPGRADE.md new file mode 100644 index 0000000..1869e9f --- /dev/null +++ b/vendor/doctrine/orm/UPGRADE.md @@ -0,0 +1,2303 @@ +# Upgrade to 3.2 + +## Deprecate the `NotSupported` exception + +The class `Doctrine\ORM\Exception\NotSupported` is deprecated without replacement. + +## Deprecate remaining `Serializable` implementation + +Relying on `SequenceGenerator` implementing the `Serializable` is deprecated +because that interface won't be implemented in ORM 4 anymore. + +The following methods are deprecated: + +* `SequenceGenerator::serialize()` +* `SequenceGenerator::unserialize()` + +## `orm:schema-tool:update` option `--complete` is deprecated + +That option behaves as a no-op, and is deprecated. It will be removed in 4.0. + +## Deprecate properties `$indexes` and `$uniqueConstraints` of `Doctrine\ORM\Mapping\Table` + +The properties `$indexes` and `$uniqueConstraints` have been deprecated since they had no effect at all. +The preferred way of defining indices and unique constraints is by +using the `\Doctrine\ORM\Mapping\UniqueConstraint` and `\Doctrine\ORM\Mapping\Index` attributes. + +# Upgrade to 3.1 + +## Deprecate `Doctrine\ORM\Mapping\ReflectionEnumProperty` + +This class is deprecated and will be removed in 4.0. +Instead, use `Doctrine\Persistence\Reflection\EnumReflectionProperty` from +`doctrine/persistence`. + +## Deprecate passing null to `ClassMetadata::fullyQualifiedClassName()` + +Passing `null` to `Doctrine\ORM\ClassMetadata::fullyQualifiedClassName()` is +deprecated and will no longer be possible in 4.0. + +## Deprecate array access + +Using array access on instances of the following classes is deprecated: + +- `Doctrine\ORM\Mapping\DiscriminatorColumnMapping` +- `Doctrine\ORM\Mapping\EmbedClassMapping` +- `Doctrine\ORM\Mapping\FieldMapping` +- `Doctrine\ORM\Mapping\JoinColumnMapping` +- `Doctrine\ORM\Mapping\JoinTableMapping` + +# Upgrade to 3.0 + +## BC BREAK: Calling `ClassMetadata::getAssociationMappedByTargetField()` with the owning side of an association now throws an exception + +Previously, calling +`Doctrine\ORM\Mapping\ClassMetadata::getAssociationMappedByTargetField()` with +the owning side of an association returned `null`, which was undocumented, and +wrong according to the phpdoc of the parent method. + +If you do not know whether you are on the owning or inverse side of an association, +you can use `Doctrine\ORM\Mapping\ClassMetadata::isAssociationInverseSide()` +to find out. + +## BC BREAK: `Doctrine\ORM\Proxy\Autoloader` no longer extends `Doctrine\Common\Proxy\Autoloader` + +Make sure to use the former when writing a type declaration or an `instanceof` check. + +## Minor BC BREAK: Changed order of arguments passed to `OneToOne`, `ManyToOne` and `Index` mapping PHP attributes + +To keep PHP mapping attributes consistent, order of arguments passed to above attributes has been changed +so `$targetEntity` is a first argument now. This change affects only non-named arguments usage. + +## BC BREAK: AUTO keyword for identity generation defaults to IDENTITY for PostgreSQL when using `doctrine/dbal` 4 + +When using the `AUTO` strategy to let Doctrine determine the identity generation mechanism for +an entity, and when using `doctrine/dbal` 4, PostgreSQL now uses `IDENTITY` +instead of `SEQUENCE` or `SERIAL`. +* If you want to upgrade your existing tables to identity columns, you will need to follow [migration to identity columns on PostgreSQL](https://www.doctrine-project.org/projects/doctrine-dbal/en/4.0/how-to/postgresql-identity-migration.html) +* If you want to keep using SQL sequences, you need to configure the ORM this way: +```php +use Doctrine\DBAL\Platforms\PostgreSQLPlatform; +use Doctrine\ORM\Configuration; +use Doctrine\ORM\Mapping\ClassMetadata; + +assert($configuration instanceof Configuration); +$configuration->setIdentityGenerationPreferences([ + PostgreSQLPlatform::CLASS => ClassMetadata::GENERATOR_TYPE_SEQUENCE, +]); +``` + +## BC BREAK: Throw exceptions when using illegal attributes on Embeddable + +There are only a few attributes allowed on an embeddable such as `#[Column]` or +`#[Embedded]`. Previously all others that target entity classes where ignored, +now they throw an exception. + +## BC BREAK: Partial objects are removed + +- The `PARTIAL` keyword in DQL no longer exists. +- `Doctrine\ORM\Query\AST\PartialObjectExpression`is removed. +- `Doctrine\ORM\Query\SqlWalker::HINT_PARTIAL` and + `Doctrine\ORM\Query::HINT_FORCE_PARTIAL_LOAD` are removed. +- `Doctrine\ORM\EntityManager*::getPartialReference()` is removed. + +## BC BREAK: `Doctrine\ORM\Persister\Entity\EntityPersister::executeInserts()` return type changed to `void` + +Implementors should adapt to the new signature, and should call +`UnitOfWork::assignPostInsertId()` for each entry in the previously returned +array. + +## BC BREAK: `Doctrine\ORM\Proxy\ProxyFactory` no longer extends abstract factory from `doctrine/common` + +It is no longer possible to call methods, constants or properties inherited +from that class on a `ProxyFactory` instance. + +`Doctrine\ORM\Proxy\ProxyFactory::createProxyDefinition()` and +`Doctrine\ORM\Proxy\ProxyFactory::resetUninitializedProxy()` are removed as well. + +## BC BREAK: lazy ghosts are enabled unconditionally + +`Doctrine\ORM\Configuration::setLazyGhostObjectEnabled()` and +`Doctrine\ORM\Configuration::isLazyGhostObjectEnabled()` are now no-ops and +will be deprecated in 3.1.0 + +## BC BREAK: collisions in identity map are unconditionally rejected + +`Doctrine\ORM\Configuration::setRejectIdCollisionInIdentityMap()` and +`Doctrine\ORM\Configuration::isRejectIdCollisionInIdentityMapEnabled()` are now +no-ops and will be deprecated in 3.1.0. + +## BC BREAK: Lifecycle callback mapping on embedded classes is now explicitly forbidden + +Lifecycle callback mapping on embedded classes produced no effect, and is now +explicitly forbidden to point out mistakes. + +## BC BREAK: The `NOTIFY` change tracking policy is removed + +You should use `DEFERRED_EXPLICIT` instead. + +## BC BREAK: `Mapping\Driver\XmlDriver::__construct()` third argument is now enabled by default + +The third argument to +`Doctrine\ORM\Mapping\Driver\XmlDriver::__construct()` was introduced to +let users opt-in to XML validation, that is now always enabled by default. + +As a consequence, the same goes for +`Doctrine\ORM\Mapping\Driver\SimplifiedXmlDriver`, and for +`Doctrine\ORM\ORMSetup::createXMLMetadataConfiguration()`. + +## BC BREAK: `Mapping\Driver\AttributeDriver::__construct()` second argument is now a no-op + +The second argument to +`Doctrine\ORM\Mapping\Driver\AttributeDriver::__construct()` was introduced to +let users opt-in to a new behavior, that is now always enforced, regardless of +the value of that argument. + +## BC BREAK: `Query::setDQL()` and `Query::setFirstResult()` no longer accept `null` + +The `$dqlQuery` argument of `Doctrine\ORM\Query::setDQL()` must always be a +string. + +The `$firstResult` argument of `Doctrine\ORM\Query::setFirstResult()` must +always be an integer. + +## BC BREAK: `orm:schema-tool:update` option `--complete` is now a no-op + +`orm:schema-tool:update` now behaves as if `--complete` was provided, +regardless of whether it is provided or not. + +## BC BREAK: Removed `Doctrine\ORM\Proxy\Proxy` interface. + +Use `Doctrine\Persistence\Proxy` instead to check whether proxies are initialized. + +## BC BREAK: Overriding fields or associations declared in other than mapped superclasses + +As stated in the documentation, fields and associations may only be overridden when being inherited +from mapped superclasses. Overriding them for parent entity classes now throws a `MappingException`. + +## BC BREAK: Undeclared entity inheritance now throws a `MappingException` + +As soon as an entity class inherits from another entity class, inheritance has to +be declared by adding the appropriate configuration for the root entity. + +## Removed `getEntityManager()` in `Doctrine\ORM\Event\OnClearEventArgs` and `Doctrine\ORM\Event\*FlushEventArgs` + +Use `getObjectManager()` instead. + +## BC BREAK: Removed `Doctrine\ORM\Mapping\ClassMetadataInfo` class + +Use `Doctrine\ORM\Mapping\ClassMetadata` instead. + +## BC BREAK: Removed `Doctrine\ORM\Event\LifecycleEventArgs` class. + +Use one of the dedicated event classes instead: + +* `Doctrine\ORM\Event\PrePersistEventArgs` +* `Doctrine\ORM\Event\PreUpdateEventArgs` +* `Doctrine\ORM\Event\PreRemoveEventArgs` +* `Doctrine\ORM\Event\PostPersistEventArgs` +* `Doctrine\ORM\Event\PostUpdateEventArgs` +* `Doctrine\ORM\Event\PostRemoveEventArgs` +* `Doctrine\ORM\Event\PostLoadEventArgs` + +## BC BREAK: Removed `AttributeDriver::$entityAnnotationClasses` and `AttributeDriver::getReader()` + +* If you need to change the behavior of `AttributeDriver::isTransient()`, + override that method instead. +* The attribute reader is internal to the driver and should not be accessed from outside. + +## BC BREAK: Removed `Doctrine\ORM\Query\AST\InExpression` + +The AST parser will create a `InListExpression` or a `InSubselectExpression` when +encountering an `IN ()` DQL expression instead of a generic `InExpression`. + +As a consequence, `SqlWalker::walkInExpression()` has been replaced by +`SqlWalker::walkInListExpression()` and `SqlWalker::walkInSubselectExpression()`. + +## BC BREAK: Changed `EntityManagerInterface#refresh($entity)`, `EntityManagerDecorator#refresh($entity)` and `UnitOfWork#refresh($entity)` signatures + +The new signatures of these methods add an optional `LockMode|int|null $lockMode` +param with default `null` value (no lock). + +## BC Break: Removed AnnotationDriver + +The annotation driver and anything related to annotation has been removed. +Please migrate to another mapping driver. + +The `Doctrine\ORM\Mapping\Annotation` maker interface has been removed in favor of the new +`Doctrine\ORM\Mapping\MappingAttribute` interface. + +## BC BREAK: Removed `EntityManager::create()` + +The constructor of `EntityManager` is now public and must be used instead of the `create()` method. +However, the constructor expects a `Connection` while `create()` accepted an array with connection parameters. +You can pass that array to DBAL's `Doctrine\DBAL\DriverManager::getConnection()` method to bootstrap the +connection. + +## BC BREAK: Removed `QueryBuilder` methods and constants. + +The following `QueryBuilder` constants and methods have been removed: + +1. `SELECT`, +2. `DELETE`, +3. `UPDATE`, +4. `STATE_DIRTY`, +5. `STATE_CLEAN`, +6. `getState()`, +7. `getType()`. + +## BC BREAK: Omitting only the alias argument for `QueryBuilder::update` and `QueryBuilder::delete` is not supported anymore + +When building an UPDATE or DELETE query and when passing a class/type to the function, the alias argument must not be omitted. + +### Before + +```php +$qb = $em->createQueryBuilder() + ->delete('User u') + ->where('u.id = :user_id') + ->setParameter('user_id', 1); +``` + +### After + +```php +$qb = $em->createQueryBuilder() + ->delete('User', 'u') + ->where('u.id = :user_id') + ->setParameter('user_id', 1); +``` + +## BC BREAK: Split output walkers and tree walkers + +`SqlWalker` and its child classes don't implement the `TreeWalker` interface +anymore. + +The following methods have been removed from the `TreeWalker` interface and +from the `TreeWalkerAdapter` and `TreeWalkerChain` classes: + +* `setQueryComponent()` +* `walkSelectClause()` +* `walkFromClause()` +* `walkFunction()` +* `walkOrderByClause()` +* `walkOrderByItem()` +* `walkHavingClause()` +* `walkJoin()` +* `walkSelectExpression()` +* `walkQuantifiedExpression()` +* `walkSubselect()` +* `walkSubselectFromClause()` +* `walkSimpleSelectClause()` +* `walkSimpleSelectExpression()` +* `walkAggregateExpression()` +* `walkGroupByClause()` +* `walkGroupByItem()` +* `walkDeleteClause()` +* `walkUpdateClause()` +* `walkUpdateItem()` +* `walkWhereClause()` +* `walkConditionalExpression()` +* `walkConditionalTerm()` +* `walkConditionalFactor()` +* `walkConditionalPrimary()` +* `walkExistsExpression()` +* `walkCollectionMemberExpression()` +* `walkEmptyCollectionComparisonExpression()` +* `walkNullComparisonExpression()` +* `walkInExpression()` +* `walkInstanceOfExpression()` +* `walkLiteral()` +* `walkBetweenExpression()` +* `walkLikeExpression()` +* `walkStateFieldPathExpression()` +* `walkComparisonExpression()` +* `walkInputParameter()` +* `walkArithmeticExpression()` +* `walkArithmeticTerm()` +* `walkStringPrimary()` +* `walkArithmeticFactor()` +* `walkSimpleArithmeticExpression()` +* `walkPathExpression()` +* `walkResultVariable()` +* `getExecutor()` + +The following changes have been made to the abstract `TreeWalkerAdapter` class: + +* The method `setQueryComponent()` is now protected. +* The method `_getQueryComponents()` has been removed in favor of + `getQueryComponents()`. + +## BC BREAK: Removed identity columns emulation through sequences + +If the platform you are using does not support identity columns, you should +switch to the `SEQUENCE` strategy. + +## BC BREAK: Made setters parameters mandatory + +The following methods require an argument when being called. Pass `null` +instead of omitting the argument. + +* `Doctrine\ORM\Event\OnClassMetadataNotFoundEventArgs::setFoundMetadata()` +* `Doctrine\ORM\AbstractQuery::setHydrationCacheProfile()` +* `Doctrine\ORM\AbstractQuery::setResultCache()` +* `Doctrine\ORM\AbstractQuery::setResultCacheProfile()` + +## BC BREAK: New argument to `NamingStrategy::joinColumnName()` + +### Before + +```php + `Exception\MissingMappingDriverImplementation::create()` + * `unrecognizedField()` => `Persisters\Exception\UnrecognizedField::byName()` + * `unexpectedAssociationValue()` => `Exception\UnexpectedAssociationValue::create()` + * `invalidOrientation()` => `Persisters\Exception\InvalidOrientation::fromClassNameAndField()` + * `entityManagerClosed()` => `Exception\EntityManagerClosed::create()` + * `invalidHydrationMode()` => `Exception\InvalidHydrationMode::fromMode()` + * `mismatchedEventManager()` => `Exception\MismatchedEventManager::create()` + * `findByRequiresParameter()` => `Repository\Exception\InvalidMagicMethodCall::onMissingParameter()` + * `invalidMagicCall()` => `Repository\Exception\InvalidMagicMethodCall::becauseFieldNotFoundIn()` + * `invalidFindByInverseAssociation()` => `Repository\Exception\InvalidFindByCall::fromInverseSideUsage()` + * `invalidResultCacheDriver()` => `Cache\Exception\InvalidResultCacheDriver::create()` + * `notSupported()` => `Exception\NotSupported::create()` + * `queryCacheNotConfigured()` => `QueryCacheNotConfigured::create()` + * `metadataCacheNotConfigured()` => `Cache\Exception\MetadataCacheNotConfigured::create()` + * `queryCacheUsesNonPersistentCache()` => `Cache\Exception\QueryCacheUsesNonPersistentCache::fromDriver()` + * `metadataCacheUsesNonPersistentCache()` => `Cache\Exception\MetadataCacheUsesNonPersistentCache::fromDriver()` + * `proxyClassesAlwaysRegenerating()` => `Exception\ProxyClassesAlwaysRegenerating::create()` + * `invalidEntityRepository()` => `Exception\InvalidEntityRepository::fromClassName()` + * `missingIdentifierField()` => `Exception\MissingIdentifierField::fromFieldAndClass()` + * `unrecognizedIdentifierFields()` => `Exception\UnrecognizedIdentifierFields::fromClassAndFieldNames()` + * `cantUseInOperatorOnCompositeKeys()` => `Persisters\Exception\CantUseInOperatorOnCompositeKeys::create()` + +## BC Break: `CacheException` is no longer a class, but an interface + +All methods in `Doctrine\ORM\Cache\CacheException` have been extracted to dedicated exceptions. + + * `updateReadOnlyCollection()` => `Cache\Exception\CannotUpdateReadOnlyCollection::fromEntityAndField()` + * `updateReadOnlyEntity()` => `Cache\Exception\CannotUpdateReadOnlyEntity::fromEntity()` + * `nonCacheableEntity()` => `Cache\Exception\NonCacheableEntity::fromEntity()` + * `nonCacheableEntityAssociation()` => `Cache\Exception\NonCacheableEntityAssociation::fromEntityAndField()` + + +## BC Break: Missing type declaration added for identifier generators + +Although undocumented, it was possible to configure a custom repository +class that implements `ObjectRepository` but does not extend the +`EntityRepository` base class. Repository classes have to extend +`EntityRepository` now. + +## BC BREAK: Removed support for entity namespace alias + +- `EntityManager::getRepository()` no longer accepts the entity namespace alias + notation. +- `Configuration::addEntityNamespace()` and + `Configuration::getEntityNamespace()` have been removed. + +## BC BREAK: Remove helper methods from `AbstractCollectionPersister` + +The following protected methods of +`Doctrine\ORM\Cache\Persister\Collection\AbstractCollectionPersister` +have been removed. + +* `evictCollectionCache()` +* `evictElementCache()` + +## BC BREAK: `Doctrine\ORM\Query\TreeWalkerChainIterator` + +This class has been removed without replacement. + +## BC BREAK: Remove quoting methods from `ClassMetadata` + +The following methods have been removed from the class metadata because +quoting is handled by implementations of `Doctrine\ORM\Mapping\QuoteStrategy`: + +* `getQuotedIdentifierColumnNames()` +* `getQuotedColumnName()` +* `getQuotedTableName()` +* `getQuotedJoinTableName()` + +## BC BREAK: Remove ability to merge detached entities + +Merge semantics was a poor fit for the PHP "share-nothing" architecture. +In addition to that, merging caused multiple issues with data integrity +in the managed entity graph, which was constantly spawning more edge-case +bugs/scenarios. + +The method `UnitOfWork::merge()` has been removed. The method +`EntityManager::merge()` will throw an exception on each call. + +## BC BREAK: Removed ability to partially flush/commit entity manager and unit of work + +The following methods don't accept a single entity or an array of entities anymore: + +* `Doctrine\ORM\EntityManager::flush()` +* `Doctrine\ORM\Decorator\EntityManagerDecorator::flush()` +* `Doctrine\ORM\UnitOfWork::commit()` + +The semantics of `flush()` and `commit()` will remain the same, but the change +tracking will be performed on all entities managed by the unit of work, and not +just on the provided entities, as the parameter is now completely ignored. + +## BC BREAK: Removed ability to partially clear entity manager and unit of work + +* Passing an argument other than `null` to `EntityManager::clear()` will raise + an exception. +* The unit of work cannot be cleared partially anymore. Passing an argument to + `UnitOfWork::clear()` does not have any effect anymore; the unit of work is + cleared completely. +* The method `EntityRepository::clear()` has been removed. +* The methods `getEntityClass()` and `clearsAllEntities()` have been removed + from `OnClearEventArgs`. + +## BC BREAK: Remove support for Doctrine Cache + +The Doctrine Cache library is not supported anymore. The following methods +have been removed from `Doctrine\ORM\Configuration`: + +* `getQueryCacheImpl()` +* `setQueryCacheImpl()` +* `getHydrationCacheImpl()` +* `setHydrationCacheImpl()` +* `getMetadataCacheImpl()` +* `setMetadataCacheImpl()` + +The methods have been replaced by PSR-6 compatible counterparts +(just strip the `Impl` suffix from the old name to get the new one). + +## BC BREAK: Remove `Doctrine\ORM\Configuration::newDefaultAnnotationDriver` + +This functionality has been moved to the new `ORMSetup` class. Call +`Doctrine\ORM\ORMSetup::createDefaultAnnotationDriver()` to create +a new annotation driver. + +## BC BREAK: Remove `Doctrine\ORM\Tools\Setup` + +In our effort to migrate from Doctrine Cache to PSR-6, the `Setup` class which +accepted a Doctrine Cache instance in each method has been removed. + +The replacement is `Doctrine\ORM\ORMSetup` which accepts a PSR-6 +cache instead. + +## BC BREAK: Removed named queries + +All APIs related to named queries have been removed. + +## BC BREAK: Remove old cache accessors and mutators from query classes + +The following methods have been removed from `AbstractQuery`: + +* `setResultCacheDriver()` +* `getResultCacheDriver()` +* `useResultCache()` +* `getResultCacheLifetime()` +* `getResultCacheId()` + +The following methods have been removed from `Query`: + +* `setQueryCacheDriver()` +* `getQueryCacheDriver()` + +## BC BREAK: Remove `Doctrine\ORM\Cache\MultiGetRegion` + +The interface has been merged into `Doctrine\ORM\Cache\Region`. + +## BC BREAK: Rename `AbstractIdGenerator::generate()` to `generateId()` + +* Implementations of `AbstractIdGenerator` have to implement the method + `generateId()`. +* The method `generate()` has been removed from `AbstractIdGenerator`. + +## BC BREAK: Remove cache settings inspection + +Doctrine does not provide its own cache implementation anymore and relies on +the PSR-6 standard instead. As a consequence, we cannot determine anymore +whether a given cache adapter is suitable for a production environment. +Because of that, functionality that aims to do so has been removed: + +* `Configuration::ensureProductionSettings()` +* the `orm:ensure-production-settings` console command + +## BC BREAK: PSR-6-based second level cache + +The second level cache has been reworked to consume a PSR-6 cache. Using a +Doctrine Cache instance is not supported anymore. + +* `DefaultCacheFactory`: The constructor expects a PSR-6 cache item pool as + second argument now. +* `DefaultMultiGetRegion`: This class has been removed. +* `DefaultRegion`: + * The constructor expects a PSR-6 cache item pool as second argument now. + * The protected `$cache` property is removed. + * The properties `$name` and `$lifetime` as well as the constant + `REGION_KEY_SEPARATOR` and the method `getCacheEntryKey()` are + `private` now. + * The method `getCache()` has been removed. + + +## BC Break: Remove `Doctrine\ORM\Mapping\Driver\PHPDriver` + +Use `StaticPHPDriver` instead when you want to programmatically configure +entity metadata. + +## BC BREAK: Remove `Doctrine\ORM\EntityManagerInterface#transactional()` + +This method has been replaced by `Doctrine\ORM\EntityManagerInterface#wrapInTransaction()`. + +## BC BREAK: Removed support for schema emulation. + +The ORM no longer attempts to emulate schemas on SQLite. + +## BC BREAK: Remove `Setup::registerAutoloadDirectory()` + +Use Composer's autoloader instead. + +## BC BREAK: Remove YAML mapping drivers. + +If your code relies on `YamlDriver` or `SimpleYamlDriver`, you **MUST** migrate to +attribute, annotation or XML drivers instead. + +You can use the `orm:convert-mapping` command to convert your metadata mapping to XML +_before_ upgrading to 3.0: + +```sh +php doctrine orm:convert-mapping xml /path/to/mapping-path-converted-to-xml +``` + +## BC BREAK: Remove code generators and related console commands + +These console commands have been removed: + +* `orm:convert-d1-schema` +* `orm:convert-mapping` +* `orm:generate:entities` +* `orm:generate-repositories` + +These classes have been deprecated: + +* `Doctrine\ORM\Tools\ConvertDoctrine1Schema` +* `Doctrine\ORM\Tools\EntityGenerator` +* `Doctrine\ORM\Tools\EntityRepositoryGenerator` + +The entire `Doctrine\ORM\Tools\Export` namespace has been removed as well. + +## BC BREAK: Removed `Doctrine\ORM\Version` + +Use Composer's runtime API if you _really_ need to check the version of the ORM package at runtime. + +## BC BREAK: EntityRepository::count() signature change + +The argument `$criteria` of `Doctrine\ORM\EntityRepository::count()` is now +optional. Overrides in child classes should be made compatible. + +## BC BREAK: changes in exception hierarchy + +- `Doctrine\ORM\ORMException` has been removed +- `Doctrine\ORM\Exception\ORMException` is now an interface + +## Variadic methods now use native variadics +The following methods were using `func_get_args()` to simulate a variadic argument: +- `Doctrine\ORM\Query\Expr#andX()` +- `Doctrine\ORM\Query\Expr#orX()` +- `Doctrine\ORM\QueryBuilder#select()` +- `Doctrine\ORM\QueryBuilder#addSelect()` +- `Doctrine\ORM\QueryBuilder#where()` +- `Doctrine\ORM\QueryBuilder#andWhere()` +- `Doctrine\ORM\QueryBuilder#orWhere()` +- `Doctrine\ORM\QueryBuilder#groupBy()` +- `Doctrine\ORM\QueryBuilder#andGroupBy()` +- `Doctrine\ORM\QueryBuilder#having()` +- `Doctrine\ORM\QueryBuilder#andHaving()` +- `Doctrine\ORM\QueryBuilder#orHaving()` +A variadic argument is now actually used in their signatures signature (`...$x`). +Signatures of overridden methods should be changed accordingly + +## Minor BC BREAK: removed `Doctrine\ORM\EntityManagerInterface#copy()` + +Method `Doctrine\ORM\EntityManagerInterface#copy()` never got its implementation and is removed in 3.0. + +## BC BREAK: Removed classes related to UUID and TABLE generator strategies + +The following classes have been removed: +- `Doctrine\ORM\Id\TableGenerator` +- `Doctrine\ORM\Id\UuidGenerator` + +Using the `UUID` strategy for generating identifiers is not supported anymore. + +## BC BREAK: Removed `Query::iterate()` + +The deprecated method `Query::iterate()` has been removed along with the +following classes and methods: + +- `AbstractHydrator::iterate()` +- `AbstractHydrator::hydrateRow()` +- `IterableResult` + +Use `toIterable()` instead. + +# Upgrade to 2.19 + +## Deprecate calling `ClassMetadata::getAssociationMappedByTargetField()` with the owning side of an association + +Calling +`Doctrine\ORM\Mapping\ClassMetadata::getAssociationMappedByTargetField()` with +the owning side of an association returns `null`, which is undocumented, and +wrong according to the phpdoc of the parent method. + +If you do not know whether you are on the owning or inverse side of an association, +you can use `Doctrine\ORM\Mapping\ClassMetadata::isAssociationInverseSide()` +to find out. + +## Deprecate `Doctrine\ORM\Query\Lexer::T_*` constants + +Use `Doctrine\ORM\Query\TokenType::T_*` instead. + +# Upgrade to 2.17 + +## Deprecate annotations classes for named queries + +The following classes have been deprecated: + +* `Doctrine\ORM\Mapping\NamedNativeQueries` +* `Doctrine\ORM\Mapping\NamedNativeQuery` +* `Doctrine\ORM\Mapping\NamedQueries` +* `Doctrine\ORM\Mapping\NamedQuery` + +## Deprecate `Doctrine\ORM\Query\Exec\AbstractSqlExecutor::_sqlStatements` + +Use `Doctrine\ORM\Query\Exec\AbstractSqlExecutor::sqlStatements` instead. + +## Undeprecate `Doctrine\ORM\Proxy\Autoloader` + +It will be a full-fledged class, no longer extending +`Doctrine\Common\Proxy\Autoloader` in 3.0.x. + +## Deprecated: reliance on the non-optimal defaults that come with the `AUTO` identifier generation strategy + +When the `AUTO` identifier generation strategy was introduced, the best +strategy at the time was selected for each database platform. +A lot of time has passed since then, and with ORM 3.0.0 and DBAL 4.0.0, support +for better strategies will be added. + +Because of that, it is now deprecated to rely on the historical defaults when +they differ from what we will be recommended in the future. + +Instead, you should pick a strategy for each database platform you use, and it +will be used when using `AUTO`. As of now, only PostgreSQL is affected by this. + +It is recommended that PostgreSQL users configure their existing and new +applications to use `SEQUENCE` until `doctrine/dbal` 4.0.0 is released: + +```php +use Doctrine\DBAL\Platforms\PostgreSQLPlatform; +use Doctrine\ORM\Configuration; + +assert($configuration instanceof Configuration); +$configuration->setIdentityGenerationPreferences([ + PostgreSQLPlatform::CLASS => ClassMetadata::GENERATOR_TYPE_SEQUENCE, +]); +``` + +When DBAL 4 is released, `AUTO` will result in `IDENTITY`, and the above +configuration should be removed to migrate to it. + +## Deprecate `EntityManagerInterface::getPartialReference()` + +This method does not have a replacement and will be removed in 3.0. + +## Deprecate not-enabling lazy-ghosts + +Not enabling lazy ghost objects is deprecated. In ORM 3.0, they will be always enabled. +Ensure `Doctrine\ORM\Configuration::setLazyGhostObjectEnabled(true)` is called to enable them. + +# Upgrade to 2.16 + +## Deprecated accepting duplicate IDs in the identity map + +For any given entity class and ID value, there should be only one object instance +representing the entity. + +In https://github.com/doctrine/orm/pull/10785, a check was added that will guard this +in the identity map. The most probable cause for violations of this rule are collisions +of application-provided IDs. + +In ORM 2.16.0, the check was added by throwing an exception. In ORM 2.16.1, this will be +changed to a deprecation notice. ORM 3.0 will make it an exception again. Use +`\Doctrine\ORM\Configuration::setRejectIdCollisionInIdentityMap()` if you want to opt-in +to the new mode. + +## Potential changes to the order in which `INSERT`s are executed + +In https://github.com/doctrine/orm/pull/10547, the commit order computation was improved +to fix a series of bugs where a correct (working) commit order was previously not found. +Also, the new computation may get away with fewer queries being executed: By inserting +referred-to entities first and using their ID values for foreign key fields in subsequent +`INSERT` statements, additional `UPDATE` statements that were previously necessary can be +avoided. + +When using database-provided, auto-incrementing IDs, this may lead to IDs being assigned +to entities in a different order than it was previously the case. + +## Deprecated returning post insert IDs from `EntityPersister::executeInserts()` + +Persisters implementing `\Doctrine\ORM\Persisters\Entity\EntityPersister` should no longer +return an array of post insert IDs from their `::executeInserts()` method. Make the +persister call `Doctrine\ORM\UnitOfWork::assignPostInsertId()` instead. + +## Changing the way how reflection-based mapping drivers report fields, deprecated the "old" mode + +In ORM 3.0, a change will be made regarding how the `AttributeDriver` reports field mappings. +This change is necessary to be able to detect (and reject) some invalid mapping configurations. + +To avoid surprises during 2.x upgrades, the new mode is opt-in. It can be activated on the +`AttributeDriver` and `AnnotationDriver` by setting the `$reportFieldsWhereDeclared` +constructor parameter to `true`. It will cause `MappingException`s to be thrown when invalid +configurations are detected. + +Not enabling the new mode will cause a deprecation notice to be raised. In ORM 3.0, +only the new mode will be available. + +# Upgrade to 2.15 + +## Deprecated configuring `JoinColumn` on the inverse side of one-to-one associations + +For one-to-one associations, the side using the `mappedBy` attribute is the inverse side. +The owning side is the entity with the table containing the foreign key. Using `JoinColumn` +configuration on the _inverse_ side now triggers a deprecation notice and will be an error +in 3.0. + +## Deprecated overriding fields or associations not declared in mapped superclasses + +As stated in the documentation, fields and associations may only be overridden when being inherited +from mapped superclasses. Overriding them for parent entity classes now triggers a deprecation notice +and will be an error in 3.0. + +## Deprecated undeclared entity inheritance + +As soon as an entity class inherits from another entity class, inheritance has to +be declared by adding the appropriate configuration for the root entity. + +## Deprecated stubs for "concrete table inheritance" + +This third way of mapping class inheritance was never implemented. Code stubs are +now deprecated and will be removed in 3.0. + +* `\Doctrine\ORM\Mapping\ClassMetadataInfo::INHERITANCE_TYPE_TABLE_PER_CLASS` constant +* `\Doctrine\ORM\Mapping\ClassMetadataInfo::isInheritanceTypeTablePerClass()` method +* Using `TABLE_PER_CLASS` as the value for the `InheritanceType` attribute or annotation + or in XML configuration files. + +# Upgrade to 2.14 + +## Deprecated `Doctrine\ORM\Persisters\Exception\UnrecognizedField::byName($field)` method. + +Use `Doctrine\ORM\Persisters\Exception\UnrecognizedField::byFullyQualifiedName($className, $field)` instead. + +## Deprecated constants of `Doctrine\ORM\Internal\CommitOrderCalculator` + +The following public constants have been deprecated: + +* `CommitOrderCalculator::NOT_VISITED` +* `CommitOrderCalculator::IN_PROGRESS` +* `CommitOrderCalculator::VISITED` + +These constants were used for internal purposes. Relying on them is discouraged. + +## Deprecated `Doctrine\ORM\Query\AST\InExpression` + +The AST parser will create a `InListExpression` or a `InSubselectExpression` when +encountering an `IN ()` DQL expression instead of a generic `InExpression`. + +As a consequence, `SqlWalker::walkInExpression()` has been deprecated in favor of +`SqlWalker::walkInListExpression()` and `SqlWalker::walkInSubselectExpression()`. + +## Deprecated constructing a `CacheKey` without `$hash` + +The `Doctrine\ORM\Cache\CacheKey` class has an explicit constructor now with +an optional parameter `$hash`. That parameter will become mandatory in 3.0. + +## Deprecated `AttributeDriver::$entityAnnotationClasses` + +If you need to change the behavior of `AttributeDriver::isTransient()`, +override that method instead. + +## Deprecated incomplete schema updates + +Using `orm:schema-tool:update` without passing the `--complete` flag is +deprecated. Use schema asset filtering if you need to preserve assets not +managed by DBAL. + +Likewise, calling `SchemaTool::updateSchema()` or +`SchemaTool::getUpdateSchemaSql()` with a second argument is deprecated. + +## Deprecated annotation mapping driver. + +Please switch to one of the other mapping drivers. Native attributes which PHP +supports since version 8.0 are probably your best option. + +As a consequence, the following methods are deprecated: +- `ORMSetup::createAnnotationMetadataConfiguration` +- `ORMSetup::createDefaultAnnotationDriver` + +The marker interface `Doctrine\ORM\Mapping\Annotation` is deprecated as well. +All annotation/attribute classes implement +`Doctrine\ORM\Mapping\MappingAttribute` now. + +## Deprecated `Doctrine\ORM\Proxy\Proxy` interface. + +Use `Doctrine\Persistence\Proxy` instead to check whether proxies are initialized. + +## Deprecated `Doctrine\ORM\Event\LifecycleEventArgs` class. + +It will be removed in 3.0. Use one of the dedicated event classes instead: + +* `Doctrine\ORM\Event\PrePersistEventArgs` +* `Doctrine\ORM\Event\PreUpdateEventArgs` +* `Doctrine\ORM\Event\PreRemoveEventArgs` +* `Doctrine\ORM\Event\PostPersistEventArgs` +* `Doctrine\ORM\Event\PostUpdateEventArgs` +* `Doctrine\ORM\Event\PostRemoveEventArgs` +* `Doctrine\ORM\Event\PostLoadEventArgs` + +# Upgrade to 2.13 + +## Deprecated `EntityManager::create()` + +The constructor of `EntityManager` is now public and should be used instead of the `create()` method. +However, the constructor expects a `Connection` while `create()` accepted an array with connection parameters. +You can pass that array to DBAL's `Doctrine\DBAL\DriverManager::getConnection()` method to bootstrap the +connection. + +## Deprecated `QueryBuilder` methods and constants. + +1. The `QueryBuilder::getState()` method has been deprecated as the builder state is an internal concern. +2. Relying on the type of the query being built by using `QueryBuilder::getType()` has been deprecated. + If necessary, track the type of the query being built outside of the builder. + +The following `QueryBuilder` constants related to the above methods have been deprecated: + +1. `SELECT`, +2. `DELETE`, +3. `UPDATE`, +4. `STATE_DIRTY`, +5. `STATE_CLEAN`. + +## Deprecated omitting only the alias argument for `QueryBuilder::update` and `QueryBuilder::delete` + +When building an UPDATE or DELETE query and when passing a class/type to the function, the alias argument must not be omitted. + +### Before + +```php +$qb = $em->createQueryBuilder() + ->delete('User u') + ->where('u.id = :user_id') + ->setParameter('user_id', 1); +``` + +### After + +```php +$qb = $em->createQueryBuilder() + ->delete('User', 'u') + ->where('u.id = :user_id') + ->setParameter('user_id', 1); +``` + +## Deprecated using the `IDENTITY` identifier strategy on platform that do not support identity columns + +If identity columns are emulated with sequences on the platform you are using, +you should switch to the `SEQUENCE` strategy. + +## Deprecated passing `null` to `Doctrine\ORM\Query::setFirstResult()` + +`$query->setFirstResult(null);` is equivalent to `$query->setFirstResult(0)`. + +## Deprecated calling setters without arguments + +The following methods will require an argument in 3.0. Pass `null` instead of +omitting the argument. + +* `Doctrine\ORM\Event\OnClassMetadataNotFoundEventArgs::setFoundMetadata()` +* `Doctrine\ORM\AbstractQuery::setHydrationCacheProfile()` +* `Doctrine\ORM\AbstractQuery::setResultCache()` +* `Doctrine\ORM\AbstractQuery::setResultCacheProfile()` + +## Deprecated passing invalid fetch modes to `AbstractQuery::setFetchMode()` + +Calling `AbstractQuery::setFetchMode()` with anything else than +`Doctrine\ORM\Mapping::FETCH_EAGER` results in +`Doctrine\ORM\Mapping::FETCH_LAZY` being used. Relying on that behavior is +deprecated and will result in an exception in 3.0. + +## Deprecated `getEntityManager()` in `Doctrine\ORM\Event\OnClearEventArgs` and `Doctrine\ORM\Event\*FlushEventArgs` + +This method has been deprecated in: + +* `Doctrine\ORM\Event\OnClearEventArgs` +* `Doctrine\ORM\Event\OnFlushEventArgs` +* `Doctrine\ORM\Event\PostFlushEventArgs` +* `Doctrine\ORM\Event\PreFlushEventArgs` + +It will be removed in 3.0. Use `getObjectManager()` instead. + +## Prepare split of output walkers and tree walkers + +In 3.0, `SqlWalker` and its child classes won't implement the `TreeWalker` +interface anymore. Relying on that inheritance is deprecated. + +The following methods of the `TreeWalker` interface have been deprecated: + +* `setQueryComponent()` +* `walkSelectClause()` +* `walkFromClause()` +* `walkFunction()` +* `walkOrderByClause()` +* `walkOrderByItem()` +* `walkHavingClause()` +* `walkJoin()` +* `walkSelectExpression()` +* `walkQuantifiedExpression()` +* `walkSubselect()` +* `walkSubselectFromClause()` +* `walkSimpleSelectClause()` +* `walkSimpleSelectExpression()` +* `walkAggregateExpression()` +* `walkGroupByClause()` +* `walkGroupByItem()` +* `walkDeleteClause()` +* `walkUpdateClause()` +* `walkUpdateItem()` +* `walkWhereClause()` +* `walkConditionalExpression()` +* `walkConditionalTerm()` +* `walkConditionalFactor()` +* `walkConditionalPrimary()` +* `walkExistsExpression()` +* `walkCollectionMemberExpression()` +* `walkEmptyCollectionComparisonExpression()` +* `walkNullComparisonExpression()` +* `walkInExpression()` +* `walkInstanceOfExpression()` +* `walkLiteral()` +* `walkBetweenExpression()` +* `walkLikeExpression()` +* `walkStateFieldPathExpression()` +* `walkComparisonExpression()` +* `walkInputParameter()` +* `walkArithmeticExpression()` +* `walkArithmeticTerm()` +* `walkStringPrimary()` +* `walkArithmeticFactor()` +* `walkSimpleArithmeticExpression()` +* `walkPathExpression()` +* `walkResultVariable()` +* `getExecutor()` + +The following changes have been made to the abstract `TreeWalkerAdapter` class: + +* All implementations of now-deprecated `TreeWalker` methods have been + deprecated as well. +* The method `setQueryComponent()` will become protected in 3.0. Calling it + publicly is deprecated. +* The method `_getQueryComponents()` is deprecated, call `getQueryComponents()` + instead. + +On the `TreeWalkerChain` class, all implementations of now-deprecated +`TreeWalker` methods have been deprecated as well. However, `SqlWalker` is +unaffected by those deprecations and will continue to implement all of those +methods. + +## Deprecated passing `null` to `Doctrine\ORM\Query::setDQL()` + +Doing `$query->setDQL(null);` achieves nothing. + +## Deprecated omitting second argument to `NamingStrategy::joinColumnName` + +When implementing `NamingStrategy`, it is deprecated to implement +`joinColumnName()` with only one argument. + +### Before + +```php +getConfiguration(); +-$config->addEntityNamespace('CMS', 'My\App\Cms'); ++use My\App\Cms\CmsUser; + +-$entityManager->getRepository('CMS:CmsUser'); ++$entityManager->getRepository(CmsUser::class); +``` + +## Deprecate `AttributeDriver::getReader()` and `AnnotationDriver::getReader()` + +That method was inherited from the abstract `AnnotationDriver` class of +`doctrine/persistence`, and does not seem to serve any purpose. + +## Un-deprecate `Doctrine\ORM\Proxy\Proxy` + +Because no forward-compatible new proxy solution had been implemented yet, the +current proxy mechanism is not considered deprecated anymore for the time +being. This applies to the following interfaces/classes: + +* `Doctrine\ORM\Proxy\Proxy` +* `Doctrine\ORM\Proxy\ProxyFactory` + +These methods have been un-deprecated: + +* `Doctrine\ORM\Configuration::getAutoGenerateProxyClasses()` +* `Doctrine\ORM\Configuration::getProxyDir()` +* `Doctrine\ORM\Configuration::getProxyNamespace()` + +Note that the `Doctrine\ORM\Proxy\Autoloader` remains deprecated and will be removed in 3.0. + +## Deprecate helper methods from `AbstractCollectionPersister` + +The following protected methods of +`Doctrine\ORM\Cache\Persister\Collection\AbstractCollectionPersister` +are not in use anymore and will be removed. + +* `evictCollectionCache()` +* `evictElementCache()` + +## Deprecate `Doctrine\ORM\Query\TreeWalkerChainIterator` + +This class won't have a replacement. + +## Deprecate `OnClearEventArgs::getEntityClass()` and `OnClearEventArgs::clearsAllEntities()` + +These methods will be removed in 3.0 along with the ability to partially clear +the entity manager. + +## Deprecate `Doctrine\ORM\Configuration::newDefaultAnnotationDriver` + +This functionality has been moved to the new `ORMSetup` class. Call +`Doctrine\ORM\ORMSetup::createDefaultAnnotationDriver()` to create +a new annotation driver. + +## Deprecate `Doctrine\ORM\Tools\Setup` + +In our effort to migrate from Doctrine Cache to PSR-6, the `Setup` class which +accepted a Doctrine Cache instance in each method has been deprecated. + +The replacement is `Doctrine\ORM\ORMSetup` which accepts a PSR-6 +cache instead. + +## Deprecate `Doctrine\ORM\Cache\MultiGetRegion` + +The interface will be merged with `Doctrine\ORM\Cache\Region` in 3.0. + +# Upgrade to 2.11 + +## Rename `AbstractIdGenerator::generate()` to `generateId()` + +Implementations of `AbstractIdGenerator` have to override the method +`generateId()` without calling the parent implementation. Not doing so is +deprecated. Calling `generate()` on any `AbstractIdGenerator` implementation +is deprecated. + +## PSR-6-based second level cache + +The second level cache has been reworked to consume a PSR-6 cache. Using a +Doctrine Cache instance is deprecated. + +* `DefaultCacheFactory`: The constructor expects a PSR-6 cache item pool as + second argument now. +* `DefaultMultiGetRegion`: This class is deprecated in favor of `DefaultRegion`. +* `DefaultRegion`: + * The constructor expects a PSR-6 cache item pool as second argument now. + * The protected `$cache` property is deprecated. + * The properties `$name` and `$lifetime` as well as the constant + `REGION_KEY_SEPARATOR` and the method `getCacheEntryKey()` are flagged as + `@internal` now. They all will become `private` in 3.0. + * The method `getCache()` is deprecated without replacement. + +## Deprecated: `Doctrine\ORM\Mapping\Driver\PHPDriver` + +Use `StaticPHPDriver` instead when you want to programmatically configure +entity metadata. + +You can convert mappings with the `orm:convert-mapping` command or more simply +in this case, `include` the metadata file from the `loadMetadata` static method +used by the `StaticPHPDriver`. + +## Deprecated: `Setup::registerAutoloadDirectory()` + +Use Composer's autoloader instead. + +## Deprecated: `AbstractHydrator::hydrateRow()` + +Following the deprecation of the method `AbstractHydrator::iterate()`, the +method `hydrateRow()` has been deprecated as well. + +## Deprecate cache settings inspection + +Doctrine does not provide its own cache implementation anymore and relies on +the PSR-6 standard instead. As a consequence, we cannot determine anymore +whether a given cache adapter is suitable for a production environment. +Because of that, functionality that aims to do so has been deprecated: + +* `Configuration::ensureProductionSettings()` +* the `orm:ensure-production-settings` console command + +# Upgrade to 2.10 + +## BC Break: `UnitOfWork` now relies on SPL object IDs, not hashes + +When calling the following methods, you are now supposed to use the result of +`spl_object_id()`, and not `spl_object_hash()`: +- `UnitOfWork::clearEntityChangeSet()` +- `UnitOfWork::setOriginalEntityProperty()` + +## BC Break: Removed `TABLE` id generator strategy + +The implementation was unfinished for 14 years. +It is now deprecated to rely on: +- `Doctrine\ORM\Id\TableGenerator`; +- `Doctrine\ORM\Mapping\ClassMetadata::GENERATOR_TYPE_TABLE`; +- `Doctrine\ORM\Mapping\ClassMetadata::$tableGeneratorDefinition`; +- or `Doctrine\ORM\Mapping\ClassMetadata::isIdGeneratorTable()`. + +## New method `Doctrine\ORM\EntityManagerInterface#wrapInTransaction($func)` + +Works the same as `Doctrine\ORM\EntityManagerInterface#transactional()` but returns any value returned from `$func` closure rather than just _non-empty value returned from the closure or true_. + +Because of BC policy, the method does not exist on the interface yet. This is the example of safe usage: + +```php +function foo(EntityManagerInterface $entityManager, callable $func) { + if (method_exists($entityManager, 'wrapInTransaction')) { + return $entityManager->wrapInTransaction($func); + } + + return $entityManager->transactional($func); +} +``` + +`Doctrine\ORM\EntityManagerInterface#transactional()` has been deprecated. + +## Minor BC BREAK: some exception methods have been removed + +The following methods were not in use and are very unlikely to be used by +downstream packages or applications, and were consequently removed: + +- `ORMException::entityMissingForeignAssignedId` +- `ORMException::entityMissingAssignedIdForField` +- `ORMException::invalidFlushMode` + +## Deprecated: database-side UUID generation + +[DB-generated UUIDs are deprecated as of `doctrine/dbal` 2.8][DBAL deprecation]. +As a consequence, using the `UUID` strategy for generating identifiers is deprecated as well. +Furthermore, relying on the following classes and methods is deprecated: + +- `Doctrine\ORM\Id\UuidGenerator` +- `Doctrine\ORM\Mapping\ClassMetadataInfo::isIdentifierUuid()` + +[DBAL deprecation]: https://github.com/doctrine/dbal/pull/3212 + +## Minor BC BREAK: Custom hydrators and `toIterable()` + +The type declaration of the `$stmt` parameter of `AbstractHydrator::toIterable()` has been removed. This change might +break custom hydrator implementations that override this very method. + +Overriding this method is not recommended, which is why the method is documented as `@final` now. + +```diff +- public function toIterable(ResultStatement $stmt, ResultSetMapping $resultSetMapping, array $hints = []): iterable ++ public function toIterable($stmt, ResultSetMapping $resultSetMapping, array $hints = []): iterable +``` + +## Deprecated: Entity Namespace Aliases + +Entity namespace aliases are deprecated, use the magic ::class constant to abbreviate full class names +in EntityManager, EntityRepository and DQL. + +```diff +- $entityManager->find('MyBundle:User', $id); ++ $entityManager->find(User::class, $id); +``` + +# Upgrade to 2.9 + +## Minor BC BREAK: Setup tool needs cache implementation + +With the deprecation of doctrine/cache, the setup tool might no longer work as expected without a different cache +implementation. To work around this: +* Install symfony/cache: `composer require symfony/cache`. This will keep previous behaviour without any changes +* Instantiate caches yourself: to use a different cache implementation, pass a cache instance when calling any + configuration factory in the setup tool: + ```diff + - $config = Setup::createAnnotationMetadataConfiguration($paths, $isDevMode, $proxyDir); + + $cache = \Doctrine\Common\Cache\Psr6\DoctrineProvider::wrap($anyPsr6Implementation); + + $config = Setup::createAnnotationMetadataConfiguration($paths, $isDevMode, $proxyDir, $cache); + ``` +* As a quick workaround, you can lock the doctrine/cache dependency to work around this: `composer require doctrine/cache ^1.11`. + Note that this is only recommended as a bandaid fix, as future versions of ORM will no longer work with doctrine/cache + 1.11. + +## Deprecated: doctrine/cache for metadata caching + +The `Doctrine\ORM\Configuration#setMetadataCacheImpl()` method is deprecated and should no longer be used. Please use +`Doctrine\ORM\Configuration#setMetadataCache()` with any PSR-6 cache adapter instead. + +## Removed: flushing metadata cache + +To support PSR-6 caches, the `--flush` option for the `orm:clear-cache:metadata` command is ignored. Metadata cache is +now always cleared regardless of the cache adapter being used. + +# Upgrade to 2.8 + +## Minor BC BREAK: Failed commit now throw OptimisticLockException + +Method `Doctrine\ORM\UnitOfWork#commit()` can throw an OptimisticLockException when a commit silently fails and returns false +since `Doctrine\DBAL\Connection#commit()` signature changed from returning void to boolean + +## Deprecated: `Doctrine\ORM\AbstractQuery#iterate()` + +The method `Doctrine\ORM\AbstractQuery#iterate()` is deprecated in favor of `Doctrine\ORM\AbstractQuery#toIterable()`. +Note that `toIterable()` yields results of the query, unlike `iterate()` which yielded each result wrapped into an array. + +# Upgrade to 2.7 + +## Added `Doctrine\ORM\AbstractQuery#enableResultCache()` and `Doctrine\ORM\AbstractQuery#disableResultCache()` methods + +Method `Doctrine\ORM\AbstractQuery#useResultCache()` which could be used for both enabling and disabling the cache +(depending on passed flag) was split into two. + +## Minor BC BREAK: paginator output walkers aren't be called anymore on sub-queries for queries without max results + +To optimize DB interaction, `Doctrine\ORM\Tools\Pagination\Paginator` no longer fetches identifiers to be able to +perform the pagination with join collections when max results isn't set in the query. + +## Minor BC BREAK: tables filtered with `schema_filter` are no longer created + +When generating schema diffs, if a source table is filtered out by a `schema_filter` expression, then a `CREATE TABLE` was +always generated, even if the table already existed. This has been changed in this release and the table will no longer +be created. + +## Deprecated number unaware `Doctrine\ORM\Mapping\UnderscoreNamingStrategy` + +In the last patch of the `v2.6.x` series, we fixed a bug that was not converting names properly when they had numbers +(e.g.: `base64Encoded` was wrongly converted to `base64encoded` instead of `base64_encoded`). + +In order to not break BC we've introduced a way to enable the fixed behavior using a boolean constructor argument. This +argument will be removed in 3.0 and the default behavior will be the fixed one. + +## Deprecated: `Doctrine\ORM\AbstractQuery#useResultCache()` + +Method `Doctrine\ORM\AbstractQuery#useResultCache()` is deprecated because it is split into `enableResultCache()` +and `disableResultCache()`. It will be removed in 3.0. + +## Deprecated code generators and related console commands + +These console commands have been deprecated: + + * `orm:convert-mapping` + * `orm:generate:entities` + * `orm:generate-repositories` + +These classes have been deprecated: + + * `Doctrine\ORM\Tools\EntityGenerator` + * `Doctrine\ORM\Tools\EntityRepositoryGenerator` + +Whole Doctrine\ORM\Tools\Export namespace with all its members have been deprecated as well. + +## Deprecated `Doctrine\ORM\Proxy\Proxy` marker interface + +Proxy objects in Doctrine ORM 3.0 will no longer implement `Doctrine\ORM\Proxy\Proxy` nor +`Doctrine\Persistence\Proxy`: instead, they implement +`ProxyManager\Proxy\GhostObjectInterface`. + +These related classes have been deprecated: + + * `Doctrine\ORM\Proxy\ProxyFactory` + * `Doctrine\ORM\Proxy\Autoloader` - we suggest using the composer autoloader instead + +These methods have been deprecated: + + * `Doctrine\ORM\Configuration#getAutoGenerateProxyClasses()` + * `Doctrine\ORM\Configuration#getProxyDir()` + * `Doctrine\ORM\Configuration#getProxyNamespace()` + +## Deprecated `Doctrine\ORM\Version` + +The `Doctrine\ORM\Version` class is now deprecated and will be removed in Doctrine ORM 3.0: +please refrain from checking the ORM version at runtime or use Composer's [runtime API](https://getcomposer.org/doc/07-runtime.md#knowing-whether-package-x-is-installed-in-version-y). + +## Deprecated `EntityManager#merge()` method + +Merge semantics was a poor fit for the PHP "share-nothing" architecture. +In addition to that, merging caused multiple issues with data integrity +in the managed entity graph, which was constantly spawning more edge-case bugs/scenarios. + +The following API methods were therefore deprecated: + +* `EntityManager#merge()` +* `UnitOfWork#merge()` + +An alternative to `EntityManager#merge()` will not be provided by ORM 3.0, since the merging +semantics should be part of the business domain rather than the persistence domain of an +application. If your application relies heavily on CRUD-alike interactions and/or `PATCH` +restful operations, you should look at alternatives such as [JMSSerializer](https://github.com/schmittjoh/serializer). + +## Extending `EntityManager` is deprecated + +Final keyword will be added to the `EntityManager::class` in Doctrine ORM 3.0 in order to ensure that EntityManager + is not used as valid extension point. Valid extension point should be EntityManagerInterface. + +## Deprecated `EntityManager#clear($entityName)` + +If your code relies on clearing a single entity type via `EntityManager#clear($entityName)`, +the signature has been changed to `EntityManager#clear()`. + +The main reason is that partial clears caused multiple issues with data integrity +in the managed entity graph, which was constantly spawning more edge-case bugs/scenarios. + +## Deprecated `EntityManager#flush($entity)` and `EntityManager#flush($entities)` + +If your code relies on single entity flushing optimisations via +`EntityManager#flush($entity)`, the signature has been changed to +`EntityManager#flush()`. + +Said API was affected by multiple data integrity bugs due to the fact +that change tracking was being restricted upon a subset of the managed +entities. The ORM cannot support committing subsets of the managed +entities while also guaranteeing data integrity, therefore this +utility was removed. + +The `flush()` semantics will remain the same, but the change tracking will be performed +on all entities managed by the unit of work, and not just on the provided +`$entity` or `$entities`, as the parameter is now completely ignored. + +The same applies to `UnitOfWork#commit($entity)`, which will simply be +`UnitOfWork#commit()`. + +If you would still like to perform batching operations over small `UnitOfWork` +instances, it is suggested to follow these paths instead: + + * eagerly use `EntityManager#clear()` in conjunction with a specific second level + cache configuration (see http://docs.doctrine-project.org/projects/doctrine-orm/en/stable/reference/second-level-cache.html) + * use an explicit change tracking policy (see http://docs.doctrine-project.org/projects/doctrine-orm/en/stable/reference/change-tracking-policies.html) + +## Deprecated `YAML` mapping drivers. + +If your code relies on `YamlDriver` or `SimpleYamlDriver`, you **MUST** change to +annotation or XML drivers instead. + +## Deprecated: `Doctrine\ORM\EntityManagerInterface#copy()` + +Method `Doctrine\ORM\EntityManagerInterface#copy()` never got its implementation and is deprecated. +It will be removed in 3.0. + +# Upgrade to 2.6 + +## Added `Doctrine\ORM\EntityRepository::count()` method + +`Doctrine\ORM\EntityRepository::count()` has been added. This new method has different +signature than `Countable::count()` (required parameter) and therefore are not compatible. +If your repository implemented the `Countable` interface, you will have to use +`$repository->count([])` instead and not implement `Countable` interface anymore. + +## Minor BC BREAK: `Doctrine\ORM\Tools\Console\ConsoleRunner` is now final + +Since it's just an utilitarian class and should not be inherited. + +## Minor BC BREAK: removed `Doctrine\ORM\Query\QueryException::associationPathInverseSideNotSupported()` + +Method `Doctrine\ORM\Query\QueryException::associationPathInverseSideNotSupported()` +now has a required parameter `$pathExpr`. + +## Minor BC BREAK: removed `Doctrine\ORM\Query\Parser#isInternalFunction()` + +Method `Doctrine\ORM\Query\Parser#isInternalFunction()` was removed because +the distinction between internal function and user defined DQL was removed. +[#6500](https://github.com/doctrine/orm/pull/6500) + +## Minor BC BREAK: removed `Doctrine\ORM\ORMException#overwriteInternalDQLFunctionNotAllowed()` + +Method `Doctrine\ORM\Query\Parser#overwriteInternalDQLFunctionNotAllowed()` was +removed because of the choice to allow users to overwrite internal functions, ie +`AVG`, `SUM`, `COUNT`, `MIN` and `MAX`. [#6500](https://github.com/doctrine/orm/pull/6500) + +## PHP 7.1 is now required + +Doctrine 2.6 now requires PHP 7.1 or newer. + +As a consequence, automatic cache setup in Doctrine\ORM\Tools\Setup::create*Configuration() was changed: +- APCu extension (ext-apcu) will now be used instead of abandoned APC (ext-apc). +- Memcached extension (ext-memcached) will be used instead of obsolete Memcache (ext-memcache). +- XCache support was dropped as it doesn't work with PHP 7. + +# Upgrade to 2.5 + +## Minor BC BREAK: removed `Doctrine\ORM\Query\SqlWalker#walkCaseExpression()` + +Method `Doctrine\ORM\Query\SqlWalker#walkCaseExpression()` was unused and part +of the internal API of the ORM, so it was removed. [#5600](https://github.com/doctrine/orm/pull/5600). + +## Minor BC BREAK: removed $className parameter on `AbstractEntityInheritancePersister#getSelectJoinColumnSQL()` + +As `$className` parameter was not used in the method, it was safely removed. + +## Minor BC BREAK: query cache key time is now a float + +As of 2.5.5, the `QueryCacheEntry#time` property will contain a float value +instead of an integer in order to have more precision and also to be consistent +with the `TimestampCacheEntry#time`. + +## Minor BC BREAK: discriminator map must now include all non-transient classes + +It is now required that you declare the root of an inheritance in the +discriminator map. + +When declaring an inheritance map, it was previously possible to skip the root +of the inheritance in the discriminator map. This was actually a validation +mistake by Doctrine2 and led to problems when trying to persist instances of +that class. + +If you don't plan to persist instances some classes in your inheritance, then +either: + + - make those classes `abstract` + - map those classes as `MappedSuperclass` + +## Minor BC BREAK: ``EntityManagerInterface`` instead of ``EntityManager`` in type-hints + +As of 2.5, classes requiring the ``EntityManager`` in any method signature will now require +an ``EntityManagerInterface`` instead. +If you are extending any of the following classes, then you need to check following +signatures: + +- ``Doctrine\ORM\Tools\DebugUnitOfWorkListener#dumpIdentityMap(EntityManagerInterface $em)`` +- ``Doctrine\ORM\Mapping\ClassMetadataFactory#setEntityManager(EntityManagerInterface $em)`` + +## Minor BC BREAK: Custom Hydrators API change + +As of 2.5, `AbstractHydrator` does not enforce the usage of cache as part of +API, and now provides you a clean API for column information through the method +`hydrateColumnInfo($column)`. +Cache variable being passed around by reference is no longer needed since +Hydrators are per query instantiated since Doctrine 2.4. + +## Minor BC BREAK: Entity based ``EntityManager#clear()`` calls follow cascade detach + +Whenever ``EntityManager#clear()`` method gets called with a given entity class +name, until 2.4, it was only detaching the specific requested entity. +As of 2.5, ``EntityManager`` will follow configured cascades, providing a better +memory management since associations will be garbage collected, optimizing +resources consumption on long running jobs. + +## BC BREAK: NamingStrategy interface changes + +1. A new method ``embeddedFieldToColumnName($propertyName, $embeddedColumnName)`` + +This method generates the column name for fields of embedded objects. If you implement your custom NamingStrategy, you +now also need to implement this new method. + +2. A change to method ``joinColumnName()`` to include the $className + +## Updates on entities scheduled for deletion are no longer processed + +In Doctrine 2.4, if you modified properties of an entity scheduled for deletion, UnitOfWork would +produce an UPDATE statement to be executed right before the DELETE statement. The entity in question +was therefore present in ``UnitOfWork#entityUpdates``, which means that ``preUpdate`` and ``postUpdate`` +listeners were (quite pointlessly) called. In ``preFlush`` listeners, it used to be possible to undo +the scheduled deletion for updated entities (by calling ``persist()`` if the entity was found in both +``entityUpdates`` and ``entityDeletions``). This does not work any longer, because the entire changeset +calculation logic is optimized away. + +## Minor BC BREAK: Default lock mode changed from LockMode::NONE to null in method signatures + +A misconception concerning default lock mode values in method signatures lead to unexpected behaviour +in SQL statements on SQL Server. With a default lock mode of ``LockMode::NONE`` throughout the +method signatures in ORM, the table lock hint ``WITH (NOLOCK)`` was appended to all locking related +queries by default. This could result in unpredictable results because an explicit ``WITH (NOLOCK)`` +table hint tells SQL Server to run a specific query in transaction isolation level READ UNCOMMITTED +instead of the default READ COMMITTED transaction isolation level. +Therefore there now is a distinction between ``LockMode::NONE`` and ``null`` to be able to tell +Doctrine whether to add table lock hints to queries by intention or not. To achieve this, the following +method signatures have been changed to declare ``$lockMode = null`` instead of ``$lockMode = LockMode::NONE``: + +- ``Doctrine\ORM\Cache\Persister\AbstractEntityPersister#getSelectSQL()`` +- ``Doctrine\ORM\Cache\Persister\AbstractEntityPersister#load()`` +- ``Doctrine\ORM\Cache\Persister\AbstractEntityPersister#refresh()`` +- ``Doctrine\ORM\Decorator\EntityManagerDecorator#find()`` +- ``Doctrine\ORM\EntityManager#find()`` +- ``Doctrine\ORM\EntityRepository#find()`` +- ``Doctrine\ORM\Persisters\BasicEntityPersister#getSelectSQL()`` +- ``Doctrine\ORM\Persisters\BasicEntityPersister#load()`` +- ``Doctrine\ORM\Persisters\BasicEntityPersister#refresh()`` +- ``Doctrine\ORM\Persisters\EntityPersister#getSelectSQL()`` +- ``Doctrine\ORM\Persisters\EntityPersister#load()`` +- ``Doctrine\ORM\Persisters\EntityPersister#refresh()`` +- ``Doctrine\ORM\Persisters\JoinedSubclassPersister#getSelectSQL()`` + +You should update signatures for these methods if you have subclassed one of the above classes. +Please also check the calling code of these methods in your application and update if necessary. + +**Note:** +This in fact is really a minor BC BREAK and should not have any affect on database vendors +other than SQL Server because it is the only one that supports and therefore cares about +``LockMode::NONE``. It's really just a FIX for SQL Server environments using ORM. + +## Minor BC BREAK: `__clone` method not called anymore when entities are instantiated via metadata API + +As of PHP 5.6, instantiation of new entities is deferred to the +[`doctrine/instantiator`](https://github.com/doctrine/instantiator) library, which will avoid calling `__clone` +or any public API on instantiated objects. + +## BC BREAK: `Doctrine\ORM\Repository\DefaultRepositoryFactory` is now `final` + +Please implement the `Doctrine\ORM\Repository\RepositoryFactory` interface instead of extending +the `Doctrine\ORM\Repository\DefaultRepositoryFactory`. + +## BC BREAK: New object expression DQL queries now respects user provided aliasing and not return consumed fields + +When executing DQL queries with new object expressions, instead of returning DTOs numerically indexes, it will now respect user provided aliases. Consider the following query: + + SELECT new UserDTO(u.id,u.name) as user,new AddressDTO(a.street,a.postalCode) as address, a.id as addressId FROM User u INNER JOIN u.addresses a WITH a.isPrimary = true + +Previously, your result would be similar to this: + + array( + 0=>array( + 0=>{UserDTO object}, + 1=>{AddressDTO object}, + 2=>{u.id scalar}, + 3=>{u.name scalar}, + 4=>{a.street scalar}, + 5=>{a.postalCode scalar}, + 'addressId'=>{a.id scalar}, + ), + ... + ) + +From now on, the resultset will look like this: + + array( + 0=>array( + 'user'=>{UserDTO object}, + 'address'=>{AddressDTO object}, + 'addressId'=>{a.id scalar} + ), + ... + ) + +## Minor BC BREAK: added second parameter $indexBy in EntityRepository#createQueryBuilder method signature + +Added way to access the underlying QueryBuilder#from() method's 'indexBy' parameter when using EntityRepository#createQueryBuilder() + +# Upgrade to 2.4 + +## BC BREAK: Compatibility Bugfix in PersistentCollection#matching() + +In Doctrine 2.3 it was possible to use the new ``matching($criteria)`` +functionality by adding constraints for assocations based on ID: + + Criteria::expr()->eq('association', $assocation->getId()); + +This functionality does not work on InMemory collections however, because +in memory criteria compares object values based on reference. +As of 2.4 the above code will throw an exception. You need to change +offending code to pass the ``$assocation`` reference directly: + + Criteria::expr()->eq('association', $assocation); + +## Composer is now the default autoloader + +The test suite now runs with composer autoloading. Support for PEAR, and tarball autoloading is deprecated. +Support for GIT submodules is removed. + +## OnFlush and PostFlush event always called + +Before 2.4 the postFlush and onFlush events were only called when there were +actually entities that changed. Now these events are called no matter if there +are entities in the UoW or changes are found. + +## Parenthesis are now considered in arithmetic expression + +Before 2.4 parenthesis are not considered in arithmetic primary expression. +That's conceptually wrong, since it might result in wrong values. For example: + +The DQL: + + SELECT 100 / ( 2 * 2 ) FROM MyEntity + +Before 2.4 it generates the SQL: + + SELECT 100 / 2 * 2 FROM my_entity + +Now parenthesis are considered, the previous DQL will generate: + + SELECT 100 / (2 * 2) FROM my_entity + +# Upgrade to 2.3 + +## Auto Discriminator Map breaks userland implementations with Listener + +The new feature to detect discriminator maps automatically when none +are provided breaks userland implementations doing this with a +listener in ``loadClassMetadata`` event. + +## EntityManager#find() not calls EntityRepository#find() anymore + +Previous to 2.3, calling ``EntityManager#find()`` would be delegated to +``EntityRepository#find()``. This has lead to some unexpected behavior in the +core of Doctrine when people have overwritten the find method in their +repositories. That is why this behavior has been reversed in 2.3, and +``EntityRepository#find()`` calls ``EntityManager#find()`` instead. + +## EntityGenerator add*() method generation + +When generating an add*() method for a collection the EntityGenerator will now not +use the Type-Hint to get the singular for the collection name, but use the field-name +and strip a trailing "s" character if there is one. + +## Merge copies non persisted properties too + +When merging an entity in UoW not only mapped properties are copied, but also others. + +## Query, QueryBuilder and NativeQuery parameters *BC break* + +From now on, parameters in queries is an ArrayCollection instead of a simple array. +This affects heavily the usage of setParameters(), because it will not append anymore +parameters to query, but will actually override the already defined ones. +Whenever you are retrieving a parameter (ie. $query->getParameter(1)), you will +receive an instance of Query\Parameter, which contains the methods "getName", +"getValue" and "getType". Parameters are also only converted to when necessary, and +not when they are set. + +Also, related functions were affected: + +* execute($parameters, $hydrationMode) the argument $parameters can be either an key=>value array or an ArrayCollection instance +* iterate($parameters, $hydrationMode) the argument $parameters can be either an key=>value array or an ArrayCollection instance +* setParameters($parameters) the argument $parameters can be either an key=>value array or an ArrayCollection instance +* getParameters() now returns ArrayCollection instead of array +* getParameter($key) now returns Parameter instance instead of parameter value + +## Query TreeWalker method renamed + +Internal changes were made to DQL and SQL generation. If you have implemented your own TreeWalker, +you probably need to update it. The method walkJoinVariableDeclaration is now named walkJoin. + +## New methods in TreeWalker interface *BC break* + +Two methods getQueryComponents() and setQueryComponent() were added to the TreeWalker interface and all its implementations +including TreeWalkerAdapter, TreeWalkerChain and SqlWalker. If you have your own implementation not inheriting from one of the +above you must implement these new methods. + +## Metadata Drivers + +Metadata drivers have been rewritten to reuse code from `Doctrine\Persistence`. Anyone who is using the +`Doctrine\ORM\Mapping\Driver\Driver` interface should instead refer to +`Doctrine\Persistence\Mapping\Driver\MappingDriver`. Same applies to +`Doctrine\ORM\Mapping\Driver\AbstractFileDriver`: you should now refer to +`Doctrine\Persistence\Mapping\Driver\FileDriver`. + +Also, following mapping drivers have been deprecated, please use their replacements in Doctrine\Common as listed: + + * `Doctrine\ORM\Mapping\Driver\DriverChain` => `Doctrine\Persistence\Mapping\Driver\MappingDriverChain` + * `Doctrine\ORM\Mapping\Driver\PHPDriver` => `Doctrine\Persistence\Mapping\Driver\PHPDriver` + * `Doctrine\ORM\Mapping\Driver\StaticPHPDriver` => `Doctrine\Persistence\Mapping\Driver\StaticPHPDriver` + +# Upgrade to 2.2 + +## ResultCache implementation rewritten + +The result cache is completely rewritten and now works on the database result level, not inside the ORM AbstractQuery +anymore. This means that for result cached queries the hydration will now always be performed again, regardless of +the hydration mode. Affected areas are: + +1. Fixes the problem that entities coming from the result cache were not registered in the UnitOfWork + leading to problems during EntityManager#flush. Calls to EntityManager#merge are not necessary anymore. +2. Affects the array hydrator which now includes the overhead of hydration compared to caching the final result. + +The API is backwards compatible however most of the getter methods on the `AbstractQuery` object are now +deprecated in favor of calling AbstractQuery#getQueryCacheProfile(). This method returns a `Doctrine\DBAL\Cache\QueryCacheProfile` +instance with access to result cache driver, lifetime and cache key. + + +## EntityManager#getPartialReference() creates read-only entity + +Entities returned from EntityManager#getPartialReference() are now marked as read-only if they +haven't been in the identity map before. This means objects of this kind never lead to changes +in the UnitOfWork. + + +## Fields omitted in a partial DQL query or a native query are never updated + +Fields of an entity that are not returned from a partial DQL Query or native SQL query +will never be updated through an UPDATE statement. + + +## Removed support for onUpdate in @JoinColumn + +The onUpdate foreign key handling makes absolutely no sense in an ORM. Additionally Oracle doesn't even support it. Support for it is removed. + + +## Changes in Annotation Handling + +There have been some changes to the annotation handling in Common 2.2 again, that affect how people with old configurations +from 2.0 have to configure the annotation driver if they don't use `Configuration::newDefaultAnnotationDriver()`: + + // Register the ORM Annotations in the AnnotationRegistry + AnnotationRegistry::registerFile('path/to/Doctrine/ORM/Mapping/Driver/DoctrineAnnotations.php'); + + $reader = new \Doctrine\Common\Annotations\SimpleAnnotationReader(); + $reader->addNamespace('Doctrine\ORM\Mapping'); + $reader = new \Doctrine\Common\Annotations\CachedReader($reader, new ArrayCache()); + + $driver = new AnnotationDriver($reader, (array)$paths); + + $config->setMetadataDriverImpl($driver); + + +## Scalar mappings can now be omitted from DQL result + +You are now allowed to mark scalar SELECT expressions as HIDDEN an they are not hydrated anymore. +Example: + +SELECT u, SUM(a.id) AS HIDDEN numArticles FROM User u LEFT JOIN u.Articles a ORDER BY numArticles DESC HAVING numArticles > 10 + +Your result will be a collection of Users, and not an array with key 0 as User object instance and "numArticles" as the number of articles per user + + +## Map entities as scalars in DQL result + +When hydrating to array or even a mixed result in object hydrator, previously you had the 0 index holding you entity instance. +You are now allowed to alias this, providing more flexibility for you code. +Example: + +SELECT u AS user FROM User u + +Will now return a collection of arrays with index "user" pointing to the User object instance. + + +## Performance optimizations + +Thousands of lines were completely reviewed and optimized for best performance. +Removed redundancy and improved code readability made now internal Doctrine code easier to understand. +Also, Doctrine 2.2 now is around 10-15% faster than 2.1. + +## EntityManager#find(null) + +Previously EntityManager#find(null) returned null. It now throws an exception. + +# Upgrade to 2.1 + +## Interface for EntityRepository + +The EntityRepository now has an interface Doctrine\Persistence\ObjectRepository. This means that your classes that override EntityRepository and extend find(), findOneBy() or findBy() must be adjusted to follow this interface. + +## AnnotationReader changes + +The annotation reader was heavily refactored between 2.0 and 2.1-RC1. In theory the operation of the new reader should be backwards compatible, but it has to be setup differently to work that way: + + // new call to the AnnotationRegistry + \Doctrine\Common\Annotations\AnnotationRegistry::registerFile('/doctrine-src/src/Mapping/Driver/DoctrineAnnotations.php'); + + $reader = new \Doctrine\Common\Annotations\AnnotationReader(); + $reader->setDefaultAnnotationNamespace('Doctrine\ORM\Mapping\\'); + // new code necessary starting here + $reader->setIgnoreNotImportedAnnotations(true); + $reader->setEnableParsePhpImports(false); + $reader = new \Doctrine\Common\Annotations\CachedReader( + new \Doctrine\Common\Annotations\IndexedReader($reader), new ArrayCache() + ); + +This is already done inside the ``$config->newDefaultAnnotationDriver``, so everything should automatically work if you are using this method. You can verify if everything still works by executing a console command such as schema-validate that loads all metadata into memory. + +# Update from 2.0-BETA3 to 2.0-BETA4 + +## XML Driver element demoted to attribute + +We changed how the XML Driver allows to define the change-tracking-policy. The working case is now: + + + +# Update from 2.0-BETA2 to 2.0-BETA3 + +## Serialization of Uninitialized Proxies + +As of Beta3 you can now serialize uninitialized proxies, an exception will only be thrown when +trying to access methods on the unserialized proxy as long as it has not been re-attached to the +EntityManager using `EntityManager#merge()`. See this example: + + $proxy = $em->getReference('User', 1); + + $serializedProxy = serialize($proxy); + $detachedProxy = unserialized($serializedProxy); + + echo $em->contains($detachedProxy); // FALSE + + try { + $detachedProxy->getId(); // uninitialized detached proxy + } catch(Exception $e) { + + } + $attachedProxy = $em->merge($detachedProxy); + echo $attackedProxy->getId(); // works! + +## Changed SQL implementation of Postgres and Oracle DateTime types + +The DBAL Type "datetime" included the Timezone Offset in both Postgres and Oracle. As of this version they are now +generated without Timezone (TIMESTAMP WITHOUT TIME ZONE instead of TIMESTAMP WITH TIME ZONE). +See [this comment to Ticket DBAL-22](http://www.doctrine-project.org/jira/browse/DBAL-22?focusedCommentId=13396&page=com.atlassian.jira.plugin.system.issuetabpanels%3Acomment-tabpanel#action_13396) +for more details as well as migration issues for PostgreSQL and Oracle. + +Both Postgres and Oracle will throw Exceptions during hydration of Objects with "DateTime" fields unless migration steps are taken! + +## Removed multi-dot/deep-path expressions in DQL + +The support for implicit joins in DQL through the multi-dot/Deep Path Expressions +was dropped. For example: + + SELECT u FROM User u WHERE u.group.name = ?1 + +See the "u.group.id" here is using multi dots (deep expression) to walk +through the graph of objects and properties. Internally the DQL parser +would rewrite these queries to: + + SELECT u FROM User u JOIN u.group g WHERE g.name = ?1 + +This explicit notation will be the only supported notation as of now. The internal +handling of multi-dots in the DQL Parser was very complex, error prone in edge cases +and required special treatment for several features we added. Additionally +it had edge cases that could not be solved without making the DQL Parser +even much more complex. For this reason we will drop the support for the +deep path expressions to increase maintainability and overall performance +of the DQL parsing process. This will benefit any DQL query being parsed, +even those not using deep path expressions. + +Note that the generated SQL of both notations is exactly the same! You +don't loose anything through this. + +## Default Allocation Size for Sequences + +The default allocation size for sequences has been changed from 10 to 1. This step was made +to not cause confusion with users and also because it is partly some kind of premature optimization. + +# Update from 2.0-BETA1 to 2.0-BETA2 + +There are no backwards incompatible changes in this release. + +# Upgrade from 2.0-ALPHA4 to 2.0-BETA1 + +## EntityRepository deprecates access to protected variables + +Instead of accessing protected variables for the EntityManager in +a custom EntityRepository it is now required to use the getter methods +for all the three instance variables: + +* `$this->_em` now accessible through `$this->getEntityManager()` +* `$this->_class` now accessible through `$this->getClassMetadata()` +* `$this->_entityName` now accessible through `$this->getEntityName()` + +Important: For Beta 2 the protected visibility of these three properties will be +changed to private! + +## Console migrated to Symfony Console + +The Doctrine CLI has been replaced by Symfony Console Configuration + +Instead of having to specify: + + [php] + $cliConfig = new CliConfiguration(); + $cliConfig->setAttribute('em', $entityManager); + +You now have to configure the script like: + + [php] + $helperSet = new \Symfony\Components\Console\Helper\HelperSet(array( + 'db' => new \Doctrine\DBAL\Tools\Console\Helper\ConnectionHelper($em->getConnection()), + 'em' => new \Doctrine\ORM\Tools\Console\Helper\EntityManagerHelper($em) + )); + +## Console: No need for Mapping Paths anymore + +In previous versions you had to specify the --from and --from-path options +to show where your mapping paths are from the console. However this information +is already known from the Mapping Driver configuration, so the requirement +for this options were dropped. + +Instead for each console command all the entities are loaded and to +restrict the operation to one or more sub-groups you can use the --filter flag. + +## AnnotationDriver is not a default mapping driver anymore + +In conjunction with the recent changes to Console we realized that the +annotations driver being a default metadata driver lead to lots of glue +code in the console components to detect where entities lie and how to load +them for batch updates like SchemaTool and other commands. However the +annotations driver being a default driver does not really help that much +anyways. + +Therefore we decided to break backwards compatibility in this issue and drop +the support for Annotations as Default Driver and require our users to +specify the driver explicitly (which allows us to ask for the path to all +entities). + +If you are using the annotations metadata driver as default driver, you +have to add the following lines to your bootstrap code: + + $driverImpl = $config->newDefaultAnnotationDriver(array(__DIR__."/Entities")); + $config->setMetadataDriverImpl($driverImpl); + +You have to specify the path to your entities as either string of a single +path or array of multiple paths +to your entities. This information will be used by all console commands to +access all entities. + +Xml and Yaml Drivers work as before! + + +## New inversedBy attribute + +It is now *mandatory* that the owning side of a bidirectional association specifies the +'inversedBy' attribute that points to the name of the field on the inverse side that completes +the association. Example: + + [php] + // BEFORE (ALPHA4 AND EARLIER) + class User + { + //... + /** @OneToOne(targetEntity="Address", mappedBy="user") */ + private $address; + //... + } + class Address + { + //... + /** @OneToOne(targetEntity="User") */ + private $user; + //... + } + + // SINCE BETA1 + // User class DOES NOT CHANGE + class Address + { + //... + /** @OneToOne(targetEntity="User", inversedBy="address") */ + private $user; + //... + } + +Thus, the inversedBy attribute is the counterpart to the mappedBy attribute. This change +was necessary to enable some simplifications and further performance improvements. We +apologize for the inconvenience. + +## Default Property for Field Mappings + +The "default" option for database column defaults has been removed. If desired, database column defaults can +be implemented by using the columnDefinition attribute of the @Column annotation (or the appropriate XML and YAML equivalents). +Prefer PHP default values, if possible. + +## Selecting Partial Objects + +Querying for partial objects now has a new syntax. The old syntax to query for partial objects +now has a different meaning. This is best illustrated by an example. If you previously +had a DQL query like this: + + [sql] + SELECT u.id, u.name FROM User u + +Since BETA1, simple state field path expressions in the select clause are used to select +object fields as plain scalar values (something that was not possible before). +To achieve the same result as previously (that is, a partial object with only id and name populated) +you need to use the following, explicit syntax: + + [sql] + SELECT PARTIAL u.{id,name} FROM User u + +## XML Mapping Driver + +The 'inheritance-type' attribute changed to take last bit of ClassMetadata constant names, i.e. +NONE, SINGLE_TABLE, INHERITANCE_TYPE_JOINED + +## YAML Mapping Driver + +The way to specify lifecycle callbacks in YAML Mapping driver was changed to allow for multiple callbacks +per event. The Old syntax ways: + + [yaml] + lifecycleCallbacks: + doStuffOnPrePersist: prePersist + doStuffOnPostPersist: postPersist + +The new syntax is: + + [yaml] + lifecycleCallbacks: + prePersist: [ doStuffOnPrePersist, doOtherStuffOnPrePersistToo ] + postPersist: [ doStuffOnPostPersist ] + +## PreUpdate Event Listeners + +Event Listeners listening to the 'preUpdate' event can only affect the primitive values of entity changesets +by using the API on the `PreUpdateEventArgs` instance passed to the preUpdate listener method. Any changes +to the state of the entitys properties won't affect the database UPDATE statement anymore. This gives drastic +performance benefits for the preUpdate event. + +## Collection API + +The Collection interface in the Common package has been updated with some missing methods +that were present only on the default implementation, ArrayCollection. Custom collection +implementations need to be updated to adhere to the updated interface. + +# Upgrade from 2.0-ALPHA3 to 2.0-ALPHA4 + +## CLI Controller changes + +CLI main object changed its name and namespace. Renamed from Doctrine\ORM\Tools\Cli to Doctrine\Common\Cli\CliController. +Doctrine\Common\Cli\CliController now only deals with namespaces. Ready to go, Core, Dbal and Orm are available and you can subscribe new tasks by retrieving the namespace and including new task. Example: + + [php] + $cli->getNamespace('Core')->addTask('my-example', '\MyProject\Tools\Cli\Tasks\MyExampleTask'); + + +## CLI Tasks documentation + +Tasks have implemented a new way to build documentation. Although it is still possible to define the help manually by extending the basicHelp and extendedHelp, they are now optional. +With new required method AbstractTask::buildDocumentation, its implementation defines the TaskDocumentation instance (accessible through AbstractTask::getDocumentation()), basicHelp and extendedHelp are now not necessary to be implemented. + +## Changes in Method Signatures + + * A bunch of Methods on both Doctrine\DBAL\Platforms\AbstractPlatform and Doctrine\DBAL\Schema\AbstractSchemaManager + have changed quite significantly by adopting the new Schema instance objects. + +## Renamed Methods + + * Doctrine\ORM\AbstractQuery::setExpireResultCache() -> expireResultCache() + * Doctrine\ORM\Query::setExpireQueryCache() -> expireQueryCache() + +## SchemaTool Changes + + * "doctrine schema-tool --drop" now always drops the complete database instead of + only those tables defined by the current database model. The previous method had + problems when foreign keys of orphaned tables pointed to tables that were scheduled + for deletion. + * Use "doctrine schema-tool --update" to get a save incremental update for your + database schema without deleting any unused tables, sequences or foreign keys. + * Use "doctrine schema-tool --complete-update" to do a full incremental update of + your schema. +# Upgrade from 2.0-ALPHA2 to 2.0-ALPHA3 + +This section details the changes made to Doctrine 2.0-ALPHA3 to make it easier for you +to upgrade your projects to use this version. + +## CLI Changes + +The $args variable used in the cli-config.php for configuring the Doctrine CLI has been renamed to $globalArguments. + +## Proxy class changes + +You are now required to make supply some minimalist configuration with regards to proxy objects. That involves 2 new configuration options. First, the directory where generated proxy classes should be placed needs to be specified. Secondly, you need to configure the namespace used for proxy classes. The following snippet shows an example: + + [php] + // step 1: configure directory for proxy classes + // $config instanceof Doctrine\ORM\Configuration + $config->setProxyDir('/path/to/myproject/lib/MyProject/Generated/Proxies'); + $config->setProxyNamespace('MyProject\Generated\Proxies'); + +Note that proxy classes behave exactly like any other classes when it comes to class loading. Therefore you need to make sure the proxy classes can be loaded by some class loader. If you place the generated proxy classes in a namespace and directory under your projects class files, like in the example above, it would be sufficient to register the MyProject namespace on a class loader. Since the proxy classes are contained in that namespace and adhere to the standards for class loading, no additional work is required. +Generating the proxy classes into a namespace within your class library is the recommended setup. + +Entities with initialized proxy objects can now be serialized and unserialized properly from within the same application. + +For more details refer to the Configuration section of the manual. + +## Removed allowPartialObjects configuration option + +The allowPartialObjects configuration option together with the `Configuration#getAllowPartialObjects` and `Configuration#setAllowPartialObjects` methods have been removed. +The new behavior is as if the option were set to FALSE all the time, basically disallowing partial objects globally. However, you can still use the `Query::HINT_FORCE_PARTIAL_LOAD` query hint to force a query to return partial objects for optimization purposes. + +## Renamed Methods + +* Doctrine\ORM\Configuration#getCacheDir() to getProxyDir() +* Doctrine\ORM\Configuration#setCacheDir($dir) to setProxyDir($dir) diff --git a/vendor/doctrine/orm/composer.json b/vendor/doctrine/orm/composer.json new file mode 100644 index 0000000..2a75126 --- /dev/null +++ b/vendor/doctrine/orm/composer.json @@ -0,0 +1,65 @@ +{ + "name": "doctrine/orm", + "type": "library", + "description": "Object-Relational-Mapper for PHP", + "keywords": ["orm", "database"], + "homepage": "https://www.doctrine-project.org/projects/orm.html", + "license": "MIT", + "authors": [ + {"name": "Guilherme Blanco", "email": "guilhermeblanco@gmail.com"}, + {"name": "Roman Borschel", "email": "roman@code-factory.org"}, + {"name": "Benjamin Eberlei", "email": "kontakt@beberlei.de"}, + {"name": "Jonathan Wage", "email": "jonwage@gmail.com"}, + {"name": "Marco Pivetta", "email": "ocramius@gmail.com"} + ], + "config": { + "allow-plugins": { + "composer/package-versions-deprecated": true, + "dealerdirect/phpcodesniffer-composer-installer": true + }, + "sort-packages": true + }, + "require": { + "php": "^8.1", + "composer-runtime-api": "^2", + "ext-ctype": "*", + "doctrine/collections": "^2.2", + "doctrine/dbal": "^3.8.2 || ^4", + "doctrine/deprecations": "^0.5.3 || ^1", + "doctrine/event-manager": "^1.2 || ^2", + "doctrine/inflector": "^1.4 || ^2.0", + "doctrine/instantiator": "^1.3 || ^2", + "doctrine/lexer": "^3", + "doctrine/persistence": "^3.3.1", + "psr/cache": "^1 || ^2 || ^3", + "symfony/console": "^5.4 || ^6.0 || ^7.0", + "symfony/var-exporter": "^6.3.9 || ^7.0" + }, + "require-dev": { + "doctrine/coding-standard": "^12.0", + "phpbench/phpbench": "^1.0", + "phpstan/phpstan": "1.11.1", + "phpunit/phpunit": "^10.4.0", + "psr/log": "^1 || ^2 || ^3", + "squizlabs/php_codesniffer": "3.7.2", + "symfony/cache": "^5.4 || ^6.2 || ^7.0", + "vimeo/psalm": "5.24.0" + }, + "suggest": { + "ext-dom": "Provides support for XSD validation for XML mapping files", + "symfony/cache": "Provides cache support for Setup Tool with doctrine/cache 2.0" + }, + "autoload": { + "psr-4": { "Doctrine\\ORM\\": "src" } + }, + "autoload-dev": { + "psr-4": { + "Doctrine\\Tests\\": "tests/Tests", + "Doctrine\\StaticAnalysis\\": "tests/StaticAnalysis", + "Doctrine\\Performance\\": "tests/Performance" + } + }, + "archive": { + "exclude": ["!vendor", "tests", "*phpunit.xml", "build.xml", "build.properties", "composer.phar", "vendor/satooshi", "lib/vendor", "*.swp"] + } +} diff --git a/vendor/doctrine/orm/doctrine-mapping.xsd b/vendor/doctrine/orm/doctrine-mapping.xsd new file mode 100644 index 0000000..58175a2 --- /dev/null +++ b/vendor/doctrine/orm/doctrine-mapping.xsd @@ -0,0 +1,589 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/vendor/doctrine/orm/phpstan-dbal3.neon b/vendor/doctrine/orm/phpstan-dbal3.neon new file mode 100644 index 0000000..b9ef942 --- /dev/null +++ b/vendor/doctrine/orm/phpstan-dbal3.neon @@ -0,0 +1,29 @@ +includes: + - phpstan-baseline.neon + - phpstan-params.neon + +parameters: + ignoreErrors: + # Symfony cache supports passing a key prefix to the clear method. + - '/^Method Psr\\Cache\\CacheItemPoolInterface\:\:clear\(\) invoked with 1 parameter, 0 required\.$/' + + # We can be certain that those values are not matched. + - + message: '~^Match expression does not handle remaining values:~' + path: src/Persisters/Entity/BasicEntityPersister.php + + # DBAL 4 compatibility + - + message: '~^Method Doctrine\\ORM\\Query\\AST\\Functions\\TrimFunction::getTrimMode\(\) never returns .* so it can be removed from the return type\.$~' + path: src/Query/AST/Functions/TrimFunction.php + - + message: '~^Method Doctrine\\ORM\\Persisters\\Entity\\BasicEntityPersister\:\:getArrayBindingType\(\) never returns .* so it can be removed from the return type\.$~' + path: src/Persisters/Entity/BasicEntityPersister.php + + - '~^Class Doctrine\\DBAL\\Platforms\\SQLitePlatform not found\.$~' + + # To be removed in 4.0 + - + message: '#Negated boolean expression is always false\.#' + paths: + - src/Mapping/Driver/AttributeDriver.php 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