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 } ] } 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/.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 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 f784cefa651..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), @@ -234,11 +234,11 @@ 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; } - $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 018c1e788fb..b07bf8aa518 100644 --- a/src/UnitOfWork.php +++ b/src/UnitOfWork.php @@ -2581,9 +2581,9 @@ 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()) { + if (! $isIteration && $assoc->isOneToMany() && ! $targetClass->isIdentifierComposite && ! $assoc->isIndexed()) { $this->scheduleCollectionForBatchLoading($pColl, $class); - } elseif (($isIteration && $assoc->isOneToMany()) || $assoc->isManyToMany()) { + } else { $this->loadCollection($pColl); $pColl->takeSnapshot(); } 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)); } } diff --git a/tests/Tests/Models/EagerFetchedCompositeOneToMany/RootEntity.php b/tests/Tests/Models/EagerFetchedCompositeOneToMany/RootEntity.php new file mode 100644 index 00000000000..d369be822d9 --- /dev/null +++ b/tests/Tests/Models/EagerFetchedCompositeOneToMany/RootEntity.php @@ -0,0 +1,43 @@ + */ + #[ORM\OneToMany(mappedBy: 'root', targetEntity: SecondLevel::class, fetch: 'EAGER')] + private Collection $secondLevel; + + public function __construct(int $id, string $other) + { + $this->otherKey = $other; + $this->secondLevel = new ArrayCollection(); + $this->id = $id; + } + + public function getId(): int|null + { + 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..0608dcffba4 --- /dev/null +++ b/tests/Tests/Models/EagerFetchedCompositeOneToMany/SecondLevel.php @@ -0,0 +1,38 @@ +upperId = $upper->getId(); + $this->otherKey = $upper->getOtherKey(); + $this->root = $upper; + } + + public function getId(): int|null + { + 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..82b9d0b8acb --- /dev/null +++ b/tests/Tests/ORM/Functional/EagerFetchOneToManyWithCompositeKeyTest.php @@ -0,0 +1,27 @@ +setUpEntitySchema([RootEntity::class, SecondLevel::class]); + + $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/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..75a4f583c83 --- /dev/null +++ b/tests/Tests/ORM/Functional/Ticket/GH11149/Locale.php @@ -0,0 +1,21 @@ +code = $code; + } +} 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()); + } +} 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();