diff --git a/composer.json b/composer.json index 1a42c2c3ab8..639485f3cc8 100644 --- a/composer.json +++ b/composer.json @@ -33,7 +33,7 @@ "doctrine/persistence": "^3.3.1", "psr/cache": "^1 || ^2 || ^3", "symfony/console": "^5.4 || ^6.0 || ^7.0", - "symfony/var-exporter": "~6.2.13 || ^6.3.2 || ^7.0" + "symfony/var-exporter": "^6.3.9 || ^7.0" }, "require-dev": { "doctrine/coding-standard": "^12.0", diff --git a/docs/en/reference/basic-mapping.rst b/docs/en/reference/basic-mapping.rst index 49aacf93046..0c37867cfff 100644 --- a/docs/en/reference/basic-mapping.rst +++ b/docs/en/reference/basic-mapping.rst @@ -228,50 +228,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:: diff --git a/psalm-baseline.xml b/psalm-baseline.xml index 668d517ef55..4faaf8ce91d 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -760,13 +760,8 @@ 4]]> - + - - - diff --git a/src/Proxy/ProxyFactory.php b/src/Proxy/ProxyFactory.php index 8dd39ddd868..6184fa7811c 100644 --- a/src/Proxy/ProxyFactory.php +++ b/src/Proxy/ProxyFactory.php @@ -216,11 +216,11 @@ protected function skipClass(ClassMetadata $metadata): bool */ 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), @@ -238,7 +238,7 @@ private function createLazyInitializer(ClassMetadata $classMetadata, EntityPersi continue; } - $property->setValue($proxy, $property->getValue($entity)); + $property->setValue($proxy, $property->getValue($original)); } }; } @@ -283,9 +283,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/src/UnitOfWork.php b/src/UnitOfWork.php index f6a30a0028c..b07bf8aa518 100644 --- a/src/UnitOfWork.php +++ b/src/UnitOfWork.php @@ -2581,7 +2581,7 @@ public function createEntity(string $className, array $data, array &$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->isOneToMany() && ! $targetClass->isIdentifierComposite) { + if (! $isIteration && $assoc->isOneToMany() && ! $targetClass->isIdentifierComposite && ! $assoc->isIndexed()) { $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..0089d9a26aa --- /dev/null +++ b/tests/Tests/ORM/Functional/Ticket/GH11149/EagerProduct.php @@ -0,0 +1,33 @@ + */ + #[ORM\OneToMany( + targetEntity: EagerProductTranslation::class, + mappedBy: 'product', + fetch: 'EAGER', + indexBy: 'locale_code', + )] + public Collection $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..8027010ca86 --- /dev/null +++ b/tests/Tests/ORM/Functional/Ticket/GH11149/EagerProductTranslation.php @@ -0,0 +1,31 @@ +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..b521b5ae374 --- /dev/null +++ b/tests/Tests/ORM/Functional/Ticket/GH11149/Locale.php @@ -0,0 +1,21 @@ +code = $code; + } +} diff --git a/tests/Tests/ORM/Proxy/ProxyFactoryTest.php b/tests/Tests/ORM/Proxy/ProxyFactoryTest.php index 7359c493a5f..31cb9cc001f 100644 --- a/tests/Tests/ORM/Proxy/ProxyFactoryTest.php +++ b/tests/Tests/ORM/Proxy/ProxyFactoryTest.php @@ -62,9 +62,8 @@ protected function setUp(): void public function testReferenceProxyDelegatesLoadingToThePersister(): void { $identifier = ['id' => 42]; - $proxyClass = 'Proxies\__CG__\Doctrine\Tests\Models\ECommerce\ECommerceFeature'; $persister = $this->getMockBuilder(BasicEntityPersister::class) - ->onlyMethods(['load']) + ->onlyMethods(['loadById']) ->disableOriginalConstructor() ->getMock(); @@ -74,8 +73,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();