diff --git a/.doctrine-project.json b/.doctrine-project.json index 0eeb48f5899..f3a38fb4bdd 100644 --- a/.doctrine-project.json +++ b/.doctrine-project.json @@ -11,17 +11,23 @@ "slug": "latest", "upcoming": true }, + { + "name": "3.3", + "branchName": "3.3.x", + "slug": "3.3", + "upcoming": true + }, { "name": "3.2", "branchName": "3.2.x", "slug": "3.2", - "upcoming": true + "current": true }, { "name": "3.1", "branchName": "3.1.x", "slug": "3.1", - "current": true + "maintained": false }, { "name": "3.0", diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index f9b2de81e24..59723f577fc 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -182,7 +182,7 @@ jobs: - "default" - "4@dev" mariadb-version: - - "10.9" + - "11.4" extension: - "mysqli" - "pdo_mysql" @@ -191,11 +191,11 @@ jobs: mariadb: image: "mariadb:${{ matrix.mariadb-version }}" env: - MYSQL_ALLOW_EMPTY_PASSWORD: yes - MYSQL_DATABASE: "doctrine_tests" + MARIADB_ALLOW_EMPTY_ROOT_PASSWORD: yes + MARIADB_DATABASE: "doctrine_tests" options: >- - --health-cmd "mysqladmin ping --silent" + --health-cmd "healthcheck.sh --connect --innodb_initialized" ports: - "3306:3306" diff --git a/README.md b/README.md index 70dceea1faa..1df322cf7e8 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ -| [4.0.x][4.0] | [3.2.x][3.2] | [3.1.x][3.1] | [2.20.x][2.20] | [2.19.x][2.19] | +| [4.0.x][4.0] | [3.3.x][3.3] | [3.2.x][3.2] | [2.20.x][2.20] | [2.19.x][2.19] | |:------------------------------------------------------:|:------------------------------------------------------:|:------------------------------------------------------:|:--------------------------------------------------------:|:--------------------------------------------------------:| -| [![Build status][4.0 image]][4.0] | [![Build status][3.2 image]][3.2] | [![Build status][3.1 image]][3.1] | [![Build status][2.20 image]][2.20] | [![Build status][2.19 image]][2.19] | -| [![Coverage Status][4.0 coverage image]][4.0 coverage] | [![Coverage Status][3.2 coverage image]][3.2 coverage] | [![Coverage Status][3.1 coverage image]][3.1 coverage] | [![Coverage Status][2.20 coverage image]][2.20 coverage] | [![Coverage Status][2.19 coverage image]][2.19 coverage] | +| [![Build status][4.0 image]][4.0] | [![Build status][3.3 image]][3.3] | [![Build status][3.2 image]][3.2] | [![Build status][2.20 image]][2.20] | [![Build status][2.19 image]][2.19] | +| [![Coverage Status][4.0 coverage image]][4.0 coverage] | [![Coverage Status][3.3 coverage image]][3.3 coverage] | [![Coverage Status][3.2 coverage image]][3.2 coverage] | [![Coverage Status][2.20 coverage image]][2.20 coverage] | [![Coverage Status][2.19 coverage image]][2.19 coverage] | [
4]]>
+
diff --git a/src/Internal/Hydration/ObjectHydrator.php b/src/Internal/Hydration/ObjectHydrator.php
index d24323d8689..d0fc101f215 100644
--- a/src/Internal/Hydration/ObjectHydrator.php
+++ b/src/Internal/Hydration/ObjectHydrator.php
@@ -356,11 +356,15 @@ protected function hydrateRowData(array $row, array &$result): void
$parentObject = $this->resultPointers[$parentAlias];
} else {
// Parent object of relation not found, mark as not-fetched again
- $element = $this->getEntity($data, $dqlAlias);
+ if (isset($nonemptyComponents[$dqlAlias])) {
+ $element = $this->getEntity($data, $dqlAlias);
- // Update result pointer and provide initial fetch data for parent
- $this->resultPointers[$dqlAlias] = $element;
- $rowData['data'][$parentAlias][$relationField] = $element;
+ // Update result pointer and provide initial fetch data for parent
+ $this->resultPointers[$dqlAlias] = $element;
+ $rowData['data'][$parentAlias][$relationField] = $element;
+ } else {
+ $element = null;
+ }
// Mark as not-fetched again
unset($this->hints['fetched'][$parentAlias][$relationField]);
diff --git a/src/Persisters/Collection/OneToManyPersister.php b/src/Persisters/Collection/OneToManyPersister.php
index c62ea565ed5..7354daa5627 100644
--- a/src/Persisters/Collection/OneToManyPersister.php
+++ b/src/Persisters/Collection/OneToManyPersister.php
@@ -8,6 +8,8 @@
use Doctrine\Common\Collections\Criteria;
use Doctrine\DBAL\Exception as DBALException;
use Doctrine\DBAL\Types\Type;
+use Doctrine\ORM\EntityNotFoundException;
+use Doctrine\ORM\Mapping\MappingException;
use Doctrine\ORM\Mapping\OneToManyAssociationMapping;
use Doctrine\ORM\PersistentCollection;
use Doctrine\ORM\Utility\PersisterHelper;
@@ -146,7 +148,11 @@ public function loadCriteria(PersistentCollection $collection, Criteria $criteri
throw new BadMethodCallException('Filtering a collection by Criteria is not supported by this CollectionPersister.');
}
- /** @throws DBALException */
+ /**
+ * @throws DBALException
+ * @throws EntityNotFoundException
+ * @throws MappingException
+ */
private function deleteEntityCollection(PersistentCollection $collection): int
{
$mapping = $this->getMapping($collection);
@@ -166,6 +172,13 @@ private function deleteEntityCollection(PersistentCollection $collection): int
$statement = 'DELETE FROM ' . $this->quoteStrategy->getTableName($targetClass, $this->platform)
. ' WHERE ' . implode(' = ? AND ', $columns) . ' = ?';
+ if ($targetClass->isInheritanceTypeSingleTable()) {
+ $discriminatorColumn = $targetClass->getDiscriminatorColumn();
+ $statement .= ' AND ' . $discriminatorColumn['name'] . ' = ?';
+ $parameters[] = $targetClass->discriminatorValue;
+ $types[] = $discriminatorColumn['type'];
+ }
+
$numAffected = $this->conn->executeStatement($statement, $parameters, $types);
assert(is_int($numAffected));
diff --git a/src/Proxy/ProxyFactory.php b/src/Proxy/ProxyFactory.php
index 6184fa7811c..b5426a014e4 100644
--- a/src/Proxy/ProxyFactory.php
+++ b/src/Proxy/ProxyFactory.php
@@ -210,15 +210,14 @@ protected function skipClass(ClassMetadata $metadata): bool
/**
* Creates a closure capable of initializing a proxy
*
- * @return Closure(InternalProxy, InternalProxy):void
+ * @return Closure(InternalProxy, array):void
*
* @throws EntityNotFoundException
*/
private function createLazyInitializer(ClassMetadata $classMetadata, EntityPersister $entityPersister, IdentifierFlattener $identifierFlattener): Closure
{
- return static function (InternalProxy $proxy) use ($entityPersister, $classMetadata, $identifierFlattener): void {
- $identifier = $classMetadata->getIdentifierValues($proxy);
- $original = $entityPersister->loadById($identifier);
+ return static function (InternalProxy $proxy, array $identifier) use ($entityPersister, $classMetadata, $identifierFlattener): void {
+ $original = $entityPersister->loadById($identifier);
if ($original === null) {
throw EntityNotFoundException::fromClassNameAndIdentifier(
@@ -234,7 +233,7 @@ private function createLazyInitializer(ClassMetadata $classMetadata, EntityPersi
$class = $entityPersister->getClassMetadata();
foreach ($class->getReflectionProperties() as $property) {
- if (! $property || ! $class->hasField($property->getName()) && ! $class->hasAssociation($property->getName())) {
+ if (! $property || isset($identifier[$property->getName()]) || ! $class->hasField($property->getName()) && ! $class->hasAssociation($property->getName())) {
continue;
}
@@ -283,7 +282,9 @@ private function getProxyFactory(string $className): Closure
$identifierFields = array_intersect_key($class->getReflectionProperties(), $identifiers);
$proxyFactory = Closure::bind(static function (array $identifier) use ($initializer, $skippedProperties, $identifierFields, $className): InternalProxy {
- $proxy = self::createLazyGhost($initializer, $skippedProperties);
+ $proxy = self::createLazyGhost(static function (InternalProxy $object) use ($initializer, $identifier): void {
+ $initializer($object, $identifier);
+ }, $skippedProperties);
foreach ($identifierFields as $idField => $reflector) {
if (! isset($identifier[$idField])) {
diff --git a/src/Query/Parser.php b/src/Query/Parser.php
index ade4bf347fe..e948f2c6b03 100644
--- a/src/Query/Parser.php
+++ b/src/Query/Parser.php
@@ -2563,7 +2563,10 @@ public function ArithmeticPrimary(): AST\Node|string
return new AST\ParenthesisExpression($expr);
}
- assert($this->lexer->lookahead !== null);
+ if ($this->lexer->lookahead === null) {
+ $this->syntaxError('ArithmeticPrimary');
+ }
+
switch ($this->lexer->lookahead->type) {
case TokenType::T_COALESCE:
case TokenType::T_NULLIF:
diff --git a/src/Query/SqlWalker.php b/src/Query/SqlWalker.php
index 004d29e773c..c6f98c12d50 100644
--- a/src/Query/SqlWalker.php
+++ b/src/Query/SqlWalker.php
@@ -911,7 +911,9 @@ public function walkJoinAssociationDeclaration(
}
}
- if ($relation->fetch === ClassMetadata::FETCH_EAGER && $condExpr !== null) {
+ $fetchMode = $this->query->getHint('fetchMode')[$assoc->sourceEntity][$assoc->fieldName] ?? $relation->fetch;
+
+ if ($fetchMode === ClassMetadata::FETCH_EAGER && $condExpr !== null) {
throw QueryException::eagerFetchJoinWithNotAllowed($assoc->sourceEntity, $assoc->fieldName);
}
diff --git a/tests/Tests/Models/ECommerce/ECommerceProduct2.php b/tests/Tests/Models/ECommerce/ECommerceProduct2.php
new file mode 100644
index 00000000000..1cbe939ef5b
--- /dev/null
+++ b/tests/Tests/Models/ECommerce/ECommerceProduct2.php
@@ -0,0 +1,46 @@
+id;
+ }
+
+ public function getName(): string|null
+ {
+ return $this->name;
+ }
+
+ public function __clone()
+ {
+ $this->id = null;
+ $this->name = 'Clone of ' . $this->name;
+ }
+}
diff --git a/tests/Tests/ORM/Functional/EagerFetchCollectionTest.php b/tests/Tests/ORM/Functional/EagerFetchCollectionTest.php
index 88098c9c6da..b8d451097af 100644
--- a/tests/Tests/ORM/Functional/EagerFetchCollectionTest.php
+++ b/tests/Tests/ORM/Functional/EagerFetchCollectionTest.php
@@ -89,6 +89,14 @@ public function testSubselectFetchJoinWithNotAllowed(): void
$query->getResult();
}
+ public function testSubselectFetchJoinWithAllowedWhenOverriddenNotEager(): void
+ {
+ $query = $this->_em->createQuery('SELECT o, c FROM ' . EagerFetchOwner::class . ' o JOIN o.children c WITH c.id = 1');
+ $query->setFetchMode(EagerFetchChild::class, 'owner', ORM\ClassMetadata::FETCH_LAZY);
+
+ $this->assertIsString($query->getSql());
+ }
+
public function testEagerFetchWithIterable(): void
{
$this->createOwnerWithChildren(2);
diff --git a/tests/Tests/ORM/Functional/ProxiesLikeEntitiesTest.php b/tests/Tests/ORM/Functional/ProxiesLikeEntitiesTest.php
index b2b3306ea1b..0cc8776ba50 100644
--- a/tests/Tests/ORM/Functional/ProxiesLikeEntitiesTest.php
+++ b/tests/Tests/ORM/Functional/ProxiesLikeEntitiesTest.php
@@ -58,7 +58,7 @@ protected function setUp(): void
public function testPersistUpdate(): void
{
// Considering case (a)
- $proxy = $this->_em->getProxyFactory()->getProxy(CmsUser::class, ['id' => 123]);
+ $proxy = $this->_em->getProxyFactory()->getProxy(CmsUser::class, ['id' => $this->user->getId()]);
$proxy->id = null;
$proxy->username = 'ocra';
diff --git a/tests/Tests/ORM/Functional/ReferenceProxyTest.php b/tests/Tests/ORM/Functional/ReferenceProxyTest.php
index 1a805d467c0..55f65956757 100644
--- a/tests/Tests/ORM/Functional/ReferenceProxyTest.php
+++ b/tests/Tests/ORM/Functional/ReferenceProxyTest.php
@@ -9,6 +9,7 @@
use Doctrine\ORM\Proxy\InternalProxy;
use Doctrine\Tests\Models\Company\CompanyAuction;
use Doctrine\Tests\Models\ECommerce\ECommerceProduct;
+use Doctrine\Tests\Models\ECommerce\ECommerceProduct2;
use Doctrine\Tests\Models\ECommerce\ECommerceShipping;
use Doctrine\Tests\OrmFunctionalTestCase;
use PHPUnit\Framework\Attributes\Group;
@@ -112,6 +113,24 @@ public function testCloneProxy(): void
self::assertFalse($entity->isCloned);
}
+ public function testCloneProxyWithResetId(): void
+ {
+ $id = $this->createProduct();
+
+ $entity = $this->_em->getReference(ECommerceProduct2::class, $id);
+ assert($entity instanceof ECommerceProduct2);
+
+ $clone = clone $entity;
+ assert($clone instanceof ECommerceProduct2);
+
+ self::assertEquals($id, $entity->getId());
+ self::assertEquals('Doctrine Cookbook', $entity->getName());
+
+ self::assertFalse($this->_em->contains($clone));
+ self::assertNull($clone->getId());
+ self::assertEquals('Clone of Doctrine Cookbook', $clone->getName());
+ }
+
#[Group('DDC-733')]
public function testInitializeProxy(): void
{
diff --git a/tests/Tests/ORM/Functional/Ticket/GH10889Test.php b/tests/Tests/ORM/Functional/Ticket/GH10889Test.php
new file mode 100644
index 00000000000..fe7d6e8c53c
--- /dev/null
+++ b/tests/Tests/ORM/Functional/Ticket/GH10889Test.php
@@ -0,0 +1,79 @@
+createSchemaForModels(
+ GH10889Person::class,
+ GH10889Company::class,
+ GH10889Resume::class,
+ );
+ }
+
+ public function testIssue(): void
+ {
+ $person = new GH10889Person();
+ $resume = new GH10889Resume($person, null);
+
+ $this->_em->persist($person);
+ $this->_em->persist($resume);
+ $this->_em->flush();
+ $this->_em->clear();
+
+ /** @var list