From 0a49274f9b92426e17dd4a5fdf3aae41dd24c59f Mon Sep 17 00:00:00 2001 From: d-ph <4347095+d-ph@users.noreply.github.com> Date: Fri, 8 Mar 2024 09:36:42 +0000 Subject: [PATCH 01/21] Update aggregate-fields.rst Change `PESSIMISTIC_READ` to `PESSIMISTIC_WRITE`. Otherwise, the solution to the race condition at the bottom of the article would allow concurrent reads, which would not solve the presented race condition problem. --- docs/en/cookbook/aggregate-fields.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/en/cookbook/aggregate-fields.rst b/docs/en/cookbook/aggregate-fields.rst index 001d70d34b4..20bfa32896f 100644 --- a/docs/en/cookbook/aggregate-fields.rst +++ b/docs/en/cookbook/aggregate-fields.rst @@ -352,7 +352,7 @@ the database using a FOR UPDATE. use Bank\Entities\Account; use Doctrine\DBAL\LockMode; - $account = $em->find(Account::class, $accId, LockMode::PESSIMISTIC_READ); + $account = $em->find(Account::class, $accId, LockMode::PESSIMISTIC_WRITE); Keeping Updates and Deletes in Sync ----------------------------------- From 129553da906ee98f4b517dd1a0f6edc519bb29c3 Mon Sep 17 00:00:00 2001 From: Maciej Malarz Date: Thu, 11 Jul 2024 11:50:04 +0200 Subject: [PATCH 02/21] Allow overriding association's cascade --- src/Mapping/ClassMetadata.php | 6 +++++- tests/Tests/ORM/Mapping/ClassMetadataTest.php | 12 +++++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/Mapping/ClassMetadata.php b/src/Mapping/ClassMetadata.php index f58e00e72fe..9c2ca97f637 100644 --- a/src/Mapping/ClassMetadata.php +++ b/src/Mapping/ClassMetadata.php @@ -1676,7 +1676,7 @@ public function setInheritanceType(int $type): void /** * Sets the association to override association mapping of property for an entity relationship. * - * @psalm-param array $overrideMapping + * @psalm-param array{joinColumns?: array, inversedBy?: ?string, joinTable?: array, fetch?: ?string, cascade?: string[]} $overrideMapping * * @throws MappingException */ @@ -1712,6 +1712,10 @@ public function setAssociationOverride(string $fieldName, array $overrideMapping $mapping['fetch'] = $overrideMapping['fetch']; } + if (isset($overrideMapping['cascade'])) { + $mapping['cascade'] = $overrideMapping['cascade']; + } + switch ($mapping['type']) { case self::ONE_TO_ONE: case self::MANY_TO_ONE: diff --git a/tests/Tests/ORM/Mapping/ClassMetadataTest.php b/tests/Tests/ORM/Mapping/ClassMetadataTest.php index a7fb88b2e03..71fb210021c 100644 --- a/tests/Tests/ORM/Mapping/ClassMetadataTest.php +++ b/tests/Tests/ORM/Mapping/ClassMetadataTest.php @@ -870,13 +870,23 @@ public function testAssociationOverrideKeepsDeclaringClass(): void { $cm = new ClassMetadata(Directory::class); $cm->mapManyToOne(['fieldName' => 'parentDirectory', 'targetEntity' => Directory::class, 'cascade' => ['remove'], 'declared' => Directory::class]); - $cm->setAssociationOverride('parentDirectory', ['cascade' => '']); + $cm->setAssociationOverride('parentDirectory', ['cascade' => ['remove']]); $mapping = $cm->getAssociationMapping('parentDirectory'); self::assertSame(Directory::class, $mapping->declared); } + public function testAssociationOverrideCanOverrideCascade(): void + { + $cm = new ClassMetadata(Directory::class); + $cm->mapManyToOne(['fieldName' => 'parentDirectory', 'targetEntity' => Directory::class, 'cascade' => ['remove'], 'declared' => Directory::class]); + $cm->setAssociationOverride('parentDirectory', ['cascade' => ['all']]); + + $mapping = $cm->getAssociationMapping('parentDirectory'); + self::assertSame(['remove', 'persist', 'refresh', 'detach'], $mapping['cascade']); + } + #[TestGroup('DDC-1955')] public function testInvalidEntityListenerClassException(): void { From e7efdede15e572cf7950b0372391cac01a51152b Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Fri, 18 Oct 2024 09:13:20 +0200 Subject: [PATCH 03/21] Update README.md --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index daef3b0c0e7..47a36ddb27e 100644 --- a/README.md +++ b/README.md @@ -3,8 +3,6 @@ | [![Build status][4.0 image]][4.0] | [![Build status][3.4 image]][3.4] | [![Build status][3.3 image]][3.3] | [![Build status][2.21 image]][2.21] | [![Build status][2.20 image]][2.20] | | [![Coverage Status][4.0 coverage image]][4.0 coverage] | [![Coverage Status][3.4 coverage image]][3.4 coverage] | [![Coverage Status][3.3 coverage image]][3.3 coverage] | [![Coverage Status][2.21 coverage image]][2.21 coverage] | [![Coverage Status][2.20 coverage image]][2.20 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), From 039b03255a80dc68ea6f04339c6e781f009ddfa2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20Paris?= Date: Sun, 20 Oct 2024 11:57:45 +0200 Subject: [PATCH 04/21] Inherit issue templates --- .github/ISSUE_TEMPLATE/BC_Break.md | 37 ---------------------- .github/ISSUE_TEMPLATE/Bug.md | 34 -------------------- .github/ISSUE_TEMPLATE/Feature_Request.md | 18 ----------- .github/ISSUE_TEMPLATE/Support_Question.md | 6 ---- .github/ISSUE_TEMPLATE/config.yml | 13 ++++++++ 5 files changed, 13 insertions(+), 95 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/BC_Break.md delete mode 100644 .github/ISSUE_TEMPLATE/Bug.md delete mode 100644 .github/ISSUE_TEMPLATE/Feature_Request.md delete mode 100644 .github/ISSUE_TEMPLATE/Support_Question.md create mode 100644 .github/ISSUE_TEMPLATE/config.yml diff --git a/.github/ISSUE_TEMPLATE/BC_Break.md b/.github/ISSUE_TEMPLATE/BC_Break.md deleted file mode 100644 index 9fdd4dd9229..00000000000 --- a/.github/ISSUE_TEMPLATE/BC_Break.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -name: πŸ’₯ BC Break -about: Have you encountered an issue during upgrade? πŸ’£ ---- - - - -### BC Break Report - - - -| Q | A -|------------ | ------ -| BC Break | yes -| Version | x.y.z - -#### Summary - - - -#### Previous behavior - - - -#### Current behavior - - - -#### How to reproduce - - diff --git a/.github/ISSUE_TEMPLATE/Bug.md b/.github/ISSUE_TEMPLATE/Bug.md deleted file mode 100644 index c65ac467ac7..00000000000 --- a/.github/ISSUE_TEMPLATE/Bug.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -name: 🐞 Bug Report -about: Something is broken? πŸ”¨ ---- - -### Bug Report - - - -| Q | A -|------------ | ------ -| BC Break | yes/no -| Version | x.y.z - -#### Summary - - - -#### Current behavior - - - -#### How to reproduce - - - -#### Expected behavior - - - diff --git a/.github/ISSUE_TEMPLATE/Feature_Request.md b/.github/ISSUE_TEMPLATE/Feature_Request.md deleted file mode 100644 index 2620581d684..00000000000 --- a/.github/ISSUE_TEMPLATE/Feature_Request.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -name: πŸŽ‰ Feature Request -about: You have a neat idea that should be implemented? 🎩 ---- - -### Feature Request - - - -| Q | A -|------------ | ------ -| New Feature | yes -| RFC | yes/no -| BC Break | yes/no - -#### Summary - - diff --git a/.github/ISSUE_TEMPLATE/Support_Question.md b/.github/ISSUE_TEMPLATE/Support_Question.md deleted file mode 100644 index e584c3994c2..00000000000 --- a/.github/ISSUE_TEMPLATE/Support_Question.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -name: ❓ Support Question -about: Have a problem that you can't figure out? πŸ€” ---- - -Please use https://github.com/doctrine/orm/discussions instead. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000000..8842420d9af --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,13 @@ +contact_links: + - name: Github Discussions + url: https://github.com/doctrine/orm/discussions + about: ORM-specific support discussions. + - name: Doctrine Slack + url: https://www.doctrine-project.org/slack + about: Join the Doctrine Slack to chat with other Doctrine users and contributors. + - name: Doctrine channel on Symfony Slack + url: https://symfony-devs.slack.com/messages/C3FQPE6LE/ + about: 'Communicate with other Symfony developers that use Doctrine in the #doctrine slack channel in the Symfony Devs slack instance.' + - name: Laravel Slack + url: http://slack.laraveldoctrine.org/ + about: Communicate with other Laravel developers that use Doctrine in the Laravel Doctrine slack instance. From 4d821cb13963cf06f2266d5379827d8e5c92b2c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20Paris?= Date: Sun, 20 Oct 2024 12:15:32 +0200 Subject: [PATCH 05/21] Remove config.yml It causes the template inheritance to fail. --- .github/ISSUE_TEMPLATE/config.yml | 13 ------------- 1 file changed, 13 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/config.yml diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml deleted file mode 100644 index 8842420d9af..00000000000 --- a/.github/ISSUE_TEMPLATE/config.yml +++ /dev/null @@ -1,13 +0,0 @@ -contact_links: - - name: Github Discussions - url: https://github.com/doctrine/orm/discussions - about: ORM-specific support discussions. - - name: Doctrine Slack - url: https://www.doctrine-project.org/slack - about: Join the Doctrine Slack to chat with other Doctrine users and contributors. - - name: Doctrine channel on Symfony Slack - url: https://symfony-devs.slack.com/messages/C3FQPE6LE/ - about: 'Communicate with other Symfony developers that use Doctrine in the #doctrine slack channel in the Symfony Devs slack instance.' - - name: Laravel Slack - url: http://slack.laraveldoctrine.org/ - about: Communicate with other Laravel developers that use Doctrine in the Laravel Doctrine slack instance. From c9253ef64b60cdee969cef2476439fed4db31fbc Mon Sep 17 00:00:00 2001 From: Kevin Bond Date: Thu, 2 Nov 2023 13:46:15 -0400 Subject: [PATCH 06/21] feat: make `PersistentCollection::first()` "extra" lazy --- docs/en/tutorials/extra-lazy-associations.rst | 1 + src/PersistentCollection.php | 14 +++++ .../Functional/ExtraLazyCollectionTest.php | 57 +++++++++++++++++++ 3 files changed, 72 insertions(+) diff --git a/docs/en/tutorials/extra-lazy-associations.rst b/docs/en/tutorials/extra-lazy-associations.rst index 4c31357eecc..1dae001a36c 100644 --- a/docs/en/tutorials/extra-lazy-associations.rst +++ b/docs/en/tutorials/extra-lazy-associations.rst @@ -17,6 +17,7 @@ can be called without triggering a full load of the collection: - ``Collection#contains($entity)`` - ``Collection#containsKey($key)`` - ``Collection#count()`` +- ``Collection#first()`` - ``Collection#get($key)`` - ``Collection#slice($offset, $length = null)`` diff --git a/src/PersistentCollection.php b/src/PersistentCollection.php index d54d3d1b997..ef5f28c3cb3 100644 --- a/src/PersistentCollection.php +++ b/src/PersistentCollection.php @@ -504,6 +504,20 @@ public function __wakeup(): void $this->em = null; } + /** + * {@inheritDoc} + */ + public function first() + { + if (! $this->initialized && ! $this->isDirty && $this->getMapping()['fetch'] === ClassMetadata::FETCH_EXTRA_LAZY) { + $persister = $this->getUnitOfWork()->getCollectionPersister($this->getMapping()); + + return array_values($persister->slice($this, 0, 1))[0] ?? false; + } + + return parent::first(); + } + /** * Extracts a slice of $length elements starting at position $offset from the Collection. * diff --git a/tests/Tests/ORM/Functional/ExtraLazyCollectionTest.php b/tests/Tests/ORM/Functional/ExtraLazyCollectionTest.php index a092f6555cd..569ea135009 100644 --- a/tests/Tests/ORM/Functional/ExtraLazyCollectionTest.php +++ b/tests/Tests/ORM/Functional/ExtraLazyCollectionTest.php @@ -179,6 +179,63 @@ public function testCountOneToManyJoinedInheritance(): void self::assertCount(2, $otherClass->childClasses); } + #[Group('non-cacheable')] + public function testFirstWhenInitialized(): void + { + $user = $this->_em->find(CmsUser::class, $this->userId); + $this->getQueryLog()->reset()->enable(); + $user->groups->toArray(); + + self::assertTrue($user->groups->isInitialized()); + self::assertInstanceOf(CmsGroup::class, $user->groups->first()); + $this->assertQueryCount(1, 'Should only execute one query to initialize collection, no extra query for first().'); + } + + public function testFirstOnEmptyCollectionWhenInitialized(): void + { + foreach ($this->_em->getRepository(CmsGroup::class)->findAll() as $group) { + $this->_em->remove($group); + } + + $this->_em->flush(); + + $user = $this->_em->find(CmsUser::class, $this->userId); + $this->getQueryLog()->reset()->enable(); + $user->groups->toArray(); + + self::assertTrue($user->groups->isInitialized()); + self::assertFalse($user->groups->first()); + $this->assertQueryCount(1, 'Should only execute one query to initialize collection, no extra query for first().'); + } + + public function testFirstWhenNotInitialized(): void + { + $user = $this->_em->find(CmsUser::class, $this->userId); + $this->getQueryLog()->reset()->enable(); + + self::assertFalse($user->groups->isInitialized()); + self::assertInstanceOf(CmsGroup::class, $user->groups->first()); + self::assertFalse($user->groups->isInitialized()); + $this->assertQueryCount(1, 'Should only execute one query for first().'); + } + + public function testFirstOnEmptyCollectionWhenNotInitialized(): void + { + foreach ($this->_em->getRepository(CmsGroup::class)->findAll() as $group) { + $this->_em->remove($group); + } + + $this->_em->flush(); + + $user = $this->_em->find(CmsUser::class, $this->userId); + $this->getQueryLog()->reset()->enable(); + + self::assertFalse($user->groups->isInitialized()); + self::assertFalse($user->groups->first()); + self::assertFalse($user->groups->isInitialized()); + $this->assertQueryCount(1, 'Should only execute one query for first().'); + } + #[Group('DDC-546')] public function testFullSlice(): void { From 021a9cce3d751833a7188f2b8f74ed2ca9814161 Mon Sep 17 00:00:00 2001 From: Jan Nedbal Date: Tue, 22 Oct 2024 15:54:18 +0200 Subject: [PATCH 07/21] UPGRADE: mention `SqlWalker` deprecations in 3.3 (#11693) --- UPGRADE.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/UPGRADE.md b/UPGRADE.md index 6da5e0be57c..6b52e2d767f 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -4,6 +4,16 @@ The class `Doctrine\ORM\Mapping\Driver\DatabaseDriver` is deprecated without replacement. +## Add `Doctrine\ORM\Query\OutputWalker` interface, deprecate `Doctrine\ORM\Query\SqlWalker::getExecutor()` + +Output walkers should implement the new `\Doctrine\ORM\Query\OutputWalker` interface and create +`Doctrine\ORM\Query\Exec\SqlFinalizer` instances instead of `Doctrine\ORM\Query\Exec\AbstractSqlExecutor`s. +The output walker must not base its workings on the query `firstResult`/`maxResult` values, so that the +`SqlFinalizer` can be kept in the query cache and used regardless of the actual `firstResult`/`maxResult` values. +Any operation dependent on `firstResult`/`maxResult` should take place within the `SqlFinalizer::createExecutor()` +method. Details can be found at https://github.com/doctrine/orm/pull/11188. + + # Upgrade to 3.2 ## Deprecate the `NotSupported` exception From 439b4dacf415b743b0d40d65c490a4123759c520 Mon Sep 17 00:00:00 2001 From: Dzmitry Bannik Date: Tue, 22 Oct 2024 22:30:39 +0300 Subject: [PATCH 08/21] Is not correctly generated sql when changed/switched sqlFilter parameters CachedPersisterContext::$selectJoinSql should be clear or regenerated when sqlFilter changed The problem reproduce when in use fetch=EAGER and use additional sql filter on this property --- .../Entity/BasicEntityPersister.php | 6 +- .../ChangeFiltersTest.php | 142 ++++++++++++++++++ .../CompanySQLFilter.php | 26 ++++ .../Ticket/SwitchContextWithFilter/Order.php | 43 ++++++ .../Ticket/SwitchContextWithFilter/User.php | 35 +++++ 5 files changed, 251 insertions(+), 1 deletion(-) create mode 100644 tests/Tests/ORM/Functional/Ticket/SwitchContextWithFilter/ChangeFiltersTest.php create mode 100644 tests/Tests/ORM/Functional/Ticket/SwitchContextWithFilter/CompanySQLFilter.php create mode 100644 tests/Tests/ORM/Functional/Ticket/SwitchContextWithFilter/Order.php create mode 100644 tests/Tests/ORM/Functional/Ticket/SwitchContextWithFilter/User.php diff --git a/src/Persisters/Entity/BasicEntityPersister.php b/src/Persisters/Entity/BasicEntityPersister.php index 5ca00cb007e..9bd8afd3cc1 100644 --- a/src/Persisters/Entity/BasicEntityPersister.php +++ b/src/Persisters/Entity/BasicEntityPersister.php @@ -199,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. @@ -1271,7 +1274,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; } @@ -1378,6 +1381,7 @@ protected function getSelectColumnsSQL() } $this->currentPersisterContext->selectColumnListSql = implode(', ', $columnList); + $this->filterHash = $this->em->getFilters()->getHash(); return $this->currentPersisterContext->selectColumnListSql; } diff --git a/tests/Tests/ORM/Functional/Ticket/SwitchContextWithFilter/ChangeFiltersTest.php b/tests/Tests/ORM/Functional/Ticket/SwitchContextWithFilter/ChangeFiltersTest.php new file mode 100644 index 00000000000..7ce97442b28 --- /dev/null +++ b/tests/Tests/ORM/Functional/Ticket/SwitchContextWithFilter/ChangeFiltersTest.php @@ -0,0 +1,142 @@ +setUpEntitySchema([ + Order::class, + User::class, + ]); + } + + /** + * @return non-empty-array<"companyA"|"companyB", array{orderId: int, userId: int}> + */ + private function prepareData(): array + { + $user1 = new User(self::COMPANY_A); + $order1 = new Order($user1); + $user2 = new User(self::COMPANY_B); + $order2 = new Order($user2); + + $this->_em->persist($user1); + $this->_em->persist($order1); + $this->_em->persist($user2); + $this->_em->persist($order2); + $this->_em->flush(); + $this->_em->clear(); + + return [ + 'companyA' => ['orderId' => $order1->id, 'userId' => $user1->id], + 'companyB' => ['orderId' => $order2->id, 'userId' => $user2->id], + ]; + } + + public function testUseEnableDisableFilter(): void + { + $this->_em->getConfiguration()->addFilter(CompanySQLFilter::class, CompanySQLFilter::class); + $this->_em->getFilters()->enable(CompanySQLFilter::class)->setParameter('company', self::COMPANY_A); + + ['companyA' => $companyA, 'companyB' => $companyB] = $this->prepareData(); + + $order1 = $this->_em->find(Order::class, $companyA['orderId']); + + self::assertNotNull($order1->user, $this->generateMessage('Order1->User1 not found')); + self::assertEquals($companyA['userId'], $order1->user->id, $this->generateMessage('Order1->User1 != User1')); + + $this->_em->getFilters()->disable(CompanySQLFilter::class); + $this->_em->getFilters()->enable(CompanySQLFilter::class)->setParameter('company', self::COMPANY_B); + + $order2 = $this->_em->find(Order::class, $companyB['orderId']); + + self::assertNotNull($order2->user, $this->generateMessage('Order2->User2 not found')); + self::assertEquals($companyB['userId'], $order2->user->id, $this->generateMessage('Order2->User2 != User2')); + } + + public function testUseChangeFilterParameters(): void + { + $this->_em->getConfiguration()->addFilter(CompanySQLFilter::class, CompanySQLFilter::class); + $filter = $this->_em->getFilters()->enable(CompanySQLFilter::class); + + ['companyA' => $companyA, 'companyB' => $companyB] = $this->prepareData(); + + $filter->setParameter('company', self::COMPANY_A); + + $order1 = $this->_em->find(Order::class, $companyA['orderId']); + + self::assertNotNull($order1->user, $this->generateMessage('Order1->User1 not found')); + self::assertEquals($companyA['userId'], $order1->user->id, $this->generateMessage('Order1->User1 != User1')); + + $filter->setParameter('company', self::COMPANY_B); + + $order2 = $this->_em->find(Order::class, $companyB['orderId']); + + self::assertNotNull($order2->user, $this->generateMessage('Order2->User2 not found')); + self::assertEquals($companyB['userId'], $order2->user->id, $this->generateMessage('Order2->User2 != User2')); + } + + public function testUseQueryBuilder(): void + { + $this->_em->getConfiguration()->addFilter(CompanySQLFilter::class, CompanySQLFilter::class); + $filter = $this->_em->getFilters()->enable(CompanySQLFilter::class); + + ['companyA' => $companyA, 'companyB' => $companyB] = $this->prepareData(); + + $getOrderByIdCache = function (int $orderId): ?Order { + return $this->_em->createQueryBuilder() + ->select('orderMaster, user') + ->from(Order::class, 'orderMaster') + ->innerJoin('orderMaster.user', 'user') + ->where('orderMaster.id = :orderId') + ->setParameter('orderId', $orderId) + ->setCacheable(true) + ->getQuery() + ->setQueryCacheLifetime(10) + ->getOneOrNullResult(); + }; + + $filter->setParameter('company', self::COMPANY_A); + + $order = $getOrderByIdCache($companyB['orderId']); + self::assertNull($order); + + $order = $getOrderByIdCache($companyA['orderId']); + + self::assertInstanceOf(Order::class, $order); + self::assertInstanceOf(User::class, $order->user); + self::assertEquals($companyA['userId'], $order->user->id); + + $filter->setParameter('company', self::COMPANY_B); + + $order = $getOrderByIdCache($companyA['orderId']); + self::assertNull($order); + + $order = $getOrderByIdCache($companyB['orderId']); + + self::assertInstanceOf(Order::class, $order); + self::assertInstanceOf(User::class, $order->user); + self::assertEquals($companyB['userId'], $order->user->id); + } + + private function generateMessage(string $message): string + { + $log = $this->getLastLoggedQuery(); + + return sprintf("%s\nSQL: %s", $message, str_replace(['?'], (array) $log['params'], $log['sql'])); + } +} diff --git a/tests/Tests/ORM/Functional/Ticket/SwitchContextWithFilter/CompanySQLFilter.php b/tests/Tests/ORM/Functional/Ticket/SwitchContextWithFilter/CompanySQLFilter.php new file mode 100644 index 00000000000..e65188334ac --- /dev/null +++ b/tests/Tests/ORM/Functional/Ticket/SwitchContextWithFilter/CompanySQLFilter.php @@ -0,0 +1,26 @@ +getName() === User::class) { + return sprintf('%s.%s = %s', $targetTableAlias, $targetEntity->fieldMappings['company']['fieldName'], $this->getParameter('company')); + } + + if ($targetEntity->getName() === Order::class) { + return sprintf('%s.%s = %s', $targetTableAlias, $targetEntity->fieldMappings['company']['fieldName'], $this->getParameter('company')); + } + + return ''; + } +} diff --git a/tests/Tests/ORM/Functional/Ticket/SwitchContextWithFilter/Order.php b/tests/Tests/ORM/Functional/Ticket/SwitchContextWithFilter/Order.php new file mode 100644 index 00000000000..a6d86dca8a2 --- /dev/null +++ b/tests/Tests/ORM/Functional/Ticket/SwitchContextWithFilter/Order.php @@ -0,0 +1,43 @@ +user = $user; + $this->company = $user->company; + } +} diff --git a/tests/Tests/ORM/Functional/Ticket/SwitchContextWithFilter/User.php b/tests/Tests/ORM/Functional/Ticket/SwitchContextWithFilter/User.php new file mode 100644 index 00000000000..294bfdf87aa --- /dev/null +++ b/tests/Tests/ORM/Functional/Ticket/SwitchContextWithFilter/User.php @@ -0,0 +1,35 @@ +company = $company; + } +} From 9e2bfa816957920697d686678f0a0bf68fd30298 Mon Sep 17 00:00:00 2001 From: Indra Gunawan Date: Fri, 25 Oct 2024 20:57:39 +0800 Subject: [PATCH 09/21] Run tests against PostgreSQL 17 (#11697) * Run tests against PostgreSQL 17 * remove pgsql 15 --- .github/workflows/continuous-integration.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index e5e887a5838..2f9650b1b70 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -119,7 +119,7 @@ jobs: - "default" - "3@dev" postgres-version: - - "15" + - "17" extension: - pdo_pgsql - pgsql From 81c0d599c956071e974f9ba4134a8cab9d07565c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20Paris?= Date: Thu, 27 Jun 2024 23:22:59 +0200 Subject: [PATCH 10/21] Implement compatibility with Persistence 4 --- composer.json | 2 +- phpcs.xml.dist | 2 ++ phpstan-baseline.neon | 5 +++ phpstan-dbal3.neon | 5 +++ phpstan.neon | 5 +++ psalm-baseline.xml | 23 ++++++++---- src/EntityManager.php | 4 +-- src/Mapping/ClassMetadata.php | 12 ++----- .../Driver/LoadMappingFileImplementation.php | 35 +++++++++++++++++++ src/Mapping/Driver/XmlDriver.php | 8 ++--- .../GetReflectionClassImplementation.php | 33 +++++++++++++++++ .../Mock/NonProxyLoadingEntityManager.php | 8 +++++ tests/Tests/Mocks/MetadataDriverMock.php | 6 ++-- .../ORM/Functional/Ticket/DDC3103Test.php | 14 ++++++-- tests/Tests/ORM/Mapping/ClassMetadataTest.php | 5 +++ 15 files changed, 139 insertions(+), 28 deletions(-) create mode 100644 src/Mapping/Driver/LoadMappingFileImplementation.php create mode 100644 src/Mapping/GetReflectionClassImplementation.php diff --git a/composer.json b/composer.json index ec2ff1fd5a7..7269a3c08a1 100644 --- a/composer.json +++ b/composer.json @@ -31,7 +31,7 @@ "doctrine/inflector": "^1.4 || ^2.0", "doctrine/instantiator": "^1.3 || ^2", "doctrine/lexer": "^3", - "doctrine/persistence": "^3.3.1", + "doctrine/persistence": "^3.3.1 || ^4", "psr/cache": "^1 || ^2 || ^3", "symfony/console": "^5.4 || ^6.0 || ^7.0", "symfony/var-exporter": "^6.3.9 || ^7.0" diff --git a/phpcs.xml.dist b/phpcs.xml.dist index 4fde4cbf5e5..b14bbc6c1d8 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -48,6 +48,8 @@ + src/Mapping/Driver/LoadMappingFileImplementation.php + src/Mapping/GetReflectionClassImplementation.php tests/* diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 7c8ec81ae84..1b6362a6d8e 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -120,6 +120,11 @@ parameters: count: 1 path: src/EntityRepository.php + - + message: "#^If condition is always true\\.$#" + count: 1 + path: src/Mapping/ClassMetadata.php + - message: "#^If condition is always true\\.$#" count: 1 diff --git a/phpstan-dbal3.neon b/phpstan-dbal3.neon index 724fe2003f7..889d94c1df3 100644 --- a/phpstan-dbal3.neon +++ b/phpstan-dbal3.neon @@ -34,3 +34,8 @@ parameters: - message: '~deprecated class Doctrine\\DBAL\\Tools\\Console\\Command\\ReservedWordsCommand\:~' path: src/Tools/Console/ConsoleRunner.php + + # Compatibility with Persistence 3 + - + message: '#Expression on left side of \?\? is not nullable.#' + path: src/Mapping/Driver/AttributeDriver.php diff --git a/phpstan.neon b/phpstan.neon index d90ec9fe41f..f98eb8d00b8 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -45,3 +45,8 @@ parameters: message: '#Negated boolean expression is always false\.#' paths: - src/Mapping/Driver/AttributeDriver.php + + # Compatibility with Persistence 3 + - + message: '#Expression on left side of \?\? is not nullable.#' + path: src/Mapping/Driver/AttributeDriver.php diff --git a/psalm-baseline.xml b/psalm-baseline.xml index c7897f4d614..cef61546b74 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -298,15 +298,9 @@ - - - - - reflClass]]> - @@ -340,6 +334,7 @@ + reflClass]]> @@ -474,6 +469,14 @@ + + + + + + + + @@ -526,6 +529,14 @@ + + + + + + + + diff --git a/src/EntityManager.php b/src/EntityManager.php index eb5a123d0b6..5324d9cac7b 100644 --- a/src/EntityManager.php +++ b/src/EntityManager.php @@ -565,9 +565,9 @@ public function initializeObject(object $obj): void /** * {@inheritDoc} */ - public function isUninitializedObject($obj): bool + public function isUninitializedObject($value): bool { - return $this->unitOfWork->isUninitializedObject($obj); + return $this->unitOfWork->isUninitializedObject($value); } public function getFilters(): FilterCollection diff --git a/src/Mapping/ClassMetadata.php b/src/Mapping/ClassMetadata.php index 7c9020805a5..b91bd9b5ac1 100644 --- a/src/Mapping/ClassMetadata.php +++ b/src/Mapping/ClassMetadata.php @@ -73,6 +73,8 @@ */ class ClassMetadata implements PersistenceClassMetadata, Stringable { + use GetReflectionClassImplementation; + /* The inheritance mapping types */ /** * NONE means the class does not participate in an inheritance hierarchy @@ -932,16 +934,6 @@ public function validateLifecycleCallbacks(ReflectionService $reflService): void } } - /** - * {@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 { diff --git a/src/Mapping/Driver/LoadMappingFileImplementation.php b/src/Mapping/Driver/LoadMappingFileImplementation.php new file mode 100644 index 00000000000..df351889ba2 --- /dev/null +++ b/src/Mapping/Driver/LoadMappingFileImplementation.php @@ -0,0 +1,35 @@ +doLoadMappingFile($file); + } + } +} else { + /** @internal */ + trait LoadMappingFileImplementation + { + /** + * {@inheritDoc} + */ + protected function loadMappingFile($file) + { + return $this->doLoadMappingFile($file); + } + } +} diff --git a/src/Mapping/Driver/XmlDriver.php b/src/Mapping/Driver/XmlDriver.php index 30e85900e2e..e11b6b61d6f 100644 --- a/src/Mapping/Driver/XmlDriver.php +++ b/src/Mapping/Driver/XmlDriver.php @@ -43,6 +43,8 @@ */ class XmlDriver extends FileDriver { + use LoadMappingFileImplementation; + public const DEFAULT_FILE_EXTENSION = '.dcm.xml'; /** @@ -878,10 +880,8 @@ private function getCascadeMappings(SimpleXMLElement $cascadeElement): array return $cascades; } - /** - * {@inheritDoc} - */ - protected function loadMappingFile($file) + /** @return array */ + private function doLoadMappingFile(string $file): array { $this->validateMapping($file); $result = []; diff --git a/src/Mapping/GetReflectionClassImplementation.php b/src/Mapping/GetReflectionClassImplementation.php new file mode 100644 index 00000000000..780015c3680 --- /dev/null +++ b/src/Mapping/GetReflectionClassImplementation.php @@ -0,0 +1,33 @@ +reflClass; + } + } +} else { + trait GetReflectionClassImplementation + { + /** + * {@inheritDoc} + * + * Can return null when using static reflection, in violation of the LSP + */ + public function getReflectionClass(): ReflectionClass|null + { + return $this->reflClass; + } + } +} diff --git a/tests/Performance/Mock/NonProxyLoadingEntityManager.php b/tests/Performance/Mock/NonProxyLoadingEntityManager.php index 20f233e0089..bff330ab9be 100644 --- a/tests/Performance/Mock/NonProxyLoadingEntityManager.php +++ b/tests/Performance/Mock/NonProxyLoadingEntityManager.php @@ -212,4 +212,12 @@ public function contains(object $object): bool { return $this->realEntityManager->contains($object); } + + /** + * {@inheritDoc} + */ + public function isUninitializedObject($value): bool + { + return $this->realEntityManager->isUninitializedObject($value); + } } diff --git a/tests/Tests/Mocks/MetadataDriverMock.php b/tests/Tests/Mocks/MetadataDriverMock.php index b3be873c596..a2472cf8c46 100644 --- a/tests/Tests/Mocks/MetadataDriverMock.php +++ b/tests/Tests/Mocks/MetadataDriverMock.php @@ -15,14 +15,14 @@ class MetadataDriverMock implements MappingDriver /** * {@inheritDoc} */ - public function loadMetadataForClass($className, ClassMetadata $metadata) + public function loadMetadataForClass($className, ClassMetadata $metadata): void { } /** * {@inheritDoc} */ - public function isTransient($className) + public function isTransient($className): bool { return false; } @@ -30,7 +30,7 @@ public function isTransient($className) /** * {@inheritDoc} */ - public function getAllClassNames() + public function getAllClassNames(): array { return []; } diff --git a/tests/Tests/ORM/Functional/Ticket/DDC3103Test.php b/tests/Tests/ORM/Functional/Ticket/DDC3103Test.php index ec505c07741..5ead8d58698 100644 --- a/tests/Tests/ORM/Functional/Ticket/DDC3103Test.php +++ b/tests/Tests/ORM/Functional/Ticket/DDC3103Test.php @@ -7,10 +7,12 @@ use Doctrine\ORM\Mapping\ClassMetadata; use Doctrine\ORM\Mapping\Column; use Doctrine\ORM\Mapping\Embeddable; +use Doctrine\Persistence\Mapping\StaticReflectionService; use Doctrine\Tests\OrmFunctionalTestCase; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Group; +use function class_exists; use function serialize; use function unserialize; @@ -18,6 +20,15 @@ #[Group('DDC-3103')] class DDC3103Test extends OrmFunctionalTestCase { + protected function setUp(): void + { + if (! class_exists(StaticReflectionService::class)) { + self::markTestSkipped('This test is not supported by the current installed doctrine/persistence version'); + } + + parent::setUp(); + } + public function testIssue(): void { $classMetadata = new ClassMetadata(DDC3103ArticleId::class); @@ -39,7 +50,6 @@ public function testIssue(): void #[Embeddable] class DDC3103ArticleId { - /** @var string */ #[Column(name: 'name', type: 'string', length: 255)] - protected $nameValue; + protected string $nameValue; } diff --git a/tests/Tests/ORM/Mapping/ClassMetadataTest.php b/tests/Tests/ORM/Mapping/ClassMetadataTest.php index 38b2bde7ca9..60b1bc1822f 100644 --- a/tests/Tests/ORM/Mapping/ClassMetadataTest.php +++ b/tests/Tests/ORM/Mapping/ClassMetadataTest.php @@ -56,6 +56,7 @@ use stdClass; use function assert; +use function class_exists; use function count; use function serialize; use function str_contains; @@ -979,6 +980,10 @@ public function testCanInstantiateInternalPhpClassSubclassFromUnserializedMetada public function testWakeupReflectionWithEmbeddableAndStaticReflectionService(): void { + if (! class_exists(StaticReflectionService::class)) { + self::markTestSkipped('This test is not supported by the current installed doctrine/persistence version'); + } + $classMetadata = new ClassMetadata(TestEntity1::class); $classMetadata->mapEmbedded( From 4b03ec778995322e8b8c2dcd35bc52d0d2949ceb Mon Sep 17 00:00:00 2001 From: Mohamed Attia <92581654+Speelwolf@users.noreply.github.com> Date: Tue, 12 Nov 2024 00:02:38 +0100 Subject: [PATCH 11/21] Refine Explanations of Doctrine orm Package Structure (#11710) * Update architecture.rst * Update architecture.rst --- docs/en/reference/architecture.rst | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/docs/en/reference/architecture.rst b/docs/en/reference/architecture.rst index 7ba2ef774d7..5dabd259972 100644 --- a/docs/en/reference/architecture.rst +++ b/docs/en/reference/architecture.rst @@ -33,14 +33,13 @@ Doctrine ORM is divided into four main packages. - ORM (depends on DBAL+Persistence+Collections) This manual mainly covers the ORM package, sometimes touching parts -of the underlying DBAL and Persistence packages. The Doctrine code base -is split in to these packages for a few reasons and they are to... +of the underlying DBAL and Persistence packages. The Doctrine codebase +is split into these packages for a few reasons: -- ...make things more maintainable and decoupled -- ...allow you to use the code in Doctrine Persistence and Collections - without the ORM or DBAL -- ...allow you to use the DBAL without the ORM +- to make things more maintainable and decoupled +- to allow you to use the code in Doctrine Persistence and Collections without the ORM or DBAL +- to allow you to use the DBAL without the ORM Collection, Event Manager and Persistence ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ From ff3ccff36a69071ceb63559e4354dd8d87da8f98 Mon Sep 17 00:00:00 2001 From: Albert Casademont Date: Wed, 13 Nov 2024 23:56:41 +0100 Subject: [PATCH 12/21] Add `isEmpty()` method to the Extra Lazy Associations tutorial Extra lazy support for it was added a long time ago (see https://github.com/doctrine/orm/pull/912) but was never properly documented. --- docs/en/tutorials/extra-lazy-associations.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/tutorials/extra-lazy-associations.rst b/docs/en/tutorials/extra-lazy-associations.rst index fbff96f428b..fbf1f00abd6 100644 --- a/docs/en/tutorials/extra-lazy-associations.rst +++ b/docs/en/tutorials/extra-lazy-associations.rst @@ -18,6 +18,7 @@ can be called without triggering a full load of the collection: - ``Collection#containsKey($key)`` - ``Collection#count()`` - ``Collection#get($key)`` +- ``Collection#isEmpty()`` - ``Collection#slice($offset, $length = null)`` For each of the above methods the following semantics apply: From da51234d5aaba3901e91461982c07e54d97cce9c Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Thu, 12 May 2022 22:26:34 +0200 Subject: [PATCH 13/21] [GH-3519] Deprecate passing the same class with different discriminator values. --- UPGRADE.md | 9 +++++++-- src/Mapping/ClassMetadata.php | 20 +++++++++++++++++++ tests/Tests/ORM/Mapping/ClassMetadataTest.php | 8 ++++++++ 3 files changed, 35 insertions(+), 2 deletions(-) diff --git a/UPGRADE.md b/UPGRADE.md index 6da5e0be57c..12e67dee2cd 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -1,3 +1,8 @@ +# Upgrade to 3.4 + +Using the same class several times in a discriminator map is deprecated. +In 4.0, this will be an error. + # Upgrade to 3.3 ## Deprecate `DatabaseDriver` @@ -737,7 +742,7 @@ Use `toIterable()` instead. Output walkers should implement the new `\Doctrine\ORM\Query\OutputWalker` interface and create `Doctrine\ORM\Query\Exec\SqlFinalizer` instances instead of `Doctrine\ORM\Query\Exec\AbstractSqlExecutor`s. -The output walker must not base its workings on the query `firstResult`/`maxResult` values, so that the +The output walker must not base its workings on the query `firstResult`/`maxResult` values, so that the `SqlFinalizer` can be kept in the query cache and used regardless of the actual `firstResult`/`maxResult` values. Any operation dependent on `firstResult`/`maxResult` should take place within the `SqlFinalizer::createExecutor()` method. Details can be found at https://github.com/doctrine/orm/pull/11188. @@ -750,7 +755,7 @@ change in behavior. Progress on this is tracked at https://github.com/doctrine/orm/issues/11624 . -## PARTIAL DQL syntax is undeprecated +## PARTIAL DQL syntax is undeprecated Use of the PARTIAL keyword is not deprecated anymore in DQL, because we will be able to support PARTIAL objects with PHP 8.4 Lazy Objects and diff --git a/src/Mapping/ClassMetadata.php b/src/Mapping/ClassMetadata.php index b91bd9b5ac1..a72099bd4d8 100644 --- a/src/Mapping/ClassMetadata.php +++ b/src/Mapping/ClassMetadata.php @@ -25,7 +25,10 @@ use Stringable; use function array_column; +use function array_count_values; use function array_diff; +use function array_filter; +use function array_flip; use function array_intersect; use function array_key_exists; use function array_keys; @@ -39,6 +42,7 @@ use function defined; use function enum_exists; use function explode; +use function implode; use function in_array; use function interface_exists; use function is_string; @@ -2178,6 +2182,22 @@ final public function getDiscriminatorColumn(): DiscriminatorColumnMapping */ public function setDiscriminatorMap(array $map): void { + if (count(array_flip($map)) !== count($map)) { + Deprecation::trigger( + 'doctrine/orm', + 'https://github.com/doctrine/orm/issues/3519', + <<<'DEPRECATION' + Mapping a class to multiple discriminator values is deprecated, + and the discriminator mapping of %s contains duplicate values + for the following discriminator values: %s. + DEPRECATION, + $this->name, + implode(', ', array_keys(array_filter(array_count_values($map), static function (int $value): bool { + return $value > 1; + }))), + ); + } + foreach ($map as $value => $className) { $this->addDiscriminatorMapClass($value, $className); } diff --git a/tests/Tests/ORM/Mapping/ClassMetadataTest.php b/tests/Tests/ORM/Mapping/ClassMetadataTest.php index 60b1bc1822f..57736cc68a8 100644 --- a/tests/Tests/ORM/Mapping/ClassMetadataTest.php +++ b/tests/Tests/ORM/Mapping/ClassMetadataTest.php @@ -1101,6 +1101,14 @@ public function testClassNameMappingDiscriminatorValue(): void $xmlElement->children()->{'discriminator-map'}->{'discriminator-mapping'}[0]->attributes()['value'], ); } + + public function testDiscriminatorMapWithSameClassMultipleTimesDeprecated(): void + { + $this->expectDeprecationWithIdentifier('https://github.com/doctrine/orm/issues/3519'); + + $cm = new ClassMetadata(CMS\CmsUser::class); + $cm->setDiscriminatorMap(['foo' => CMS\CmsUser::class, 'bar' => CMS\CmsUser::class]); + } } #[MappedSuperclass] From 43ce0bef78618494058899d03f5a96b59d1f8587 Mon Sep 17 00:00:00 2001 From: Asmir Mustafic Date: Mon, 26 Sep 2022 12:53:37 +0200 Subject: [PATCH 14/21] lazy and eager collection refresh --- .../Ticket/LazyEagerCollectionTest.php | 160 ++++++++++++++++++ 1 file changed, 160 insertions(+) create mode 100644 tests/Doctrine/Tests/ORM/Functional/Ticket/LazyEagerCollectionTest.php diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/LazyEagerCollectionTest.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/LazyEagerCollectionTest.php new file mode 100644 index 00000000000..2797dfd67bb --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/LazyEagerCollectionTest.php @@ -0,0 +1,160 @@ +createSchemaForModels( + LazyEagerCollectionUser::class, + LazyEagerCollectionAddress::class, + LazyEagerCollectionPhone::class + ); + } + + public function testRefreshRefreshesBothLazyAndEagerCollections(): void + { + $user = new LazyEagerCollectionUser(); + $user->data = 'Guilherme'; + + $ph = new LazyEagerCollectionPhone(); + $ph->data = '12345'; + $user->addPhone($ph); + + $ad = new LazyEagerCollectionAddress(); + $ad->data = '6789'; + $user->addAddress($ad); + + $this->_em->persist($user); + $this->_em->persist($ad); + $this->_em->persist($ph); + $this->_em->flush(); + $this->_em->clear(); + + $user = $this->_em->find(LazyEagerCollectionUser::class, $user->id); + $ph = $user->phones[0]; + $ad = $user->addresses[0]; + + $ph->data = 'abc'; + $ad->data = 'def'; + + $this->_em->refresh($user); + + self::assertSame('12345', $ph->data); + self::assertSame('6789', $ad->data); + } +} + +/** + * @Entity + */ +class LazyEagerCollectionUser +{ + /** + * @var int + * @Id + * @Column(type="integer") + * @GeneratedValue(strategy="AUTO") + */ + public $id; + + /** + * @var string + * @Column(type="string", length=255) + */ + public $data; + + /** + * @ORM\OneToMany(targetEntity="LazyEagerCollectionPhone", cascade={"refresh"}, fetch="EAGER", mappedBy="user") + * + * @var LazyEagerCollectionPhone[] + */ + public $phones; + + /** + * @ORM\OneToMany(targetEntity="LazyEagerCollectionAddress", cascade={"refresh"}, mappedBy="user") + * + * @var LazyEagerCollectionAddress[] + */ + public $addresses; + + public function __construct() + { + $this->addresses = new ArrayCollection(); + $this->phones = new ArrayCollection(); + } + + public function addPhone(LazyEagerCollectionPhone $phone): void + { + $phone->user = $this; + $this->phones[] = $phone; + } + + public function addAddress(LazyEagerCollectionAddress $address): void + { + $address->user = $this; + $this->addresses[] = $address; + } +} + +/** @Entity */ +class LazyEagerCollectionPhone +{ + /** + * @var int + * @Id + * @Column(type="integer") + * @GeneratedValue(strategy="AUTO") + */ + public $id; + + /** + * @var string + * @Column(type="string", length=255) + */ + public $data; + + /** + * @ORM\ManyToOne(targetEntity="LazyEagerCollectionUser", inversedBy="phones") + * + * @var LazyEagerCollectionUser + */ + public $user; +} + +/** @Entity */ +class LazyEagerCollectionAddress +{ + /** + * @var int + * @Id + * @Column(type="integer") + * @GeneratedValue(strategy="AUTO") + */ + public $id; + + /** + * @var string + * @Column(type="string", length=255) + */ + public $data; + + /** + * @ORM\ManyToOne(targetEntity="LazyEagerCollectionUser", inversedBy="addresses") + * + * @var LazyEagerCollectionUser + */ + public $user; +} From 7d1b24f3b1fde6e12385c299ba6ea4b0da385031 Mon Sep 17 00:00:00 2001 From: Asmir Mustafic Date: Mon, 26 Sep 2022 13:16:38 +0200 Subject: [PATCH 15/21] attempt a fix --- src/UnitOfWork.php | 4 ++-- .../Tests/ORM/Functional/Ticket/LazyEagerCollectionTest.php | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/UnitOfWork.php b/src/UnitOfWork.php index 2969a3e0e0f..0f775910d83 100644 --- a/src/UnitOfWork.php +++ b/src/UnitOfWork.php @@ -2473,13 +2473,13 @@ private function doRefresh($entity, array &$visited, ?int $lockMode = null): voi throw ORMInvalidArgumentException::entityNotManaged($entity); } + $this->cascadeRefresh($entity, $visited, $lockMode); + $this->getEntityPersister($class->name)->refresh( array_combine($class->getIdentifierFieldNames(), $this->entityIdentifiers[$oid]), $entity, $lockMode ); - - $this->cascadeRefresh($entity, $visited, $lockMode); } /** diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/LazyEagerCollectionTest.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/LazyEagerCollectionTest.php index 2797dfd67bb..926e0523153 100644 --- a/tests/Doctrine/Tests/ORM/Functional/Ticket/LazyEagerCollectionTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/LazyEagerCollectionTest.php @@ -17,6 +17,7 @@ class LazyEagerCollectionTest extends OrmFunctionalTestCase protected function setUp(): void { parent::setUp(); + $this->createSchemaForModels( LazyEagerCollectionUser::class, LazyEagerCollectionAddress::class, From 4fbce94999d870eadb7ede560145c79c33c70080 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 18 Nov 2024 06:30:54 +0000 Subject: [PATCH 16/21] Bump codecov/codecov-action from 4 to 5 Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 4 to 5. - [Release notes](https://github.com/codecov/codecov-action/releases) - [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/codecov/codecov-action/compare/v4...v5) --- updated-dependencies: - dependency-name: codecov/codecov-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/continuous-integration.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 2f9650b1b70..44b252e2956 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -381,7 +381,7 @@ jobs: path: "reports" - name: "Upload to Codecov" - uses: "codecov/codecov-action@v4" + uses: "codecov/codecov-action@v5" with: directory: reports env: From bb5b2a33002d31b98761c103dd161c7431dabc1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20Paris?= Date: Mon, 18 Nov 2024 21:46:52 +0100 Subject: [PATCH 17/21] Ignore deprecation about StaticReflectionService It is from a class that is deprecated and removed in later branches. --- psalm.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/psalm.xml b/psalm.xml index 19b4e9368ca..b9e2421fdfd 100644 --- a/psalm.xml +++ b/psalm.xml @@ -50,6 +50,7 @@ + From 346c49832c08ca9ea6a95352fbcb7c921431eda2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20Pineau?= Date: Thu, 7 Nov 2024 12:07:37 +0100 Subject: [PATCH 18/21] Fix `Events::onFlush` and `PostFlush()` documentation: events are always raised see https://github.com/doctrine/orm/blob/9e2bfa816957920697d686678f0a0bf68fd30298/src/UnitOfWork.php#L399-L413 --- src/Events.php | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/Events.php b/src/Events.php index e8f123e488b..4695a7fb772 100644 --- a/src/Events.php +++ b/src/Events.php @@ -103,16 +103,14 @@ private function __construct() * The onFlush event occurs when the EntityManager#flush() operation is invoked, * after any changes to managed entities have been determined but before any * actual database operations are executed. The event is only raised if there is - * actually something to do for the underlying UnitOfWork. If nothing needs to be done, - * the onFlush event is not raised. + * actually something to do for the underlying UnitOfWork. */ public const onFlush = 'onFlush'; /** * The postFlush event occurs when the EntityManager#flush() operation is invoked and * after all actual database operations are executed successfully. The event is only raised if there is - * actually something to do for the underlying UnitOfWork. If nothing needs to be done, - * the postFlush event is not raised. The event won't be raised if an error occurs during the + * actually something to do for the underlying UnitOfWork. The event won't be raised if an error occurs during the * flush operation. */ public const postFlush = 'postFlush'; From 8422a414230eef8acfcc12edb8dafbd54e834950 Mon Sep 17 00:00:00 2001 From: Konrad Abicht Date: Mon, 22 Jul 2024 10:29:56 +0200 Subject: [PATCH 19/21] unitofwork.rst: php => PHP --- docs/en/reference/unitofwork.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/en/reference/unitofwork.rst b/docs/en/reference/unitofwork.rst index 6d429ec01e8..9c18fba0da7 100644 --- a/docs/en/reference/unitofwork.rst +++ b/docs/en/reference/unitofwork.rst @@ -102,7 +102,7 @@ How Doctrine Detects Changes ---------------------------- Doctrine is a data-mapper that tries to achieve persistence-ignorance (PI). -This means you map php objects into a relational database that don't +This means you map PHP objects into a relational database that don't necessarily know about the database at all. A natural question would now be, "how does Doctrine even detect objects have changed?". From 65e9f607e5d4888a05542d3ccf0f978bc43709a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20Paris?= Date: Sat, 23 Nov 2024 21:05:10 +0100 Subject: [PATCH 20/21] Use properties over array keys Using array access is deprecated. --- .../Ticket/SwitchContextWithFilter/CompanySQLFilter.php | 4 ++-- tests/Tests/ORM/Mapping/ClassMetadataTest.php | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/Tests/ORM/Functional/Ticket/SwitchContextWithFilter/CompanySQLFilter.php b/tests/Tests/ORM/Functional/Ticket/SwitchContextWithFilter/CompanySQLFilter.php index e65188334ac..d1e38528bf1 100644 --- a/tests/Tests/ORM/Functional/Ticket/SwitchContextWithFilter/CompanySQLFilter.php +++ b/tests/Tests/ORM/Functional/Ticket/SwitchContextWithFilter/CompanySQLFilter.php @@ -14,11 +14,11 @@ class CompanySQLFilter extends SQLFilter public function addFilterConstraint(ClassMetadata $targetEntity, $targetTableAlias): string { if ($targetEntity->getName() === User::class) { - return sprintf('%s.%s = %s', $targetTableAlias, $targetEntity->fieldMappings['company']['fieldName'], $this->getParameter('company')); + return sprintf('%s.%s = %s', $targetTableAlias, $targetEntity->fieldMappings['company']->fieldName, $this->getParameter('company')); } if ($targetEntity->getName() === Order::class) { - return sprintf('%s.%s = %s', $targetTableAlias, $targetEntity->fieldMappings['company']['fieldName'], $this->getParameter('company')); + return sprintf('%s.%s = %s', $targetTableAlias, $targetEntity->fieldMappings['company']->fieldName, $this->getParameter('company')); } return ''; diff --git a/tests/Tests/ORM/Mapping/ClassMetadataTest.php b/tests/Tests/ORM/Mapping/ClassMetadataTest.php index f187371dfd0..4c57891c2d8 100644 --- a/tests/Tests/ORM/Mapping/ClassMetadataTest.php +++ b/tests/Tests/ORM/Mapping/ClassMetadataTest.php @@ -886,7 +886,7 @@ public function testAssociationOverrideCanOverrideCascade(): void $cm->setAssociationOverride('parentDirectory', ['cascade' => ['all']]); $mapping = $cm->getAssociationMapping('parentDirectory'); - self::assertSame(['remove', 'persist', 'refresh', 'detach'], $mapping['cascade']); + self::assertSame(['remove', 'persist', 'refresh', 'detach'], $mapping->cascade); } #[TestGroup('DDC-1955')] From dd3604f523cc35283f0c4f4c36329b322845e57e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20Paris?= Date: Sat, 23 Nov 2024 21:06:56 +0100 Subject: [PATCH 21/21] Use properties over array keys Using array access is deprecated. --- src/PersistentCollection.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PersistentCollection.php b/src/PersistentCollection.php index ef5f28c3cb3..876a92a261a 100644 --- a/src/PersistentCollection.php +++ b/src/PersistentCollection.php @@ -509,7 +509,7 @@ public function __wakeup(): void */ public function first() { - if (! $this->initialized && ! $this->isDirty && $this->getMapping()['fetch'] === ClassMetadata::FETCH_EXTRA_LAZY) { + if (! $this->initialized && ! $this->isDirty && $this->getMapping()->fetch === ClassMetadata::FETCH_EXTRA_LAZY) { $persister = $this->getUnitOfWork()->getCollectionPersister($this->getMapping()); return array_values($persister->slice($this, 0, 1))[0] ?? false;