From 9d4f54b9a476f13479c3845350b12c466873fc42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20Paris?= Date: Fri, 24 May 2024 00:25:01 +0200 Subject: [PATCH 01/15] Update branch metadata (#11474) --- .doctrine-project.json | 10 ++++++++-- README.md | 14 +++++++------- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/.doctrine-project.json b/.doctrine-project.json index 0eeb48f5899..f3a38fb4bdd 100644 --- a/.doctrine-project.json +++ b/.doctrine-project.json @@ -11,17 +11,23 @@ "slug": "latest", "upcoming": true }, + { + "name": "3.3", + "branchName": "3.3.x", + "slug": "3.3", + "upcoming": true + }, { "name": "3.2", "branchName": "3.2.x", "slug": "3.2", - "upcoming": true + "current": true }, { "name": "3.1", "branchName": "3.1.x", "slug": "3.1", - "current": true + "maintained": false }, { "name": "3.0", diff --git a/README.md b/README.md index 70dceea1faa..1df322cf7e8 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ -| [4.0.x][4.0] | [3.2.x][3.2] | [3.1.x][3.1] | [2.20.x][2.20] | [2.19.x][2.19] | +| [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.2 image]][3.2] | [![Build status][3.1 image]][3.1] | [![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.2 coverage image]][3.2 coverage] | [![Coverage Status][3.1 coverage image]][3.1 coverage] | [![Coverage Status][2.20 coverage image]][2.20 coverage] | [![Coverage Status][2.19 coverage image]][2.19 coverage] | +| [![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) @@ -22,14 +22,14 @@ without requiring unnecessary code duplication. [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 - [3.1 image]: https://github.com/doctrine/orm/actions/workflows/continuous-integration.yml/badge.svg?branch=3.1.x - [3.1]: https://github.com/doctrine/orm/tree/3.1.x - [3.1 coverage image]: https://codecov.io/gh/doctrine/orm/branch/3.1.x/graph/badge.svg - [3.1 coverage]: https://codecov.io/gh/doctrine/orm/branch/3.1.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 From 9696c3434d8cc8d4f0f54b170cba65772bb5eb6d Mon Sep 17 00:00:00 2001 From: Alix Mauro Date: Fri, 19 Jan 2024 14:16:24 +0100 Subject: [PATCH 02/15] Consider usage of setFetchMode when checking for simultaneous usage of fetch-mode EAGER and WITH condition. This fixes a bug that arises when an entity relation is mapped with fetch-mode EAGER but setFetchMode LAZY (or anything that is not EAGER) has been used on the query. If the query use WITH condition, an exception is incorrectly raised (Associations with fetch-mode=EAGER may not be using WITH conditions). Fixes #11128 Co-Authored-By: Albert Prat --- src/Query/SqlWalker.php | 4 +++- tests/Tests/ORM/Functional/EagerFetchCollectionTest.php | 8 ++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/Query/SqlWalker.php b/src/Query/SqlWalker.php index 4948be46536..4c25fb63a68 100644 --- a/src/Query/SqlWalker.php +++ b/src/Query/SqlWalker.php @@ -1062,7 +1062,9 @@ public function walkJoinAssociationDeclaration($joinAssociationDeclaration, $joi } } - if ($relation['fetch'] === ClassMetadata::FETCH_EAGER && $condExpr !== null) { + $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']); } diff --git a/tests/Tests/ORM/Functional/EagerFetchCollectionTest.php b/tests/Tests/ORM/Functional/EagerFetchCollectionTest.php index ff0eab56d63..88397c6a12f 100644 --- a/tests/Tests/ORM/Functional/EagerFetchCollectionTest.php +++ b/tests/Tests/ORM/Functional/EagerFetchCollectionTest.php @@ -88,6 +88,14 @@ public function testSubselectFetchJoinWithNotAllowed(): void $query->getResult(); } + public function testSubselectFetchJoinWithAllowedWhenOverriddenNotEager(): void + { + $query = $this->_em->createQuery('SELECT o, c FROM ' . EagerFetchOwner::class . ' o JOIN o.children c WITH c.id = 1'); + $query->setFetchMode(EagerFetchChild::class, 'owner', ORM\ClassMetadata::FETCH_LAZY); + + $this->assertIsString($query->getSql()); + } + public function testEagerFetchWithIterable(): void { $this->createOwnerWithChildren(2); From 75bc22980ef85a5774696d9660b91b0006ea89a0 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Thu, 23 May 2024 18:27:17 +0200 Subject: [PATCH 03/15] Fix cloning entities --- psalm-baseline.xml | 4 +- src/Proxy/ProxyFactory.php | 13 ++--- .../Models/ECommerce/ECommerceProduct2.php | 52 +++++++++++++++++++ .../Functional/ProxiesLikeEntitiesTest.php | 2 +- .../ORM/Functional/ReferenceProxyTest.php | 19 +++++++ 5 files changed, 82 insertions(+), 8 deletions(-) create mode 100644 tests/Tests/Models/ECommerce/ECommerceProduct2.php diff --git a/psalm-baseline.xml b/psalm-baseline.xml index 7a3b729e849..d191c3782ff 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -1502,7 +1502,9 @@ - + diff --git a/src/Proxy/ProxyFactory.php b/src/Proxy/ProxyFactory.php index 5b2d2eca0c9..dc8a72bfcea 100644 --- a/src/Proxy/ProxyFactory.php +++ b/src/Proxy/ProxyFactory.php @@ -354,15 +354,14 @@ private function createInitializer(ClassMetadata $classMetadata, EntityPersister /** * Creates a closure capable of initializing a proxy * - * @return Closure(InternalProxy, InternalProxy):void + * @return Closure(InternalProxy, array):void * * @throws EntityNotFoundException */ private function createLazyInitializer(ClassMetadata $classMetadata, EntityPersister $entityPersister, IdentifierFlattener $identifierFlattener): Closure { - return static function (InternalProxy $proxy) use ($entityPersister, $classMetadata, $identifierFlattener): void { - $identifier = $classMetadata->getIdentifierValues($proxy); - $original = $entityPersister->loadById($identifier); + return static function (InternalProxy $proxy, array $identifier) use ($entityPersister, $classMetadata, $identifierFlattener): void { + $original = $entityPersister->loadById($identifier); if ($original === null) { throw EntityNotFoundException::fromClassNameAndIdentifier( @@ -378,7 +377,7 @@ private function createLazyInitializer(ClassMetadata $classMetadata, EntityPersi $class = $entityPersister->getClassMetadata(); foreach ($class->getReflectionProperties() as $property) { - if (! $class->hasField($property->name) && ! $class->hasAssociation($property->name)) { + if (isset($identifier[$property->name]) || ! $class->hasField($property->name) && ! $class->hasAssociation($property->name)) { continue; } @@ -468,7 +467,9 @@ private function getProxyFactory(string $className): Closure $identifierFields = array_intersect_key($class->getReflectionProperties(), $identifiers); $proxyFactory = Closure::bind(static function (array $identifier) use ($initializer, $skippedProperties, $identifierFields, $className): InternalProxy { - $proxy = self::createLazyGhost($initializer, $skippedProperties); + $proxy = self::createLazyGhost(static function (InternalProxy $object) use ($initializer, $identifier): void { + $initializer($object, $identifier); + }, $skippedProperties); foreach ($identifierFields as $idField => $reflector) { if (! isset($identifier[$idField])) { diff --git a/tests/Tests/Models/ECommerce/ECommerceProduct2.php b/tests/Tests/Models/ECommerce/ECommerceProduct2.php new file mode 100644 index 00000000000..89f37417d2b --- /dev/null +++ b/tests/Tests/Models/ECommerce/ECommerceProduct2.php @@ -0,0 +1,52 @@ +id; + } + + public function getName(): string + { + return $this->name; + } + + public function __clone() + { + $this->id = null; + $this->name = 'Clone of ' . $this->name; + } +} diff --git a/tests/Tests/ORM/Functional/ProxiesLikeEntitiesTest.php b/tests/Tests/ORM/Functional/ProxiesLikeEntitiesTest.php index 01f82c8de7d..1cd05c3fc5a 100644 --- a/tests/Tests/ORM/Functional/ProxiesLikeEntitiesTest.php +++ b/tests/Tests/ORM/Functional/ProxiesLikeEntitiesTest.php @@ -58,7 +58,7 @@ protected function setUp(): void public function testPersistUpdate(): void { // Considering case (a) - $proxy = $this->_em->getProxyFactory()->getProxy(CmsUser::class, ['id' => 123]); + $proxy = $this->_em->getProxyFactory()->getProxy(CmsUser::class, ['id' => $this->user->getId()]); $proxy->id = null; $proxy->username = 'ocra'; diff --git a/tests/Tests/ORM/Functional/ReferenceProxyTest.php b/tests/Tests/ORM/Functional/ReferenceProxyTest.php index 88c14253e20..bb4a2cfb36c 100644 --- a/tests/Tests/ORM/Functional/ReferenceProxyTest.php +++ b/tests/Tests/ORM/Functional/ReferenceProxyTest.php @@ -9,6 +9,7 @@ use Doctrine\ORM\Proxy\InternalProxy; use Doctrine\Tests\Models\Company\CompanyAuction; use Doctrine\Tests\Models\ECommerce\ECommerceProduct; +use Doctrine\Tests\Models\ECommerce\ECommerceProduct2; use Doctrine\Tests\Models\ECommerce\ECommerceShipping; use Doctrine\Tests\OrmFunctionalTestCase; @@ -112,6 +113,24 @@ public function testCloneProxy(): void self::assertFalse($entity->isCloned); } + public function testCloneProxyWithResetId(): void + { + $id = $this->createProduct(); + + $entity = $this->_em->getReference(ECommerceProduct2::class, $id); + assert($entity instanceof ECommerceProduct2); + + $clone = clone $entity; + assert($clone instanceof ECommerceProduct2); + + self::assertEquals($id, $entity->getId()); + self::assertEquals('Doctrine Cookbook', $entity->getName()); + + self::assertFalse($this->_em->contains($clone)); + self::assertNull($clone->getId()); + self::assertEquals('Clone of Doctrine Cookbook', $clone->getName()); + } + /** @group DDC-733 */ public function testInitializeProxy(): void { From 93c2dd9d4b74dd78fe17834b2be70df88cca55c9 Mon Sep 17 00:00:00 2001 From: Indra Gunawan Date: Mon, 20 May 2024 18:41:40 +0800 Subject: [PATCH 04/15] update EntityManager#transactional to EntityManager#wrapInTransaction One has been deprecated in favor of the other. --- .../transactions-and-concurrency.rst | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/docs/en/reference/transactions-and-concurrency.rst b/docs/en/reference/transactions-and-concurrency.rst index 9e477474280..afcbb216bce 100644 --- a/docs/en/reference/transactions-and-concurrency.rst +++ b/docs/en/reference/transactions-and-concurrency.rst @@ -88,7 +88,7 @@ requirement. A more convenient alternative for explicit transaction demarcation is the use of provided control abstractions in the form of -``Connection#transactional($func)`` and ``EntityManager#transactional($func)``. +``Connection#transactional($func)`` and ``EntityManager#wrapInTransaction($func)``. When used, these control abstractions ensure that you never forget to rollback the transaction, in addition to the obvious code reduction. An example that is functionally equivalent to the previously shown code looks as follows: @@ -96,21 +96,23 @@ functionally equivalent to the previously shown code looks as follows: .. code-block:: php transactional(function($conn) { + // ... do some work + $user = new User; + $user->setName('George'); + }); + + // transactional with EntityManager instance // $em instanceof EntityManager - $em->transactional(function($em) { + $em->wrapInTransaction(function($em) { // ... do some work $user = new User; $user->setName('George'); $em->persist($user); }); -.. warning:: - - For historical reasons, ``EntityManager#transactional($func)`` will return - ``true`` whenever the return value of ``$func`` is loosely false. - Some examples of this include ``array()``, ``"0"``, ``""``, ``0``, and - ``null``. - The difference between ``Connection#transactional($func)`` and ``EntityManager#transactional($func)`` is that the latter abstraction flushes the ``EntityManager`` prior to transaction From 06eca401340d3a4387a940f5777c68b8f0b0b3fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20Paris?= Date: Sun, 5 May 2024 23:22:29 +0200 Subject: [PATCH 05/15] Use ramsey/composer-install in PHPBench workflow It will handle caching for us. --- .github/workflows/phpbench.yml | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/.github/workflows/phpbench.yml b/.github/workflows/phpbench.yml index 1e7ad8c10d1..b223e635930 100644 --- a/.github/workflows/phpbench.yml +++ b/.github/workflows/phpbench.yml @@ -47,15 +47,8 @@ jobs: coverage: "pcov" ini-values: "zend.assertions=1, apc.enable_cli=1" - - name: "Cache dependencies installed with composer" - uses: "actions/cache@v3" - with: - path: "~/.composer/cache" - key: "php-${{ matrix.php-version }}-composer-locked-${{ hashFiles('composer.lock') }}" - restore-keys: "php-${{ matrix.php-version }}-composer-locked-" - - - name: "Install dependencies with composer" - run: "composer update --no-interaction --no-progress" + - name: "Install dependencies with Composer" + uses: "ramsey/composer-install@v3" - name: "Run PHPBench" run: "vendor/bin/phpbench run --report=default" From 59c8bc09abf1f878694346986dc0755a6aed9fd2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20Paris?= Date: Mon, 3 Jun 2024 23:08:27 +0200 Subject: [PATCH 06/15] Replace assertion with exception (#11489) --- src/Query/Parser.php | 5 ++- .../ORM/Functional/Ticket/GH11487Test.php | 40 +++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 tests/Tests/ORM/Functional/Ticket/GH11487Test.php diff --git a/src/Query/Parser.php b/src/Query/Parser.php index 949a8f4ebdd..10cb008d12a 100644 --- a/src/Query/Parser.php +++ b/src/Query/Parser.php @@ -2924,7 +2924,10 @@ public function ArithmeticPrimary() return new AST\ParenthesisExpression($expr); } - assert($this->lexer->lookahead !== null); + if ($this->lexer->lookahead === null) { + $this->syntaxError('ArithmeticPrimary'); + } + switch ($this->lexer->lookahead->type) { case TokenType::T_COALESCE: case TokenType::T_NULLIF: diff --git a/tests/Tests/ORM/Functional/Ticket/GH11487Test.php b/tests/Tests/ORM/Functional/Ticket/GH11487Test.php new file mode 100644 index 00000000000..ef036e48ada --- /dev/null +++ b/tests/Tests/ORM/Functional/Ticket/GH11487Test.php @@ -0,0 +1,40 @@ +expectException(QueryException::class); + $this->expectExceptionMessage('Syntax Error'); + $this->_em->createQuery('UPDATE Doctrine\Tests\ORM\Functional\Ticket\TaxType t SET t.default =')->execute(); + } +} + +/** @Entity */ +class TaxType +{ + /** + * @var int|null + * @Column(type="integer") + * @Id + * @GeneratedValue + */ + public $id; + + /** + * @var bool + * @Column(type="boolean") + */ + public $default = false; +} From 87a8ee21c9eb88bb7037ad66b339816c24f6f2d7 Mon Sep 17 00:00:00 2001 From: Sam Mousa Date: Tue, 11 Jun 2024 16:21:28 +0200 Subject: [PATCH 07/15] fix(docs): use string value in `addAttribute` --- docs/en/tutorials/composite-primary-keys.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/en/tutorials/composite-primary-keys.rst b/docs/en/tutorials/composite-primary-keys.rst index 456adeaf5de..386f8f140c0 100644 --- a/docs/en/tutorials/composite-primary-keys.rst +++ b/docs/en/tutorials/composite-primary-keys.rst @@ -188,7 +188,7 @@ We keep up the example of an Article with arbitrary attributes, the mapping look #[OneToMany(targetEntity: ArticleAttribute::class, mappedBy: 'article', cascade: ['ALL'], indexBy: 'attribute')] private Collection $attributes; - public function addAttribute(string $name, ArticleAttribute $value): void + public function addAttribute(string $name, string $value): void { $this->attributes[$name] = new ArticleAttribute($name, $value, $this); } From 39153fd88a8c3cbc008b7ab6ff19899d48c07aa1 Mon Sep 17 00:00:00 2001 From: Daniel Black Date: Fri, 14 Jun 2024 03:34:46 +1000 Subject: [PATCH 08/15] ci: maintained and stable mariadb version (11.4 current lts) (#11490) Also use MARIADB env names and the healthcheck.sh included in the container. --- .github/workflows/continuous-integration.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 47453fdae52..c6c3cb752aa 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -190,7 +190,7 @@ jobs: - "default" - "3@dev" mariadb-version: - - "10.9" + - "11.4" extension: - "mysqli" - "pdo_mysql" @@ -204,11 +204,11 @@ jobs: mariadb: image: "mariadb:${{ matrix.mariadb-version }}" env: - MYSQL_ALLOW_EMPTY_PASSWORD: yes - MYSQL_DATABASE: "doctrine_tests" + MARIADB_ALLOW_EMPTY_ROOT_PASSWORD: yes + MARIADB_DATABASE: "doctrine_tests" options: >- - --health-cmd "mysqladmin ping --silent" + --health-cmd "healthcheck.sh --connect --innodb_initialized" ports: - "3306:3306" From 3b499132d9560263c4d93ea623fbf66abd186ba6 Mon Sep 17 00:00:00 2001 From: Noemi Salaun Date: Sun, 28 Jan 2024 18:32:39 +0100 Subject: [PATCH 09/15] Skip joined entity creation for empty relation (#10889) --- src/Internal/Hydration/ObjectHydrator.php | 12 +- .../ORM/Functional/Ticket/GH10889Test.php | 107 ++++++++++++++++++ 2 files changed, 115 insertions(+), 4 deletions(-) create mode 100644 tests/Tests/ORM/Functional/Ticket/GH10889Test.php diff --git a/src/Internal/Hydration/ObjectHydrator.php b/src/Internal/Hydration/ObjectHydrator.php index a5d97d30966..c01496a5ca8 100644 --- a/src/Internal/Hydration/ObjectHydrator.php +++ b/src/Internal/Hydration/ObjectHydrator.php @@ -367,11 +367,15 @@ protected function hydrateRowData(array $row, array &$result) $parentObject = $this->resultPointers[$parentAlias]; } else { // Parent object of relation not found, mark as not-fetched again - $element = $this->getEntity($data, $dqlAlias); + 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; + // 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]); diff --git a/tests/Tests/ORM/Functional/Ticket/GH10889Test.php b/tests/Tests/ORM/Functional/Ticket/GH10889Test.php new file mode 100644 index 00000000000..451fc887d20 --- /dev/null +++ b/tests/Tests/ORM/Functional/Ticket/GH10889Test.php @@ -0,0 +1,107 @@ +createSchemaForModels( + GH10889Person::class, + GH10889Company::class, + GH10889Resume::class + ); + } + + public function testIssue(): void + { + $person = new GH10889Person(); + $resume = new GH10889Resume($person, null); + + $this->_em->persist($person); + $this->_em->persist($resume); + $this->_em->flush(); + $this->_em->clear(); + + /** @var list $resumes */ + $resumes = $this->_em + ->getRepository(GH10889Resume::class) + ->createQueryBuilder('resume') + ->leftJoin('resume.currentCompany', 'company')->addSelect('company') + ->getQuery() + ->getResult(); + + $this->assertArrayHasKey(0, $resumes); + $this->assertEquals(1, $resumes[0]->person->id); + $this->assertNull($resumes[0]->currentCompany); + } +} + +/** + * @ORM\Entity + */ +class GH10889Person +{ + /** + * @ORM\Id + * @ORM\Column(type="integer") + * @ORM\GeneratedValue + * + * @var int + */ + public $id; +} + +/** + * @ORM\Entity + */ +class GH10889Company +{ + /** + * @ORM\Id + * @ORM\Column(type="integer") + * @ORM\GeneratedValue + * + * @var int + */ + public $id; +} + +/** + * @ORM\Entity + */ +class GH10889Resume +{ + /** + * @ORM\Id + * @ORM\OneToOne(targetEntity="GH10889Person") + * + * @var GH10889Person + */ + public $person; + + /** + * @ORM\ManyToOne(targetEntity="GH10889Company") + * + * @var GH10889Company|null + */ + public $currentCompany; + + public function __construct(GH10889Person $person, ?GH10889Company $currentCompany) + { + $this->person = $person; + $this->currentCompany = $currentCompany; + } +} From e4d46c4276b48cece92443c69990a6d12bd4effa Mon Sep 17 00:00:00 2001 From: Kyron Taylor Date: Sat, 15 Jun 2024 00:45:36 +0100 Subject: [PATCH 10/15] Fix OneToManyPersister::deleteEntityCollection missing discriminator column/value. (GH-11500) --- .../Collection/OneToManyPersister.php | 15 +- .../ORM/Functional/Ticket/GH11500Test.php | 137 ++++++++++++++++++ 2 files changed, 151 insertions(+), 1 deletion(-) create mode 100644 tests/Tests/ORM/Functional/Ticket/GH11500Test.php diff --git a/src/Persisters/Collection/OneToManyPersister.php b/src/Persisters/Collection/OneToManyPersister.php index f39415fc0c9..6769acca909 100644 --- a/src/Persisters/Collection/OneToManyPersister.php +++ b/src/Persisters/Collection/OneToManyPersister.php @@ -8,6 +8,8 @@ use Doctrine\Common\Collections\Criteria; use Doctrine\DBAL\Exception as DBALException; use Doctrine\DBAL\Types\Type; +use Doctrine\ORM\EntityNotFoundException; +use Doctrine\ORM\Mapping\MappingException; use Doctrine\ORM\PersistentCollection; use Doctrine\ORM\Utility\PersisterHelper; @@ -166,7 +168,11 @@ public function loadCriteria(PersistentCollection $collection, Criteria $criteri throw new BadMethodCallException('Filtering a collection by Criteria is not supported by this CollectionPersister.'); } - /** @throws DBALException */ + /** + * @throws DBALException + * @throws EntityNotFoundException + * @throws MappingException + */ private function deleteEntityCollection(PersistentCollection $collection): int { $mapping = $collection->getMapping(); @@ -186,6 +192,13 @@ private function deleteEntityCollection(PersistentCollection $collection): int $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)); diff --git a/tests/Tests/ORM/Functional/Ticket/GH11500Test.php b/tests/Tests/ORM/Functional/Ticket/GH11500Test.php new file mode 100644 index 00000000000..4be3bf2b76e --- /dev/null +++ b/tests/Tests/ORM/Functional/Ticket/GH11500Test.php @@ -0,0 +1,137 @@ +setUpEntitySchema([ + GH11500AbstractTestEntity::class, + GH11500TestEntityOne::class, + GH11500TestEntityTwo::class, + GH11500TestEntityHolder::class, + ]); + } + + /** + * @throws ORMException + */ + public function testDeleteOneToManyCollectionWithSingleTableInheritance(): void + { + $testEntityOne = new GH11500TestEntityOne(); + $testEntityTwo = new GH11500TestEntityTwo(); + $testEntityHolder = new GH11500TestEntityHolder(); + + $testEntityOne->testEntityHolder = $testEntityHolder; + $testEntityHolder->testEntityOnes->add($testEntityOne); + + $testEntityTwo->testEntityHolder = $testEntityHolder; + $testEntityHolder->testEntityTwos->add($testEntityTwo); + + $em = $this->getEntityManager(); + $em->persist($testEntityOne); + $em->persist($testEntityTwo); + $em->persist($testEntityHolder); + $em->flush(); + + $testEntityTwosBeforeRemovalOfTestEntityOnes = $testEntityHolder->testEntityTwos->toArray(); + + $testEntityHolder->testEntityOnes = new ArrayCollection(); + $em->persist($testEntityHolder); + $em->flush(); + $em->refresh($testEntityHolder); + + static::assertEmpty($testEntityHolder->testEntityOnes->toArray(), 'All records should have been deleted'); + static::assertEquals($testEntityTwosBeforeRemovalOfTestEntityOnes, $testEntityHolder->testEntityTwos->toArray(), 'Different Entity\'s records should not have been deleted'); + } +} + + + +/** + * @ORM\Entity + * @ORM\Table(name="one_to_many_single_table_inheritance_test_entities") + * @ORM\InheritanceType("SINGLE_TABLE") + * @ORM\DiscriminatorColumn(name="type", type="string") + * @ORM\DiscriminatorMap({"test_entity_one"="GH11500TestEntityOne", "test_entity_two"="GH11500TestEntityTwo"}) + */ +class GH11500AbstractTestEntity +{ + /** + * @ORM\Id + * @ORM\Column(type="integer") + * @ORM\GeneratedValue + * + * @var int + */ + public $id; +} + + +/** @ORM\Entity */ +class GH11500TestEntityOne extends GH11500AbstractTestEntity +{ + /** + * @ORM\ManyToOne(targetEntity="GH11500TestEntityHolder", inversedBy="testEntityOnes") + * @ORM\JoinColumn(name="test_entity_holder_id", referencedColumnName="id") + * + * @var GH11500TestEntityHolder + */ + public $testEntityHolder; +} + +/** @ORM\Entity */ +class GH11500TestEntityTwo extends GH11500AbstractTestEntity +{ + /** + * @ORM\ManyToOne(targetEntity="GH11500TestEntityHolder", inversedBy="testEntityTwos") + * @ORM\JoinColumn(name="test_entity_holder_id", referencedColumnName="id") + * + * @var GH11500TestEntityHolder + */ + public $testEntityHolder; +} + +/** @ORM\Entity */ +class GH11500TestEntityHolder +{ + /** + * @ORM\Id + * @ORM\Column(type="integer") + * @ORM\GeneratedValue + * + * @var int + */ + public $id; + + /** + * @ORM\OneToMany(targetEntity="GH11500TestEntityOne", mappedBy="testEntityHolder", orphanRemoval=true) + * + * @var Collection + */ + public $testEntityOnes; + + /** + * @ORM\OneToMany(targetEntity="GH11500TestEntityTwo", mappedBy="testEntityHolder", orphanRemoval=true) + * + * @var Collection + */ + public $testEntityTwos; + + public function __construct() + { + $this->testEntityOnes = new ArrayCollection(); + $this->testEntityTwos = new ArrayCollection(); + } +} From 2ea6a1a5fb0ad61906b32632057ce7f996312b5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20Paris?= Date: Wed, 19 Jun 2024 21:45:34 +0200 Subject: [PATCH 11/15] Remove unneeded CS rule --- phpcs.xml.dist | 1 - src/Mapping/InverseJoinColumn.php | 1 - 2 files changed, 2 deletions(-) diff --git a/phpcs.xml.dist b/phpcs.xml.dist index 6de9ebe57a2..fa28a585b6e 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -14,7 +14,6 @@ src tests - */src/Mapping/InverseJoinColumn.php */tests/Tests/Proxies/__CG__* */tests/Tests/ORM/Tools/Export/export/* diff --git a/src/Mapping/InverseJoinColumn.php b/src/Mapping/InverseJoinColumn.php index 89c8db0006b..2a77f3fc73b 100644 --- a/src/Mapping/InverseJoinColumn.php +++ b/src/Mapping/InverseJoinColumn.php @@ -2,7 +2,6 @@ declare(strict_types=1); - namespace Doctrine\ORM\Mapping; use Attribute; From ed53defaa17bb8fb420b88d47a49d373338de534 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20Paris?= Date: Wed, 19 Jun 2024 21:59:29 +0200 Subject: [PATCH 12/15] Deprecate DatabaseDriver It was used for the reverse engineering feature, which has been removed. --- UPGRADE.md | 6 ++++++ src/Mapping/Driver/DatabaseDriver.php | 2 ++ 2 files changed, 8 insertions(+) diff --git a/UPGRADE.md b/UPGRADE.md index 1869e9f28ff..189d8d0a1bf 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -1,3 +1,9 @@ +# Upgrade to 3.3 + +## Deprecate `DatabaseDriver` + +The class `Doctrine\ORM\Mapping\Driver\DatabaseDriver` is deprecated without replacement. + # Upgrade to 3.2 ## Deprecate the `NotSupported` exception diff --git a/src/Mapping/Driver/DatabaseDriver.php b/src/Mapping/Driver/DatabaseDriver.php index 49e2e93520c..301e44a97e2 100644 --- a/src/Mapping/Driver/DatabaseDriver.php +++ b/src/Mapping/Driver/DatabaseDriver.php @@ -35,6 +35,8 @@ /** * The DatabaseDriver reverse engineers the mapping metadata from a database. * + * @deprecated No replacement planned + * * @link www.doctrine-project.org */ class DatabaseDriver implements MappingDriver From c06f6b93761a12380e57d5d241eb0d6e22a0d3df Mon Sep 17 00:00:00 2001 From: Christophe Coevoet Date: Thu, 20 Jun 2024 09:08:10 +0200 Subject: [PATCH 13/15] Add the propoer void return type on the __load method of proxies When using ghost objects, the method was leaking a `static` return type due to the way it was implemented, which is incompatible with the native return type that will be added in doctrine/persistence v4. --- src/Proxy/ProxyFactory.php | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/Proxy/ProxyFactory.php b/src/Proxy/ProxyFactory.php index b5426a014e4..b2d114a6698 100644 --- a/src/Proxy/ProxyFactory.php +++ b/src/Proxy/ProxyFactory.php @@ -387,12 +387,18 @@ private function generateUseLazyGhostTrait(ClassMetadata $class): string $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 __load; + initializeLazyObject as private; setLazyObjectAsInitialized as public __setInitialized; isLazyObjectInitialized as private; createLazyGhost as private; resetLazyObject as private; - }'), $code); + } + + public function __load(): void + { + $this->initializeLazyObject(); + } + '), $code); return $code; } From 77467cd824091102cf8f82a9ff582c474d312ed2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20Paris?= Date: Thu, 20 Jun 2024 21:47:54 +0200 Subject: [PATCH 14/15] Address doctrine/persistence 3.3.3 release FileDriver became templatable, and some very wrong phpdoc has been fixed, causing Psalm to better understand the 2 FileDriver classes in this project. --- phpstan-dbal2.neon | 6 ++++ phpstan-persistence2.neon | 6 ++++ psalm-baseline.xml | 47 ------------------------------- src/Mapping/Driver/XmlDriver.php | 5 +++- src/Mapping/Driver/YamlDriver.php | 2 ++ 5 files changed, 18 insertions(+), 48 deletions(-) diff --git a/phpstan-dbal2.neon b/phpstan-dbal2.neon index 31f0f2823ab..98cba7badc1 100644 --- a/phpstan-dbal2.neon +++ b/phpstan-dbal2.neon @@ -74,3 +74,9 @@ parameters: - message: '#^Call to method injectObjectManager\(\) on an unknown class Doctrine\\Persistence\\ObjectManagerAware\.$#' path: src/UnitOfWork.php + + - + message: '#contains generic type.*but class.*is not generic#' + paths: + - src/Mapping/Driver/XmlDriver.php + - src/Mapping/Driver/YamlDriver.php diff --git a/phpstan-persistence2.neon b/phpstan-persistence2.neon index dfa68ac9ec8..931876129c3 100644 --- a/phpstan-persistence2.neon +++ b/phpstan-persistence2.neon @@ -64,3 +64,9 @@ parameters: # Symfony cache supports passing a key prefix to the clear method. - '/^Method Psr\\Cache\\CacheItemPoolInterface\:\:clear\(\) invoked with 1 parameter, 0 required\.$/' + + - + message: '#contains generic type.*but class.*is not generic#' + paths: + - src/Mapping/Driver/XmlDriver.php + - src/Mapping/Driver/YamlDriver.php diff --git a/psalm-baseline.xml b/psalm-baseline.xml index 2be2ff0b8c3..b9473bebb66 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -932,13 +932,8 @@ table]]> - - {'discriminator-column'}]]> - {'discriminator-map'}]]> - - $usage, 'region' => $region, @@ -962,7 +957,6 @@ * options?: array * }]]> - @@ -971,15 +965,6 @@ - - {'discriminator-column'}]]> - {'discriminator-map'}]]> - - - getName() === 'embeddable']]> - getName() === 'entity']]> - getName() === 'mapped-superclass']]> - @@ -1011,38 +996,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/Mapping/Driver/XmlDriver.php b/src/Mapping/Driver/XmlDriver.php index 60223020dc9..c993a2ba794 100644 --- a/src/Mapping/Driver/XmlDriver.php +++ b/src/Mapping/Driver/XmlDriver.php @@ -37,6 +37,8 @@ * XmlDriver is a metadata driver that enables mapping through XML files. * * @link www.doctrine-project.org + * + * @template-extends FileDriver */ class XmlDriver extends FileDriver { @@ -79,7 +81,6 @@ public function __construct($locator, $fileExtension = self::DEFAULT_FILE_EXTENS public function loadMetadataForClass($className, PersistenceClassMetadata $metadata) { $xmlRoot = $this->getElement($className); - assert($xmlRoot instanceof SimpleXMLElement); if ($xmlRoot->getName() === 'entity') { if (isset($xmlRoot['repository-class'])) { @@ -203,6 +204,7 @@ public function loadMetadataForClass($className, PersistenceClassMetadata $metad ]; if (isset($discrColumn['options'])) { + assert($discrColumn['options'] instanceof SimpleXMLElement); $columnDef['options'] = $this->parseOptions($discrColumn['options']->children()); } @@ -214,6 +216,7 @@ public function loadMetadataForClass($className, PersistenceClassMetadata $metad // 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']; } diff --git a/src/Mapping/Driver/YamlDriver.php b/src/Mapping/Driver/YamlDriver.php index c6ff83e0f17..5e24b0b3a45 100644 --- a/src/Mapping/Driver/YamlDriver.php +++ b/src/Mapping/Driver/YamlDriver.php @@ -31,6 +31,8 @@ * The YamlDriver reads the mapping metadata from yaml schema files. * * @deprecated 2.7 This class is being removed from the ORM and won't have any replacement + * + * @template-extends FileDriver> */ class YamlDriver extends FileDriver { From 1153b9468ccfef452625064c66b4a135d0ea8a46 Mon Sep 17 00:00:00 2001 From: "Alexander M. Turek" Date: Fri, 21 Jun 2024 13:31:45 +0200 Subject: [PATCH 15/15] Fix deprecated array access usage (#11517) --- src/Persisters/Collection/OneToManyPersister.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Persisters/Collection/OneToManyPersister.php b/src/Persisters/Collection/OneToManyPersister.php index 7354daa5627..0727b1f8a7e 100644 --- a/src/Persisters/Collection/OneToManyPersister.php +++ b/src/Persisters/Collection/OneToManyPersister.php @@ -174,9 +174,9 @@ private function deleteEntityCollection(PersistentCollection $collection): int if ($targetClass->isInheritanceTypeSingleTable()) { $discriminatorColumn = $targetClass->getDiscriminatorColumn(); - $statement .= ' AND ' . $discriminatorColumn['name'] . ' = ?'; + $statement .= ' AND ' . $discriminatorColumn->name . ' = ?'; $parameters[] = $targetClass->discriminatorValue; - $types[] = $discriminatorColumn['type']; + $types[] = $discriminatorColumn->type; } $numAffected = $this->conn->executeStatement($statement, $parameters, $types);