From 43061bd1e0e3b5f7eeec89137f5020c433e7086e Mon Sep 17 00:00:00 2001 From: Michael Telgmann Date: Mon, 17 Mar 2025 14:42:28 +0100 Subject: [PATCH] chore: Update doctrine/orm to 2.20.2 --- composer.json | 2 +- composer.lock | 31 +-- .../Common/Proxy/AbstractProxyFactory.php | 2 + .../Entity/BasicEntityPersister.php | 177 ++++++++++++------ 4 files changed, 140 insertions(+), 72 deletions(-) diff --git a/composer.json b/composer.json index f9037c51cf1..b17af49dee0 100644 --- a/composer.json +++ b/composer.json @@ -48,7 +48,7 @@ "doctrine/dbal": "2.13.9", "doctrine/event-manager": "1.2.0", "doctrine/inflector": "2.0.10", - "doctrine/orm": "2.15.5", + "doctrine/orm": "2.20.2", "doctrine/persistence": "3.4.0", "elasticsearch/elasticsearch": "^7", "fig/link-util": "1.1.2", diff --git a/composer.lock b/composer.lock index 9f233005845..63a89a44375 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "dce98e28d81b7c9aa07575f30a7e65bb", + "content-hash": "1644ca009ef1b5651103472d2be9519d", "packages": [ { "name": "aws/aws-crt-php", @@ -1289,16 +1289,16 @@ }, { "name": "doctrine/orm", - "version": "2.15.5", + "version": "2.20.2", "source": { "type": "git", "url": "https://github.com/doctrine/orm.git", - "reference": "d2de4ec03c63ddd7bdfda0421e237e11343c75ee" + "reference": "19912de9270fa6abb3d25a1a37784af6b818d534" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/orm/zipball/d2de4ec03c63ddd7bdfda0421e237e11343c75ee", - "reference": "d2de4ec03c63ddd7bdfda0421e237e11343c75ee", + "url": "https://api.github.com/repos/doctrine/orm/zipball/19912de9270fa6abb3d25a1a37784af6b818d534", + "reference": "19912de9270fa6abb3d25a1a37784af6b818d534", "shasum": "" }, "require": { @@ -1311,12 +1311,12 @@ "doctrine/event-manager": "^1.2 || ^2", "doctrine/inflector": "^1.4 || ^2.0", "doctrine/instantiator": "^1.3 || ^2", - "doctrine/lexer": "^2", + "doctrine/lexer": "^2 || ^3", "doctrine/persistence": "^2.4 || ^3", "ext-ctype": "*", "php": "^7.1 || ^8.0", "psr/cache": "^1 || ^2 || ^3", - "symfony/console": "^4.2 || ^5.0 || ^6.0", + "symfony/console": "^4.2 || ^5.0 || ^6.0 || ^7.0", "symfony/polyfill-php72": "^1.23", "symfony/polyfill-php80": "^1.16" }, @@ -1327,14 +1327,15 @@ "doctrine/annotations": "^1.13 || ^2", "doctrine/coding-standard": "^9.0.2 || ^12.0", "phpbench/phpbench": "^0.16.10 || ^1.0", - "phpstan/phpstan": "~1.4.10 || 1.10.25", + "phpstan/extension-installer": "~1.1.0 || ^1.4", + "phpstan/phpstan": "~1.4.10 || 2.0.3", + "phpstan/phpstan-deprecation-rules": "^1 || ^2", "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6", "psr/log": "^1 || ^2 || ^3", "squizlabs/php_codesniffer": "3.7.2", - "symfony/cache": "^4.4 || ^5.4 || ^6.0", - "symfony/var-exporter": "^4.4 || ^5.4 || ^6.2", - "symfony/yaml": "^3.4 || ^4.0 || ^5.0 || ^6.0", - "vimeo/psalm": "4.30.0 || 5.13.1" + "symfony/cache": "^4.4 || ^5.4 || ^6.4 || ^7.0", + "symfony/var-exporter": "^4.4 || ^5.4 || ^6.2 || ^7.0", + "symfony/yaml": "^3.4 || ^4.0 || ^5.0 || ^6.0 || ^7.0" }, "suggest": { "ext-dom": "Provides support for XSD validation for XML mapping files", @@ -1347,7 +1348,7 @@ "type": "library", "autoload": { "psr-4": { - "Doctrine\\ORM\\": "lib/Doctrine/ORM" + "Doctrine\\ORM\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -1384,9 +1385,9 @@ ], "support": { "issues": "https://github.com/doctrine/orm/issues", - "source": "https://github.com/doctrine/orm/tree/2.15.5" + "source": "https://github.com/doctrine/orm/tree/2.20.2" }, - "time": "2023-07-28T14:08:17+00:00" + "time": "2025-02-04T19:17:01+00:00" }, { "name": "doctrine/persistence", diff --git a/engine/Library/Doctrine/Common/Proxy/AbstractProxyFactory.php b/engine/Library/Doctrine/Common/Proxy/AbstractProxyFactory.php index c742291aee9..8ad34dc9632 100644 --- a/engine/Library/Doctrine/Common/Proxy/AbstractProxyFactory.php +++ b/engine/Library/Doctrine/Common/Proxy/AbstractProxyFactory.php @@ -15,6 +15,8 @@ /** * Abstract factory for proxy objects. + * + * @deprecated The AbstractProxyFactory class is deprecated since doctrine/common 3.5. */ abstract class AbstractProxyFactory { diff --git a/engine/Library/Doctrine/ORM/Persisters/Entity/BasicEntityPersister.php b/engine/Library/Doctrine/ORM/Persisters/Entity/BasicEntityPersister.php index e2b6484064f..e39244c704f 100644 --- a/engine/Library/Doctrine/ORM/Persisters/Entity/BasicEntityPersister.php +++ b/engine/Library/Doctrine/ORM/Persisters/Entity/BasicEntityPersister.php @@ -7,7 +7,6 @@ use BackedEnum; use Doctrine\Common\Collections\Criteria; use Doctrine\Common\Collections\Expr\Comparison; -use Doctrine\Common\Util\ClassUtils; use Doctrine\DBAL\Connection; use Doctrine\DBAL\LockMode; use Doctrine\DBAL\Platforms\AbstractPlatform; @@ -16,6 +15,7 @@ use Doctrine\DBAL\Types\Types; use Doctrine\Deprecations\Deprecation; use Doctrine\ORM\EntityManagerInterface; +use Doctrine\ORM\Internal\CriteriaOrderings; use Doctrine\ORM\Mapping\ClassMetadata; use Doctrine\ORM\Mapping\MappingException; use Doctrine\ORM\Mapping\QuoteStrategy; @@ -26,11 +26,13 @@ use Doctrine\ORM\Persisters\Exception\UnrecognizedField; use Doctrine\ORM\Persisters\SqlExpressionVisitor; use Doctrine\ORM\Persisters\SqlValueVisitor; +use Doctrine\ORM\Proxy\DefaultProxyClassNameResolver; use Doctrine\ORM\Query; use Doctrine\ORM\Query\QueryException; use Doctrine\ORM\Repository\Exception\InvalidFindByCall; use Doctrine\ORM\UnitOfWork; use Doctrine\ORM\Utility\IdentifierFlattener; +use Doctrine\ORM\Utility\LockSqlHelper; use Doctrine\ORM\Utility\PersisterHelper; use LengthException; @@ -88,10 +90,13 @@ * Subclasses can be created to provide custom persisting and querying strategies, * i.e. spanning multiple tables. * - * @psalm-import-type AssociationMapping from ClassMetadata + * @phpstan-import-type AssociationMapping from ClassMetadata */ class BasicEntityPersister implements EntityPersister { + use CriteriaOrderings; + use LockSqlHelper; + /** @var array */ private static $comparisonMap = [ Comparison::EQ => '= %s', @@ -138,7 +143,7 @@ class BasicEntityPersister implements EntityPersister /** * Queued inserts. * - * @psalm-var array + * @phpstan-var array */ protected $queuedInserts = []; @@ -183,7 +188,7 @@ class BasicEntityPersister implements EntityPersister * * @var IdentifierFlattener */ - private $identifierFlattener; + protected $identifierFlattener; /** @var CachedPersisterContext */ protected $currentPersisterContext; @@ -194,6 +199,9 @@ class BasicEntityPersister implements EntityPersister /** @var CachedPersisterContext */ private $noLimitsContext; + /** @var ?string */ + private $filterHash = null; + /** * Initializes a new BasicEntityPersister that uses the given EntityManager * and persists instances of the class described by the given ClassMetadata descriptor. @@ -256,17 +264,17 @@ public function getInserts() public function executeInserts() { if (! $this->queuedInserts) { - return []; + return; } - $postInsertIds = []; + $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 $entity) { + foreach ($this->queuedInserts as $key => $entity) { $insertData = $this->prepareInsertData($entity); if (isset($insertData[$tableName])) { @@ -280,12 +288,10 @@ public function executeInserts() $stmt->executeStatement(); if ($isPostInsertId) { - $generatedId = $idGenerator->generateId($this->em, $entity); - $id = [$this->class->identifier[0] => $generatedId]; - $postInsertIds[] = [ - 'generatedId' => $generatedId, - 'entity' => $entity, - ]; + $generatedId = $idGenerator->generateId($this->em, $entity); + $id = [$this->class->identifier[0] => $generatedId]; + + $uow->assignPostInsertId($entity, $generatedId); } else { $id = $this->class->getIdentifierValues($entity); } @@ -293,11 +299,16 @@ public function executeInserts() if ($this->class->requiresFetchAfterChange) { $this->assignDefaultVersionAndUpsertableValues($entity, $id); } - } - - $this->queuedInserts = []; - return $postInsertIds; + // 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]); + } } /** @@ -374,9 +385,9 @@ protected function fetchVersionAndNotUpsertableValues($versionedClass, array $id * @param mixed[] $id * * @return int[]|null[]|string[] - * @psalm-return list + * @phpstan-return list */ - private function extractIdentifierTypes(array $id, ClassMetadata $versionedClass): array + final protected function extractIdentifierTypes(array $id, ClassMetadata $versionedClass): array { $types = []; @@ -622,7 +633,7 @@ public function delete($entity) * @param bool $isInsert Whether the data to be prepared refers to an insert statement. * * @return mixed[][] The prepared data. - * @psalm-return array> + * @phpstan-return array> */ protected function prepareUpdateData($entity, bool $isInsert = false) { @@ -675,10 +686,30 @@ protected function prepareUpdateData($entity, bool $isInsert = false) if ($newVal !== null) { $oid = spl_object_id($newVal); - if (isset($this->queuedInserts[$oid]) || $uow->isScheduledForInsert($newVal)) { - // The associated entity $newVal is not yet persisted, so we must - // set $newVal = null, in order to insert a null value and schedule an - // extra update on the UnitOfWork. + // 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; @@ -767,7 +798,7 @@ protected function prepareUpdateData($entity, bool $isInsert = false) * @param object $entity The entity for which to prepare the data. * * @return mixed[][] The prepared data for the tables to update. - * @psalm-return array + * @phpstan-return array */ protected function prepareInsertData($entity) { @@ -850,17 +881,42 @@ public function loadOneToOneEntity(array $assoc, $sourceEntity, array $identifie $computedIdentifier = []; + /** @var array|null $sourceEntityData */ + $sourceEntityData = null; + // 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 - ); - } + // The likely case here is that the column is a join column + // in an association mapping. However, there is no guarantee + // at this point that a corresponding (generally identifying) + // association has been mapped in the source entity. To handle + // this case we directly reference the column-keyed data used + // to initialize the source entity before throwing an exception. + $resolvedSourceData = false; + if (! isset($sourceEntityData)) { + $sourceEntityData = $this->em->getUnitOfWork()->getOriginalEntityData($sourceEntity); + } + + if (isset($sourceEntityData[$sourceKeyColumn])) { + $dataValue = $sourceEntityData[$sourceKeyColumn]; + if ($dataValue !== null) { + $resolvedSourceData = true; + $computedIdentifier[$targetClass->getFieldForColumn($targetKeyColumn)] = + $dataValue; + } + } - $computedIdentifier[$targetClass->getFieldForColumn($targetKeyColumn)] = - $sourceClass->reflFields[$sourceClass->fieldNames[$sourceKeyColumn]]->getValue($sourceEntity); + if (! $resolvedSourceData) { + throw MappingException::joinColumnMustPointToMappedField( + $sourceClass->name, + $sourceKeyColumn + ); + } + } else { + $computedIdentifier[$targetClass->getFieldForColumn($targetKeyColumn)] = + $sourceClass->reflFields[$sourceClass->fieldNames[$sourceKeyColumn]]->getValue($sourceEntity); + } } $targetEntity = $this->load($computedIdentifier, null, $assoc); @@ -904,7 +960,7 @@ public function count($criteria = []) */ public function loadCriteria(Criteria $criteria) { - $orderBy = $criteria->getOrderings(); + $orderBy = self::getCriteriaOrderings($criteria); $limit = $criteria->getMaxResults(); $offset = $criteria->getFirstResult(); $query = $this->getSelectSQL($criteria, null, null, $limit, $offset, $orderBy); @@ -1036,7 +1092,7 @@ public function loadManyToManyCollection(array $assoc, $sourceEntity, Persistent /** * @param object $sourceEntity - * @psalm-param array $assoc + * @phpstan-param array $assoc * * @return Result * @@ -1139,11 +1195,11 @@ public function getSelectSQL($criteria, $assoc = null, $lockMode = null, $limit switch ($lockMode) { case LockMode::PESSIMISTIC_READ: - $lockSql = ' ' . $this->platform->getReadLockSQL(); + $lockSql = ' ' . $this->getReadLockSQL($this->platform); break; case LockMode::PESSIMISTIC_WRITE: - $lockSql = ' ' . $this->platform->getWriteLockSQL(); + $lockSql = ' ' . $this->getWriteLockSQL($this->platform); break; } @@ -1200,7 +1256,7 @@ public function getCountSQL($criteria = []) /** * Gets the ORDER BY SQL snippet for ordered collections. * - * @psalm-param array $orderBy + * @phpstan-param array $orderBy * * @throws InvalidOrientation * @throws InvalidFindByCall @@ -1264,7 +1320,7 @@ final protected function getOrderBySQL(array $orderBy, string $baseTableAlias): */ protected function getSelectColumnsSQL() { - if ($this->currentPersisterContext->selectColumnListSql !== null) { + if ($this->currentPersisterContext->selectColumnListSql !== null && $this->filterHash === $this->em->getFilters()->getHash()) { return $this->currentPersisterContext->selectColumnListSql; } @@ -1287,7 +1343,7 @@ protected function getSelectColumnsSQL() } $isAssocToOneInverseSide = $assoc['type'] & ClassMetadata::TO_ONE && ! $assoc['isOwningSide']; - $isAssocFromOneEager = $assoc['type'] !== ClassMetadata::MANY_TO_MANY && $assoc['fetch'] === ClassMetadata::FETCH_EAGER; + $isAssocFromOneEager = $assoc['type'] & ClassMetadata::TO_ONE && $assoc['fetch'] === ClassMetadata::FETCH_EAGER; if (! ($isAssocFromOneEager || $isAssocToOneInverseSide)) { continue; @@ -1371,6 +1427,7 @@ protected function getSelectColumnsSQL() } $this->currentPersisterContext->selectColumnListSql = implode(', ', $columnList); + $this->filterHash = $this->em->getFilters()->getHash(); return $this->currentPersisterContext->selectColumnListSql; } @@ -1412,7 +1469,7 @@ protected function getSelectColumnAssociationSQL($field, $assoc, ClassMetadata $ * Gets the SQL join fragment used when selecting entities from a * many-to-many association. * - * @psalm-param AssociationMapping $manyToMany + * @phpstan-param AssociationMapping $manyToMany * * @return string */ @@ -1493,7 +1550,7 @@ public function getInsertSQL() * columns placed in the INSERT statements used by the persister. * * @return string[] The list of columns. - * @psalm-return list + * @phpstan-return list */ protected function getInsertColumnList() { @@ -1549,7 +1606,15 @@ protected function getSelectColumnSQL($field, ClassMetadata $class, $alias = 'r' $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']); + + $columnAlias = null; + if ($this->currentPersisterContext->rsm->hasColumnAliasByField($alias, $field)) { + $columnAlias = $this->currentPersisterContext->rsm->getColumnAliasByField($alias, $field); + } + + if ($columnAlias === null) { + $columnAlias = $this->getSQLColumnAlias($fieldMapping['columnName']); + } $this->currentPersisterContext->rsm->addFieldResult($alias, $columnAlias, $field); if (! empty($fieldMapping['enumType'])) { @@ -1601,11 +1666,11 @@ public function lock(array $criteria, $lockMode) switch ($lockMode) { case LockMode::PESSIMISTIC_READ: - $lockSql = $this->platform->getReadLockSQL(); + $lockSql = $this->getReadLockSQL($this->platform); break; case LockMode::PESSIMISTIC_WRITE: - $lockSql = $this->platform->getWriteLockSQL(); + $lockSql = $this->getWriteLockSQL($this->platform); break; } @@ -1625,7 +1690,7 @@ public function lock(array $criteria, $lockMode) * Gets the FROM and optionally JOIN conditions to lock the entity managed by this persister. * * @param int|null $lockMode One of the Doctrine\DBAL\LockMode::* constants. - * @psalm-param LockMode::*|null $lockMode + * @phpstan-param LockMode::*|null $lockMode * * @return string */ @@ -1740,10 +1805,10 @@ public function getSelectConditionStatementSQL($field, $value, $assoc = null, $c /** * Builds the left-hand-side of a where condition statement. * - * @psalm-param AssociationMapping|null $assoc + * @phpstan-param AssociationMapping|null $assoc * * @return string[] - * @psalm-return list + * @phpstan-return list * * @throws InvalidFindByCall * @throws UnrecognizedField @@ -1814,8 +1879,8 @@ private function getSelectConditionStatementColumnSQL( * or alter the criteria by which entities are selected. * * @param AssociationMapping|null $assoc - * @psalm-param array $criteria - * @psalm-param array|null $assoc + * @phpstan-param array $criteria + * @phpstan-param array|null $assoc * * @return string */ @@ -1856,7 +1921,7 @@ public function loadOneToManyCollection(array $assoc, $sourceEntity, PersistentC * Builds criteria and execute SQL statement to fetch the one to many entities from. * * @param object $sourceEntity - * @psalm-param AssociationMapping $assoc + * @phpstan-param AssociationMapping $assoc */ private function getOneToManyStatement( array $assoc, @@ -1939,7 +2004,7 @@ public function expandParameters($criteria) * - class to which the field belongs to * * @return mixed[][] - * @psalm-return array{0: array, 1: list} + * @phpstan-return array{0: array, 1: list} */ private function expandToManyParameters(array $criteria): array { @@ -1964,7 +2029,7 @@ private function expandToManyParameters(array $criteria): array * @param mixed $value * * @return int[]|null[]|string[] - * @psalm-return list + * @phpstan-return list * * @throws QueryException */ @@ -2039,7 +2104,7 @@ private function getValues($value): array * * @param mixed $value * - * @psalm-return list + * @phpstan-return list */ private function getIndividualValue($value): array { @@ -2051,7 +2116,7 @@ private function getIndividualValue($value): array return [$value->value]; } - $valueClass = ClassUtils::getClass($value); + $valueClass = DefaultProxyClassNameResolver::getClass($value); if ($this->em->getMetadataFactory()->isTransient($valueClass)) { return [$value]; @@ -2111,7 +2176,7 @@ public function exists($entity, ?Criteria $extraConditions = null) * Generates the appropriate join SQL for the given join column. * * @param array[] $joinColumns The join columns definition of an association. - * @psalm-param array> $joinColumns + * @phpstan-param array> $joinColumns * * @return string LEFT JOIN if one of the columns is nullable, INNER JOIN otherwise. */ @@ -2184,7 +2249,7 @@ protected function switchPersisterContext($offset, $limit) /** * @return string[] - * @psalm-return list + * @phpstan-return list */ protected function getClassIdentifiersTypes(ClassMetadata $class): array {