From 8d4718f875b3ea142203ae4258ad4e91f1bacecb Mon Sep 17 00:00:00 2001 From: Mark Schmale Date: Thu, 22 Feb 2024 10:58:50 +0100 Subject: [PATCH 01/17] provides a test case for github issue 11154 After 2.17 (some?) EAGER fetched OneToMany associations stopped working, if they have multiple join columns. Loads for these associations will trigger a `MessingPositionalParameter` exception "Positional parameter at index 1 does not have a bound value". This test case should reproduce this issue, so it can be fixed. --- .../RootEntity.php | 56 +++++++++++++++++ .../SecondLevel.php | 62 +++++++++++++++++++ ...agerFetchOneToManyWithCompositeKeyTest.php | 31 ++++++++++ tests/Tests/OrmFunctionalTestCase.php | 9 +++ 4 files changed, 158 insertions(+) create mode 100644 tests/Tests/Models/EagerFetchedCompositeOneToMany/RootEntity.php create mode 100644 tests/Tests/Models/EagerFetchedCompositeOneToMany/SecondLevel.php create mode 100644 tests/Tests/ORM/Functional/EagerFetchOneToManyWithCompositeKeyTest.php diff --git a/tests/Tests/Models/EagerFetchedCompositeOneToMany/RootEntity.php b/tests/Tests/Models/EagerFetchedCompositeOneToMany/RootEntity.php new file mode 100644 index 00000000000..af16c686970 --- /dev/null +++ b/tests/Tests/Models/EagerFetchedCompositeOneToMany/RootEntity.php @@ -0,0 +1,56 @@ + + */ + private $secondLevel; + + public function __construct(int $id, string $other) + { + $this->otherKey = $other; + $this->secondLevel = new ArrayCollection(); + $this->id = $id; + } + + public function getId(): ?int + { + return $this->id; + } + + public function getOtherKey(): string + { + return $this->otherKey; + } +} diff --git a/tests/Tests/Models/EagerFetchedCompositeOneToMany/SecondLevel.php b/tests/Tests/Models/EagerFetchedCompositeOneToMany/SecondLevel.php new file mode 100644 index 00000000000..e67f3abc4fb --- /dev/null +++ b/tests/Tests/Models/EagerFetchedCompositeOneToMany/SecondLevel.php @@ -0,0 +1,62 @@ +id = $id; + $this->upperId = $upper->getId(); + $this->otherKey = $upper->getOtherKey(); + $this->root = $upper; + } + + public function getId(): ?int + { + return $this->id; + } +} diff --git a/tests/Tests/ORM/Functional/EagerFetchOneToManyWithCompositeKeyTest.php b/tests/Tests/ORM/Functional/EagerFetchOneToManyWithCompositeKeyTest.php new file mode 100644 index 00000000000..8ad78cb253d --- /dev/null +++ b/tests/Tests/ORM/Functional/EagerFetchOneToManyWithCompositeKeyTest.php @@ -0,0 +1,31 @@ +useModelSet('eager_fetched_composite_one_to_many'); + + parent::setUp(); + } + + /** @ticket 11154 */ + public function testItDoesNotThrowAnExceptionWhenTriggeringALoad(): void + { + $a1 = new RootEntity(1, 'A'); + + $this->_em->persist($a1); + $this->_em->flush(); + + $this->_em->clear(); + + self::assertCount(1, $this->_em->getRepository(RootEntity::class)->findAll()); + } +} diff --git a/tests/Tests/OrmFunctionalTestCase.php b/tests/Tests/OrmFunctionalTestCase.php index 4a64136f184..ea2a0110405 100644 --- a/tests/Tests/OrmFunctionalTestCase.php +++ b/tests/Tests/OrmFunctionalTestCase.php @@ -342,6 +342,10 @@ abstract class OrmFunctionalTestCase extends OrmTestCase Models\Issue9300\Issue9300Child::class, Models\Issue9300\Issue9300Parent::class, ], + 'eager_fetched_composite_one_to_many' => [ + Models\EagerFetchedCompositeOneToMany\RootEntity::class, + Models\EagerFetchedCompositeOneToMany\SecondLevel::class, + ], ]; /** @param class-string ...$models */ @@ -671,6 +675,11 @@ protected function tearDown(): void $conn->executeStatement('DELETE FROM issue5989_managers'); } + if (isset($this->_usedModelSets['eager_fetched_composite_one_to_many'])) { + $conn->executeStatement('DELETE FROM eager_composite_join_second_level'); + $conn->executeStatement('DELETE FROM eager_composite_join_root'); + } + $this->_em->clear(); } From fcd02b1ee276635d4b39d55f492f7c8b0c1ebed4 Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Sat, 16 Mar 2024 23:04:57 +0100 Subject: [PATCH 02/17] Cleanup tests not to use model sets. --- .../EagerFetchOneToManyWithCompositeKeyTest.php | 10 +++------- tests/Tests/OrmFunctionalTestCase.php | 9 --------- 2 files changed, 3 insertions(+), 16 deletions(-) diff --git a/tests/Tests/ORM/Functional/EagerFetchOneToManyWithCompositeKeyTest.php b/tests/Tests/ORM/Functional/EagerFetchOneToManyWithCompositeKeyTest.php index 8ad78cb253d..82b9d0b8acb 100644 --- a/tests/Tests/ORM/Functional/EagerFetchOneToManyWithCompositeKeyTest.php +++ b/tests/Tests/ORM/Functional/EagerFetchOneToManyWithCompositeKeyTest.php @@ -5,20 +5,16 @@ namespace Doctrine\Tests\ORM\Functional; use Doctrine\Tests\Models\EagerFetchedCompositeOneToMany\RootEntity; +use Doctrine\Tests\Models\EagerFetchedCompositeOneToMany\SecondLevel; use Doctrine\Tests\OrmFunctionalTestCase; final class EagerFetchOneToManyWithCompositeKeyTest extends OrmFunctionalTestCase { - protected function setUp(): void - { - $this->useModelSet('eager_fetched_composite_one_to_many'); - - parent::setUp(); - } - /** @ticket 11154 */ public function testItDoesNotThrowAnExceptionWhenTriggeringALoad(): void { + $this->setUpEntitySchema([RootEntity::class, SecondLevel::class]); + $a1 = new RootEntity(1, 'A'); $this->_em->persist($a1); diff --git a/tests/Tests/OrmFunctionalTestCase.php b/tests/Tests/OrmFunctionalTestCase.php index ea2a0110405..4a64136f184 100644 --- a/tests/Tests/OrmFunctionalTestCase.php +++ b/tests/Tests/OrmFunctionalTestCase.php @@ -342,10 +342,6 @@ abstract class OrmFunctionalTestCase extends OrmTestCase Models\Issue9300\Issue9300Child::class, Models\Issue9300\Issue9300Parent::class, ], - 'eager_fetched_composite_one_to_many' => [ - Models\EagerFetchedCompositeOneToMany\RootEntity::class, - Models\EagerFetchedCompositeOneToMany\SecondLevel::class, - ], ]; /** @param class-string ...$models */ @@ -675,11 +671,6 @@ protected function tearDown(): void $conn->executeStatement('DELETE FROM issue5989_managers'); } - if (isset($this->_usedModelSets['eager_fetched_composite_one_to_many'])) { - $conn->executeStatement('DELETE FROM eager_composite_join_second_level'); - $conn->executeStatement('DELETE FROM eager_composite_join_root'); - } - $this->_em->clear(); } From 820a0da4c188fb96de6782fef53ed5639ff690b9 Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Sat, 16 Mar 2024 23:05:28 +0100 Subject: [PATCH 03/17] Do not schedule batch loading for target classes with composite identifier. --- src/UnitOfWork.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/UnitOfWork.php b/src/UnitOfWork.php index 00698e56c60..c5996613b48 100644 --- a/src/UnitOfWork.php +++ b/src/UnitOfWork.php @@ -3169,9 +3169,9 @@ public function createEntity($className, array $data, &$hints = []) if ($hints['fetchMode'][$class->name][$field] === ClassMetadata::FETCH_EAGER) { $isIteration = isset($hints[Query::HINT_INTERNAL_ITERATION]) && $hints[Query::HINT_INTERNAL_ITERATION]; - if (! $isIteration && $assoc['type'] === ClassMetadata::ONE_TO_MANY) { + if ($assoc['type'] === ClassMetadata::ONE_TO_MANY && ! $isIteration && ! $targetClass->isIdentifierComposite) { $this->scheduleCollectionForBatchLoading($pColl, $class); - } elseif (($isIteration && $assoc['type'] === ClassMetadata::ONE_TO_MANY) || $assoc['type'] === ClassMetadata::MANY_TO_MANY) { + } else { $this->loadCollection($pColl); $pColl->takeSnapshot(); } From 528ef40fc411ac3375ab8c9b5fb8f850fb9bbcd2 Mon Sep 17 00:00:00 2001 From: Marko Kaznovac Date: Sun, 17 Mar 2024 15:55:54 +0100 Subject: [PATCH 04/17] Minor code style fix in AbstractRemoteControl --- tests/Tests/Models/AbstractFetchEager/AbstractRemoteControl.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/Tests/Models/AbstractFetchEager/AbstractRemoteControl.php b/tests/Tests/Models/AbstractFetchEager/AbstractRemoteControl.php index 59d69da28a4..5790cbe365d 100644 --- a/tests/Tests/Models/AbstractFetchEager/AbstractRemoteControl.php +++ b/tests/Tests/Models/AbstractFetchEager/AbstractRemoteControl.php @@ -27,8 +27,6 @@ abstract class AbstractRemoteControl public $id; /** - * /** - * * @ORM\Column(type="string") * * @var string From 1622b7877dcd8db015ff3d99fd662e0bc506ff37 Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Sun, 17 Mar 2024 18:02:11 +0100 Subject: [PATCH 05/17] Fix entities and mapping. --- .../SecondLevel.php | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/tests/Tests/Models/EagerFetchedCompositeOneToMany/SecondLevel.php b/tests/Tests/Models/EagerFetchedCompositeOneToMany/SecondLevel.php index e67f3abc4fb..121a57ecc4c 100644 --- a/tests/Tests/Models/EagerFetchedCompositeOneToMany/SecondLevel.php +++ b/tests/Tests/Models/EagerFetchedCompositeOneToMany/SecondLevel.php @@ -12,14 +12,6 @@ */ class SecondLevel { - /** - * @ORM\Id - * @ORM\Column(type="integer", nullable=false) - * - * @var int|null - */ - private $id = null; - /** * @ORM\Id * @ORM\Column(type="integer", nullable=false) @@ -39,17 +31,16 @@ class SecondLevel /** * @ORM\ManyToOne(targetEntity=RootEntity::class, inversedBy="secondLevel") * @ORM\JoinColumns({ - * @ORM\JoinColumn(name="other_key", referencedColumnName="other_key"), - * @ORM\JoinColumn(name="upper_id", referencedColumnName="id") + * @ORM\JoinColumn(name="root_other_key", referencedColumnName="other_key"), + * @ORM\JoinColumn(name="root_id", referencedColumnName="id") * }) * * @var RootEntity */ private $root; - public function __construct(int $id, RootEntity $upper) + public function __construct(RootEntity $upper) { - $this->id = $id; $this->upperId = $upper->getId(); $this->otherKey = $upper->getOtherKey(); $this->root = $upper; From 5e6d5c06a987c073f01128577c78102dd468a608 Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Sun, 17 Mar 2024 19:43:26 +0100 Subject: [PATCH 06/17] Key on fk --- .../Models/EagerFetchedCompositeOneToMany/SecondLevel.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/Tests/Models/EagerFetchedCompositeOneToMany/SecondLevel.php b/tests/Tests/Models/EagerFetchedCompositeOneToMany/SecondLevel.php index 121a57ecc4c..99fed077723 100644 --- a/tests/Tests/Models/EagerFetchedCompositeOneToMany/SecondLevel.php +++ b/tests/Tests/Models/EagerFetchedCompositeOneToMany/SecondLevel.php @@ -8,7 +8,9 @@ /** * @ORM\Entity - * @ORM\Table(name="eager_composite_join_second_level") + * @ORM\Table(name="eager_composite_join_second_level", indexes={ + * @ORM\Index(name="root_other_key_idx", columns={"root_other_key", "root_id"}) + * }) */ class SecondLevel { From 3e3c023c95d6c7c848a1bcd71180baf7e033743a Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Sun, 17 Mar 2024 19:50:56 +0100 Subject: [PATCH 07/17] Switch join columns around, otherwise index doesnt match --- .../Models/EagerFetchedCompositeOneToMany/SecondLevel.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Tests/Models/EagerFetchedCompositeOneToMany/SecondLevel.php b/tests/Tests/Models/EagerFetchedCompositeOneToMany/SecondLevel.php index 99fed077723..33f878c8f41 100644 --- a/tests/Tests/Models/EagerFetchedCompositeOneToMany/SecondLevel.php +++ b/tests/Tests/Models/EagerFetchedCompositeOneToMany/SecondLevel.php @@ -33,8 +33,8 @@ class SecondLevel /** * @ORM\ManyToOne(targetEntity=RootEntity::class, inversedBy="secondLevel") * @ORM\JoinColumns({ - * @ORM\JoinColumn(name="root_other_key", referencedColumnName="other_key"), - * @ORM\JoinColumn(name="root_id", referencedColumnName="id") + * @ORM\JoinColumn(name="root_id", referencedColumnName="id"), + * @ORM\JoinColumn(name="root_other_key", referencedColumnName="other_key") * }) * * @var RootEntity From 08a9e60ed04e7cf08023ee69b7d8c68a3bfeec66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20Paris?= Date: Sun, 17 Mar 2024 23:06:30 +0100 Subject: [PATCH 08/17] Remove outdated git metadata files (#11362) Some of it seems related to the previous documentation build system, some of it seems related to IntelliJ. --- docs/.gitignore | 4 ---- docs/.gitmodules | 3 --- 2 files changed, 7 deletions(-) delete mode 100644 docs/.gitignore delete mode 100644 docs/.gitmodules diff --git a/docs/.gitignore b/docs/.gitignore deleted file mode 100644 index 59cd005a9a4..00000000000 --- a/docs/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -en/_exts/configurationblock.pyc -build -en/_build -.idea diff --git a/docs/.gitmodules b/docs/.gitmodules deleted file mode 100644 index e38d44b0adf..00000000000 --- a/docs/.gitmodules +++ /dev/null @@ -1,3 +0,0 @@ -[submodule "en/_theme"] - path = en/_theme - url = https://github.com/doctrine/doctrine-sphinx-theme.git From 9d1a4973ae2d8ddedd6b66e958e32ebb58cdf3d6 Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Tue, 19 Mar 2024 09:19:25 +0100 Subject: [PATCH 09/17] Improve lazy ghost performance by avoiding self-referencing closure. (#11376) * Improve lazy ghost performance by avoiding self-referencing closure. Co-authored-by: Nicolas Grekas * update baselien --------- Co-authored-by: Nicolas Grekas --- psalm-baseline.xml | 8 ++------ src/Proxy/ProxyFactory.php | 14 ++++++-------- tests/Tests/ORM/Proxy/ProxyFactoryTest.php | 6 +++--- 3 files changed, 11 insertions(+), 17 deletions(-) diff --git a/psalm-baseline.xml b/psalm-baseline.xml index c175642e62b..7a3b729e849 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -1492,6 +1492,7 @@ + @@ -1501,13 +1502,8 @@ - + - - - diff --git a/src/Proxy/ProxyFactory.php b/src/Proxy/ProxyFactory.php index 0660c423f0d..5b2d2eca0c9 100644 --- a/src/Proxy/ProxyFactory.php +++ b/src/Proxy/ProxyFactory.php @@ -360,11 +360,11 @@ private function createInitializer(ClassMetadata $classMetadata, EntityPersister */ private function createLazyInitializer(ClassMetadata $classMetadata, EntityPersister $entityPersister, IdentifierFlattener $identifierFlattener): Closure { - return static function (InternalProxy $proxy, InternalProxy $original) use ($entityPersister, $classMetadata, $identifierFlattener): void { - $identifier = $classMetadata->getIdentifierValues($original); - $entity = $entityPersister->loadById($identifier, $original); + return static function (InternalProxy $proxy) use ($entityPersister, $classMetadata, $identifierFlattener): void { + $identifier = $classMetadata->getIdentifierValues($proxy); + $original = $entityPersister->loadById($identifier); - if ($entity === null) { + if ($original === null) { throw EntityNotFoundException::fromClassNameAndIdentifier( $classMetadata->getName(), $identifierFlattener->flattenIdentifier($classMetadata, $identifier) @@ -382,7 +382,7 @@ private function createLazyInitializer(ClassMetadata $classMetadata, EntityPersi continue; } - $property->setValue($proxy, $property->getValue($entity)); + $property->setValue($proxy, $property->getValue($original)); } }; } @@ -468,9 +468,7 @@ 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(static function (InternalProxy $object) use ($initializer, &$proxy): void { - $initializer($object, $proxy); - }, $skippedProperties); + $proxy = self::createLazyGhost($initializer, $skippedProperties); foreach ($identifierFields as $idField => $reflector) { if (! isset($identifier[$idField])) { diff --git a/tests/Tests/ORM/Proxy/ProxyFactoryTest.php b/tests/Tests/ORM/Proxy/ProxyFactoryTest.php index 3739aaf4dd5..d0989953def 100644 --- a/tests/Tests/ORM/Proxy/ProxyFactoryTest.php +++ b/tests/Tests/ORM/Proxy/ProxyFactoryTest.php @@ -65,7 +65,7 @@ public function testReferenceProxyDelegatesLoadingToThePersister(): void { $identifier = ['id' => 42]; $proxyClass = 'Proxies\__CG__\Doctrine\Tests\Models\ECommerce\ECommerceFeature'; - $persister = $this->getMockBuilderWithOnlyMethods(BasicEntityPersister::class, ['load']) + $persister = $this->getMockBuilderWithOnlyMethods(BasicEntityPersister::class, ['loadById']) ->disableOriginalConstructor() ->getMock(); @@ -75,8 +75,8 @@ public function testReferenceProxyDelegatesLoadingToThePersister(): void $persister ->expects(self::atLeastOnce()) - ->method('load') - ->with(self::equalTo($identifier), self::isInstanceOf($proxyClass)) + ->method('loadById') + ->with(self::equalTo($identifier)) ->will(self::returnValue($proxy)); $proxy->getDescription(); From d15624f72f920cfc79ea86b7d3c0a7586ad71574 Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Wed, 20 Mar 2024 15:45:47 +0100 Subject: [PATCH 10/17] [GH-11185] Bugfix: do not use collection batch loading for indexBy assocations. (#11380) --- src/UnitOfWork.php | 2 +- .../Ticket/GH11149/EagerProduct.php | 41 ++++++++++++++++ .../GH11149/EagerProductTranslation.php | 45 ++++++++++++++++++ .../Functional/Ticket/GH11149/GH11149Test.php | 47 +++++++++++++++++++ .../ORM/Functional/Ticket/GH11149/Locale.php | 27 +++++++++++ 5 files changed, 161 insertions(+), 1 deletion(-) create mode 100644 tests/Tests/ORM/Functional/Ticket/GH11149/EagerProduct.php create mode 100644 tests/Tests/ORM/Functional/Ticket/GH11149/EagerProductTranslation.php create mode 100644 tests/Tests/ORM/Functional/Ticket/GH11149/GH11149Test.php create mode 100644 tests/Tests/ORM/Functional/Ticket/GH11149/Locale.php diff --git a/src/UnitOfWork.php b/src/UnitOfWork.php index 3bb9b8efeb6..25b88221583 100644 --- a/src/UnitOfWork.php +++ b/src/UnitOfWork.php @@ -3166,7 +3166,7 @@ public function createEntity($className, array $data, &$hints = []) if ($hints['fetchMode'][$class->name][$field] === ClassMetadata::FETCH_EAGER) { $isIteration = isset($hints[Query::HINT_INTERNAL_ITERATION]) && $hints[Query::HINT_INTERNAL_ITERATION]; - if ($assoc['type'] === ClassMetadata::ONE_TO_MANY && ! $isIteration && ! $targetClass->isIdentifierComposite) { + if ($assoc['type'] === ClassMetadata::ONE_TO_MANY && ! $isIteration && ! $targetClass->isIdentifierComposite && ! isset($assoc['indexBy'])) { $this->scheduleCollectionForBatchLoading($pColl, $class); } else { $this->loadCollection($pColl); diff --git a/tests/Tests/ORM/Functional/Ticket/GH11149/EagerProduct.php b/tests/Tests/ORM/Functional/Ticket/GH11149/EagerProduct.php new file mode 100644 index 00000000000..448f7d82eb6 --- /dev/null +++ b/tests/Tests/ORM/Functional/Ticket/GH11149/EagerProduct.php @@ -0,0 +1,41 @@ + + */ + public $translations; + + public function __construct(int $id) + { + $this->id = $id; + $this->translations = new ArrayCollection(); + } +} diff --git a/tests/Tests/ORM/Functional/Ticket/GH11149/EagerProductTranslation.php b/tests/Tests/ORM/Functional/Ticket/GH11149/EagerProductTranslation.php new file mode 100644 index 00000000000..32c501868ec --- /dev/null +++ b/tests/Tests/ORM/Functional/Ticket/GH11149/EagerProductTranslation.php @@ -0,0 +1,45 @@ +id = $id; + $this->product = $product; + $this->locale = $locale; + } +} diff --git a/tests/Tests/ORM/Functional/Ticket/GH11149/GH11149Test.php b/tests/Tests/ORM/Functional/Ticket/GH11149/GH11149Test.php new file mode 100644 index 00000000000..28bab541b90 --- /dev/null +++ b/tests/Tests/ORM/Functional/Ticket/GH11149/GH11149Test.php @@ -0,0 +1,47 @@ +setUpEntitySchema([ + Locale::class, + EagerProduct::class, + EagerProductTranslation::class, + ]); + } + + public function testFetchEagerModeWithIndexBy(): void + { + // Load entities into database + $this->_em->persist($product = new EagerProduct(11149)); + $this->_em->persist($locale = new Locale('fr_FR')); + $this->_em->persist(new EagerProductTranslation(11149, $product, $locale)); + $this->_em->flush(); + $this->_em->clear(); + + // Fetch entity from database + $product = $this->_em->find(EagerProduct::class, 11149); + + // Assert associated entity is loaded eagerly + static::assertInstanceOf(EagerProduct::class, $product); + static::assertInstanceOf(PersistentCollection::class, $product->translations); + static::assertTrue($product->translations->isInitialized()); + static::assertCount(1, $product->translations); + + // Assert associated entity is indexed by given property + $translation = $product->translations->get('fr_FR'); + static::assertInstanceOf(EagerProductTranslation::class, $translation); + static::assertNotInstanceOf(Proxy::class, $translation); + } +} diff --git a/tests/Tests/ORM/Functional/Ticket/GH11149/Locale.php b/tests/Tests/ORM/Functional/Ticket/GH11149/Locale.php new file mode 100644 index 00000000000..d54eda636ff --- /dev/null +++ b/tests/Tests/ORM/Functional/Ticket/GH11149/Locale.php @@ -0,0 +1,27 @@ +code = $code; + } +} From 5ccbc201bff51cdc2006db8585b0d22d6695859d Mon Sep 17 00:00:00 2001 From: Thomas Landauer Date: Wed, 20 Mar 2024 23:34:10 +0100 Subject: [PATCH 11/17] [Documentation] Removing "Doctrine Mapping Types" ... (#11384) ... in favor of https://www.doctrine-project.org/projects/doctrine-dbal/en/3.8/reference/types.html#reference Page: https://www.doctrine-project.org/projects/doctrine-orm/en/2.19/reference/basic-mapping.html#doctrine-mapping-types As announced in https://github.com/doctrine/dbal/pull/6336#issuecomment-2003720361 , the goal is to remove this duplicated type information from ORM and replace it with a link to DBAL. In https://github.com/doctrine/dbal/pull/6341 , I'm adding any detail which I'm deleting here to the DBAL. --- docs/en/reference/basic-mapping.rst | 48 +++-------------------------- 1 file changed, 5 insertions(+), 43 deletions(-) diff --git a/docs/en/reference/basic-mapping.rst b/docs/en/reference/basic-mapping.rst index a61e5d8d0e4..32f8626806b 100644 --- a/docs/en/reference/basic-mapping.rst +++ b/docs/en/reference/basic-mapping.rst @@ -300,50 +300,12 @@ and a custom ``Doctrine\ORM\Mapping\TypedFieldMapper`` implementation. Doctrine Mapping Types ---------------------- -The ``type`` option used in the ``@Column`` accepts any of the existing -Doctrine types or even your own custom types. A Doctrine type defines +The ``type`` option used in the ``@Column`` accepts any of the +`existing Doctrine DBAL types `_ +or :doc:`your own custom mapping types +<../cookbook/custom-mapping-types>`. A Doctrine type defines the conversion between PHP and SQL types, independent from the database vendor -you are using. All Mapping Types that ship with Doctrine are fully portable -between the supported database systems. - -As an example, the Doctrine Mapping Type ``string`` defines the -mapping from a PHP string to a SQL VARCHAR (or VARCHAR2 etc. -depending on the RDBMS brand). Here is a quick overview of the -built-in mapping types: - -- ``string``: Type that maps a SQL VARCHAR to a PHP string. -- ``integer``: Type that maps a SQL INT to a PHP integer. -- ``smallint``: Type that maps a database SMALLINT to a PHP - integer. -- ``bigint``: Type that maps a database BIGINT to a PHP string. -- ``boolean``: Type that maps a SQL boolean or equivalent (TINYINT) to a PHP boolean. -- ``decimal``: Type that maps a SQL DECIMAL to a PHP string. -- ``date``: Type that maps a SQL DATETIME to a PHP DateTime - object. -- ``time``: Type that maps a SQL TIME to a PHP DateTime object. -- ``datetime``: Type that maps a SQL DATETIME/TIMESTAMP to a PHP - DateTime object. -- ``datetimetz``: Type that maps a SQL DATETIME/TIMESTAMP to a PHP - DateTime object with timezone. -- ``text``: Type that maps a SQL CLOB to a PHP string. -- ``object``: Type that maps a SQL CLOB to a PHP object using - ``serialize()`` and ``unserialize()`` -- ``array``: Type that maps a SQL CLOB to a PHP array using - ``serialize()`` and ``unserialize()`` -- ``simple_array``: Type that maps a SQL CLOB to a PHP array using - ``implode()`` and ``explode()``, with a comma as delimiter. *IMPORTANT* - Only use this type if you are sure that your values cannot contain a ",". -- ``json_array``: Type that maps a SQL CLOB to a PHP array using - ``json_encode()`` and ``json_decode()`` -- ``float``: Type that maps a SQL Float (Double Precision) to a - PHP double. *IMPORTANT*: Works only with locale settings that use - decimal points as separator. -- ``guid``: Type that maps a database GUID/UUID to a PHP string. Defaults to - varchar but uses a specific type if the platform supports it. -- ``blob``: Type that maps a SQL BLOB to a PHP resource stream - -A cookbook article shows how to define :doc:`your own custom mapping types -<../cookbook/custom-mapping-types>`. +you are using. .. note:: From e384978e0bd1bdced06755ee190adf7f713dd006 Mon Sep 17 00:00:00 2001 From: Claudio Zizza <859964+SenseException@users.noreply.github.com> Date: Wed, 20 Mar 2024 23:35:25 +0100 Subject: [PATCH 12/17] Remove older versions from the docs (#11383) To reduce Algolia operations and indexes older versions get removed --- .doctrine-project.json | 36 ------------------------------------ 1 file changed, 36 deletions(-) diff --git a/.doctrine-project.json b/.doctrine-project.json index e761549298c..0eeb48f5899 100644 --- a/.doctrine-project.json +++ b/.doctrine-project.json @@ -94,42 +94,6 @@ "branchName": "2.10.x", "slug": "2.10", "maintained": false - }, - { - "name": "2.9", - "branchName": "2.9.x", - "slug": "2.9", - "maintained": false - }, - { - "name": "2.8", - "branchName": "2.8.x", - "slug": "2.8", - "maintained": false - }, - { - "name": "2.7", - "branchName": "2.7", - "slug": "2.7", - "maintained": false - }, - { - "name": "2.6", - "branchName": "2.6", - "slug": "2.6", - "maintained": false - }, - { - "name": "2.5", - "branchName": "2.5", - "slug": "2.5", - "maintained": false - }, - { - "name": "2.4", - "branchName": "2.4", - "slug": "2.4", - "maintained": false } ] } From 67ac5a82dab0b76764fedb469202006f3c80b60b Mon Sep 17 00:00:00 2001 From: Valentin Karnauhov Date: Wed, 20 Mar 2024 17:59:34 +0200 Subject: [PATCH 13/17] Fixed proxy initialization for EnumReflectionProperty --- src/Proxy/ProxyFactory.php | 2 +- .../Ticket/GH11386/GH11386EntityCart.php | 55 +++++++++++++ .../Ticket/GH11386/GH11386EntityCustomer.php | 80 +++++++++++++++++++ .../Ticket/GH11386/GH11386EnumType.php | 11 +++ .../Functional/Ticket/GH11386/GH11386Test.php | 39 +++++++++ 5 files changed, 186 insertions(+), 1 deletion(-) create mode 100644 tests/Tests/ORM/Functional/Ticket/GH11386/GH11386EntityCart.php create mode 100644 tests/Tests/ORM/Functional/Ticket/GH11386/GH11386EntityCustomer.php create mode 100644 tests/Tests/ORM/Functional/Ticket/GH11386/GH11386EnumType.php create mode 100644 tests/Tests/ORM/Functional/Ticket/GH11386/GH11386Test.php diff --git a/src/Proxy/ProxyFactory.php b/src/Proxy/ProxyFactory.php index f784cefa651..8dd39ddd868 100644 --- a/src/Proxy/ProxyFactory.php +++ b/src/Proxy/ProxyFactory.php @@ -234,7 +234,7 @@ private function createLazyInitializer(ClassMetadata $classMetadata, EntityPersi $class = $entityPersister->getClassMetadata(); foreach ($class->getReflectionProperties() as $property) { - if (! $property || ! $class->hasField($property->name) && ! $class->hasAssociation($property->name)) { + if (! $property || ! $class->hasField($property->getName()) && ! $class->hasAssociation($property->getName())) { continue; } diff --git a/tests/Tests/ORM/Functional/Ticket/GH11386/GH11386EntityCart.php b/tests/Tests/ORM/Functional/Ticket/GH11386/GH11386EntityCart.php new file mode 100644 index 00000000000..032171aac52 --- /dev/null +++ b/tests/Tests/ORM/Functional/Ticket/GH11386/GH11386EntityCart.php @@ -0,0 +1,55 @@ +id; + } + + public function getAmount(): int|null + { + return $this->amount; + } + + public function setAmount(int $amount): static + { + $this->amount = $amount; + + return $this; + } + + public function getCustomer(): GH11386EntityCustomer|null + { + return $this->customer; + } + + public function setCustomer(GH11386EntityCustomer|null $customer): self + { + $this->customer = $customer; + + return $this; + } +} diff --git a/tests/Tests/ORM/Functional/Ticket/GH11386/GH11386EntityCustomer.php b/tests/Tests/ORM/Functional/Ticket/GH11386/GH11386EntityCustomer.php new file mode 100644 index 00000000000..3290a6f99bf --- /dev/null +++ b/tests/Tests/ORM/Functional/Ticket/GH11386/GH11386EntityCustomer.php @@ -0,0 +1,80 @@ + true])] + private GH11386EnumType|null $type = null; + + #[OneToOne(mappedBy: 'customer')] + private GH11386EntityCart|null $cart = null; + + public function getId(): int|null + { + return $this->id; + } + + public function getName(): string|null + { + return $this->name; + } + + public function setName(string $name): static + { + $this->name = $name; + + return $this; + } + + public function getType(): GH11386EnumType|null + { + return $this->type; + } + + public function setType(GH11386EnumType $type): static + { + $this->type = $type; + + return $this; + } + + public function getCart(): GH11386EntityCart|null + { + return $this->cart; + } + + public function setCart(GH11386EntityCart|null $cart): self + { + // unset the owning side of the relation if necessary + if ($cart === null && $this->cart !== null) { + $this->cart->setCustomer(null); + } + + // set the owning side of the relation if necessary + if ($cart !== null && $cart->getCustomer() !== $this) { + $cart->setCustomer($this); + } + + $this->cart = $cart; + + return $this; + } +} diff --git a/tests/Tests/ORM/Functional/Ticket/GH11386/GH11386EnumType.php b/tests/Tests/ORM/Functional/Ticket/GH11386/GH11386EnumType.php new file mode 100644 index 00000000000..c07da865b95 --- /dev/null +++ b/tests/Tests/ORM/Functional/Ticket/GH11386/GH11386EnumType.php @@ -0,0 +1,11 @@ +createSchemaForModels( + GH11386EntityCart::class, + GH11386EntityCustomer::class, + ); + } + + public function testInitializeClonedProxy(): void + { + $cart = new GH11386EntityCart(); + $cart->setAmount(1000); + + $customer = new GH11386EntityCustomer(); + $customer->setName('John Doe') + ->setType(GH11386EnumType::MALE) + ->setCart($cart); + + $this->_em->persist($cart); + $this->_em->flush(); + $this->_em->clear(); + + $cart = $this->_em->find(GH11386EntityCart::class, 1); + $customer = clone $cart->getCustomer(); + self::assertEquals('John Doe', $customer->getName()); + } +} From db6e702088a251f7f0adc2fcdaa2003f46a5a4f6 Mon Sep 17 00:00:00 2001 From: "Alexander M. Turek" Date: Thu, 21 Mar 2024 10:32:55 +0100 Subject: [PATCH 14/17] Remove unused variable (#11391) --- tests/Tests/ORM/Proxy/ProxyFactoryTest.php | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/Tests/ORM/Proxy/ProxyFactoryTest.php b/tests/Tests/ORM/Proxy/ProxyFactoryTest.php index d0989953def..e6335c46c8f 100644 --- a/tests/Tests/ORM/Proxy/ProxyFactoryTest.php +++ b/tests/Tests/ORM/Proxy/ProxyFactoryTest.php @@ -64,7 +64,6 @@ protected function setUp(): void public function testReferenceProxyDelegatesLoadingToThePersister(): void { $identifier = ['id' => 42]; - $proxyClass = 'Proxies\__CG__\Doctrine\Tests\Models\ECommerce\ECommerceFeature'; $persister = $this->getMockBuilderWithOnlyMethods(BasicEntityPersister::class, ['loadById']) ->disableOriginalConstructor() ->getMock(); From 95795c87a8924fa28b1465dec67ad8a5b85d8213 Mon Sep 17 00:00:00 2001 From: "Alexander M. Turek" Date: Thu, 21 Mar 2024 10:38:59 +0100 Subject: [PATCH 15/17] Add missing import --- tests/Tests/ORM/Functional/Ticket/GH11149/EagerProduct.php | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/Tests/ORM/Functional/Ticket/GH11149/EagerProduct.php b/tests/Tests/ORM/Functional/Ticket/GH11149/EagerProduct.php index 448f7d82eb6..e2528089abf 100644 --- a/tests/Tests/ORM/Functional/Ticket/GH11149/EagerProduct.php +++ b/tests/Tests/ORM/Functional/Ticket/GH11149/EagerProduct.php @@ -5,6 +5,7 @@ namespace Doctrine\Tests\ORM\Functional\Ticket\GH11149; use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; /** From 1a5a4c674a416b4fdf76833c627c5e7f58bbb890 Mon Sep 17 00:00:00 2001 From: "Alexander M. Turek" Date: Thu, 21 Mar 2024 12:01:42 +0100 Subject: [PATCH 16/17] Set column length explicitly (#11393) --- tests/Tests/ORM/Functional/Ticket/GH11149/Locale.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Tests/ORM/Functional/Ticket/GH11149/Locale.php b/tests/Tests/ORM/Functional/Ticket/GH11149/Locale.php index d54eda636ff..8ec8a548bf1 100644 --- a/tests/Tests/ORM/Functional/Ticket/GH11149/Locale.php +++ b/tests/Tests/ORM/Functional/Ticket/GH11149/Locale.php @@ -14,7 +14,7 @@ class Locale { /** * @ORM\Id - * @ORM\Column(type="string") + * @ORM\Column(type="string", length=5) * * @var string */ From 9c560713925ac5859342e6ff370c4c997acf2fd4 Mon Sep 17 00:00:00 2001 From: "Alexander M. Turek" Date: Thu, 21 Mar 2024 12:37:52 +0100 Subject: [PATCH 17/17] Adjust PHPBench mocks --- .../Performance/Mock/NonLoadingPersister.php | 25 ++++++------------- .../Mock/NonProxyLoadingEntityManager.php | 2 +- .../Mock/NonProxyLoadingUnitOfWork.php | 13 ++++++---- 3 files changed, 17 insertions(+), 23 deletions(-) diff --git a/tests/Performance/Mock/NonLoadingPersister.php b/tests/Performance/Mock/NonLoadingPersister.php index 7058092bf68..bf487978c9f 100644 --- a/tests/Performance/Mock/NonLoadingPersister.php +++ b/tests/Performance/Mock/NonLoadingPersister.php @@ -4,8 +4,7 @@ namespace Doctrine\Performance\Mock; -use Doctrine\DBAL\LockMode; -use Doctrine\ORM\Mapping\AssociationMapping; +use Doctrine\ORM\Mapping\ClassMetadata; use Doctrine\ORM\Persisters\Entity\BasicEntityPersister; /** @@ -13,22 +12,14 @@ */ class NonLoadingPersister extends BasicEntityPersister { - public function __construct() - { + public function __construct( + ClassMetadata $class, + ) { + $this->class = $class; } - /** - * {@inheritDoc} - */ - public function load( - array $criteria, - object|null $entity = null, - AssociationMapping|null $assoc = null, - array $hints = [], - LockMode|int|null $lockMode = null, - int|null $limit = null, - array|null $orderBy = null, - ): object|null { - return $entity; + public function loadById(array $identifier, object|null $entity = null): object|null + { + return $entity ?? new ($this->class->name)(); } } diff --git a/tests/Performance/Mock/NonProxyLoadingEntityManager.php b/tests/Performance/Mock/NonProxyLoadingEntityManager.php index e60e476c408..20f233e0089 100644 --- a/tests/Performance/Mock/NonProxyLoadingEntityManager.php +++ b/tests/Performance/Mock/NonProxyLoadingEntityManager.php @@ -57,7 +57,7 @@ public function getClassMetadata(string $className): ClassMetadata public function getUnitOfWork(): UnitOfWork { - return new NonProxyLoadingUnitOfWork(); + return new NonProxyLoadingUnitOfWork($this); } public function getCache(): Cache|null diff --git a/tests/Performance/Mock/NonProxyLoadingUnitOfWork.php b/tests/Performance/Mock/NonProxyLoadingUnitOfWork.php index d9ca05c6879..68884a3f232 100644 --- a/tests/Performance/Mock/NonProxyLoadingUnitOfWork.php +++ b/tests/Performance/Mock/NonProxyLoadingUnitOfWork.php @@ -4,6 +4,7 @@ namespace Doctrine\Performance\Mock; +use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\UnitOfWork; /** @@ -11,15 +12,17 @@ */ class NonProxyLoadingUnitOfWork extends UnitOfWork { - private NonLoadingPersister $entityPersister; + /** @var array */ + private array $entityPersisters = []; - public function __construct() - { - $this->entityPersister = new NonLoadingPersister(); + public function __construct( + private EntityManagerInterface $entityManager, + ) { } public function getEntityPersister(string $entityName): NonLoadingPersister { - return $this->entityPersister; + return $this->entityPersisters[$entityName] + ??= new NonLoadingPersister($this->entityManager->getClassMetadata($entityName)); } }