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] | [

πŸ‡ΊπŸ‡¦ UKRAINE NEEDS YOUR HELP NOW!

](https://www.doctrine-project.org/stop-war.html) @@ -22,14 +22,14 @@ without requiring unnecessary code duplication. [4.0]: https://github.com/doctrine/orm/tree/4.0.x [4.0 coverage image]: https://codecov.io/gh/doctrine/orm/branch/4.0.x/graph/badge.svg [4.0 coverage]: https://codecov.io/gh/doctrine/orm/branch/4.0.x + [3.3 image]: https://github.com/doctrine/orm/actions/workflows/continuous-integration.yml/badge.svg?branch=3.3.x + [3.3]: https://github.com/doctrine/orm/tree/3.3.x + [3.3 coverage image]: https://codecov.io/gh/doctrine/orm/branch/3.3.x/graph/badge.svg + [3.3 coverage]: https://codecov.io/gh/doctrine/orm/branch/3.3.x [3.2 image]: https://github.com/doctrine/orm/actions/workflows/continuous-integration.yml/badge.svg?branch=3.2.x [3.2]: https://github.com/doctrine/orm/tree/3.2.x [3.2 coverage image]: https://codecov.io/gh/doctrine/orm/branch/3.2.x/graph/badge.svg [3.2 coverage]: https://codecov.io/gh/doctrine/orm/branch/3.2.x - [3.1 image]: https://github.com/doctrine/orm/actions/workflows/continuous-integration.yml/badge.svg?branch=3.1.x - [3.1]: https://github.com/doctrine/orm/tree/3.1.x - [3.1 coverage image]: https://codecov.io/gh/doctrine/orm/branch/3.1.x/graph/badge.svg - [3.1 coverage]: https://codecov.io/gh/doctrine/orm/branch/3.1.x [2.20 image]: https://github.com/doctrine/orm/actions/workflows/continuous-integration.yml/badge.svg?branch=2.20.x [2.20]: https://github.com/doctrine/orm/tree/2.20.x [2.20 coverage image]: https://codecov.io/gh/doctrine/orm/branch/2.20.x/graph/badge.svg diff --git a/UPGRADE.md b/UPGRADE.md index bcbee2d9a91..d0c647e13a7 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -44,6 +44,12 @@ The properties `$indexes` and `$uniqueConstraints` have been removed since they The preferred way of defining indices and unique constraints is by using the `\Doctrine\ORM\Mapping\UniqueConstraint` and `\Doctrine\ORM\Mapping\Index` attributes. +# Upgrade to 3.3 + +## Deprecate `DatabaseDriver` + +The class `Doctrine\ORM\Mapping\Driver\DatabaseDriver` is deprecated without replacement. + # Upgrade to 3.2 ## Deprecate the `NotSupported` exception diff --git a/docs/en/reference/transactions-and-concurrency.rst b/docs/en/reference/transactions-and-concurrency.rst index 8bbd661ba42..485b73e7251 100644 --- a/docs/en/reference/transactions-and-concurrency.rst +++ b/docs/en/reference/transactions-and-concurrency.rst @@ -88,7 +88,7 @@ requirement. A more convenient alternative for explicit transaction demarcation is the use of provided control abstractions in the form of -``Connection#transactional($func)`` and ``EntityManager#transactional($func)``. +``Connection#transactional($func)`` and ``EntityManager#wrapInTransaction($func)``. When used, these control abstractions ensure that you never forget to rollback the transaction, in addition to the obvious code reduction. An example that is functionally equivalent to the previously shown code looks as follows: @@ -96,21 +96,23 @@ functionally equivalent to the previously shown code looks as follows: .. code-block:: php transactional(function($conn) { + // ... do some work + $user = new User; + $user->setName('George'); + }); + + // transactional with EntityManager instance // $em instanceof EntityManager - $em->transactional(function($em) { + $em->wrapInTransaction(function($em) { // ... do some work $user = new User; $user->setName('George'); $em->persist($user); }); -.. warning:: - - For historical reasons, ``EntityManager#transactional($func)`` will return - ``true`` whenever the return value of ``$func`` is loosely false. - Some examples of this include ``array()``, ``"0"``, ``""``, ``0``, and - ``null``. - The difference between ``Connection#transactional($func)`` and ``EntityManager#transactional($func)`` is that the latter abstraction flushes the ``EntityManager`` prior to transaction diff --git a/docs/en/tutorials/composite-primary-keys.rst b/docs/en/tutorials/composite-primary-keys.rst index 12e078e0097..980feecbd76 100644 --- a/docs/en/tutorials/composite-primary-keys.rst +++ b/docs/en/tutorials/composite-primary-keys.rst @@ -145,7 +145,7 @@ We keep up the example of an Article with arbitrary attributes, the mapping look #[OneToMany(targetEntity: ArticleAttribute::class, mappedBy: 'article', cascade: ['ALL'], indexBy: 'attribute')] private Collection $attributes; - public function addAttribute(string $name, ArticleAttribute $value): void + public function addAttribute(string $name, string $value): void { $this->attributes[$name] = new ArticleAttribute($name, $value, $this); } diff --git a/phpcs.xml.dist b/phpcs.xml.dist index 7ead84a96fa..4fde4cbf5e5 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -14,7 +14,6 @@ src tests - */src/Mapping/InverseJoinColumn.php */tests/Tests/Proxies/__CG__* */tests/Tests/ORM/Tools/Export/export/* diff --git a/psalm-baseline.xml b/psalm-baseline.xml index 4985b71e60c..12e6b74dd1f 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -488,20 +488,14 @@ table]]> - - {'discriminator-column'}]]> - {'discriminator-map'}]]> - - $usage, 'region' => $region, ]]]> - - - {'discriminator-column'}]]> - {'discriminator-map'}]]> - - - getName() === 'embeddable']]> - getName() === 'entity']]> - getName() === 'mapped-superclass']]> - @@ -738,7 +723,9 @@ 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/Mapping/Driver/DatabaseDriver.php b/src/Mapping/Driver/DatabaseDriver.php index 4c09e2b01c9..19504d832fb 100644 --- a/src/Mapping/Driver/DatabaseDriver.php +++ b/src/Mapping/Driver/DatabaseDriver.php @@ -35,6 +35,8 @@ /** * The DatabaseDriver reverse engineers the mapping metadata from a database. * + * @deprecated No replacement planned + * * @link www.doctrine-project.org */ class DatabaseDriver implements MappingDriver diff --git a/src/Mapping/Driver/XmlDriver.php b/src/Mapping/Driver/XmlDriver.php index 63f234e513a..48f650576c1 100644 --- a/src/Mapping/Driver/XmlDriver.php +++ b/src/Mapping/Driver/XmlDriver.php @@ -38,6 +38,8 @@ * XmlDriver is a metadata driver that enables mapping through XML files. * * @link www.doctrine-project.org + * + * @template-extends FileDriver */ class XmlDriver extends FileDriver { @@ -78,7 +80,6 @@ public function __construct( public function loadMetadataForClass($className, PersistenceClassMetadata $metadata): void { $xmlRoot = $this->getElement($className); - assert($xmlRoot instanceof SimpleXMLElement); if ($xmlRoot->getName() === 'entity') { if (isset($xmlRoot['repository-class'])) { @@ -134,6 +135,7 @@ public function loadMetadataForClass($className, PersistenceClassMetadata $metad ]; if (isset($discrColumn['options'])) { + assert($discrColumn['options'] instanceof SimpleXMLElement); $columnDef['options'] = $this->parseOptions($discrColumn['options']->children()); } @@ -145,6 +147,7 @@ public function loadMetadataForClass($className, PersistenceClassMetadata $metad // Evaluate if (isset($xmlRoot->{'discriminator-map'})) { $map = []; + assert($xmlRoot->{'discriminator-map'}->{'discriminator-mapping'} instanceof SimpleXMLElement); foreach ($xmlRoot->{'discriminator-map'}->{'discriminator-mapping'} as $discrMapElement) { $map[(string) $discrMapElement['value']] = (string) $discrMapElement['class']; } diff --git a/src/Mapping/InverseJoinColumn.php b/src/Mapping/InverseJoinColumn.php index 89c8db0006b..2a77f3fc73b 100644 --- a/src/Mapping/InverseJoinColumn.php +++ b/src/Mapping/InverseJoinColumn.php @@ -2,7 +2,6 @@ declare(strict_types=1); - namespace Doctrine\ORM\Mapping; use Attribute; diff --git a/src/Persisters/Collection/OneToManyPersister.php b/src/Persisters/Collection/OneToManyPersister.php index c62ea565ed5..0727b1f8a7e 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..b2d114a6698 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])) { @@ -386,12 +387,18 @@ private function generateUseLazyGhostTrait(ClassMetadata $class): string $code = substr($code, 7 + (int) strpos($code, "\n{")); $code = substr($code, 0, (int) strpos($code, "\n}")); $code = str_replace('LazyGhostTrait;', str_replace("\n ", "\n", 'LazyGhostTrait { - initializeLazyObject as __load; + initializeLazyObject as private; setLazyObjectAsInitialized as public __setInitialized; isLazyObjectInitialized as private; createLazyGhost as private; resetLazyObject as private; - }'), $code); + } + + public function __load(): void + { + $this->initializeLazyObject(); + } + '), $code); return $code; } 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 $resumes */ + $resumes = $this->_em + ->getRepository(GH10889Resume::class) + ->createQueryBuilder('resume') + ->leftJoin('resume.currentCompany', 'company')->addSelect('company') + ->getQuery() + ->getResult(); + + $this->assertArrayHasKey(0, $resumes); + $this->assertEquals(1, $resumes[0]->person->id); + $this->assertNull($resumes[0]->currentCompany); + } +} + +#[ORM\Entity] +class GH10889Person +{ + #[ORM\Id] + #[ORM\Column] + #[ORM\GeneratedValue] + public int|null $id = null; +} + +#[ORM\Entity] +class GH10889Company +{ + #[ORM\Id] + #[ORM\Column] + #[ORM\GeneratedValue] + public int|null $id = null; +} + +#[ORM\Entity] +class GH10889Resume +{ + public function __construct( + #[ORM\Id] + #[ORM\OneToOne] + public GH10889Person $person, + #[ORM\ManyToOne] + public GH10889Company|null $currentCompany, + ) { + } +} diff --git a/tests/Tests/ORM/Functional/Ticket/GH11487Test.php b/tests/Tests/ORM/Functional/Ticket/GH11487Test.php new file mode 100644 index 00000000000..3e622611454 --- /dev/null +++ b/tests/Tests/ORM/Functional/Ticket/GH11487Test.php @@ -0,0 +1,34 @@ +expectException(QueryException::class); + $this->expectExceptionMessage('Syntax Error'); + $this->_em->createQuery('UPDATE Doctrine\Tests\ORM\Functional\Ticket\TaxType t SET t.default =')->execute(); + } +} + +#[Entity] +class TaxType +{ + #[Column] + #[Id] + #[GeneratedValue] + public int|null $id = null; + + #[Column] + public bool $default = false; +} diff --git a/tests/Tests/ORM/Functional/Ticket/GH11500Test.php b/tests/Tests/ORM/Functional/Ticket/GH11500Test.php new file mode 100644 index 00000000000..b2f8123fabd --- /dev/null +++ b/tests/Tests/ORM/Functional/Ticket/GH11500Test.php @@ -0,0 +1,109 @@ +setUpEntitySchema([ + GH11500AbstractTestEntity::class, + GH11500TestEntityOne::class, + GH11500TestEntityTwo::class, + GH11500TestEntityHolder::class, + ]); + } + + /** @throws ORMException */ + public function testDeleteOneToManyCollectionWithSingleTableInheritance(): void + { + $testEntityOne = new GH11500TestEntityOne(); + $testEntityTwo = new GH11500TestEntityTwo(); + $testEntityHolder = new GH11500TestEntityHolder(); + + $testEntityOne->testEntityHolder = $testEntityHolder; + $testEntityHolder->testEntityOnes->add($testEntityOne); + + $testEntityTwo->testEntityHolder = $testEntityHolder; + $testEntityHolder->testEntityTwos->add($testEntityTwo); + + $em = $this->getEntityManager(); + $em->persist($testEntityOne); + $em->persist($testEntityTwo); + $em->persist($testEntityHolder); + $em->flush(); + + $testEntityTwosBeforeRemovalOfTestEntityOnes = $testEntityHolder->testEntityTwos->toArray(); + + $testEntityHolder->testEntityOnes = new ArrayCollection(); + $em->persist($testEntityHolder); + $em->flush(); + $em->refresh($testEntityHolder); + + static::assertEmpty($testEntityHolder->testEntityOnes->toArray(), 'All records should have been deleted'); + static::assertEquals($testEntityTwosBeforeRemovalOfTestEntityOnes, $testEntityHolder->testEntityTwos->toArray(), 'Different Entity\'s records should not have been deleted'); + } +} + + + +#[ORM\Entity] +#[ORM\Table(name: 'one_to_many_single_table_inheritance_test_entities')] +#[ORM\InheritanceType('SINGLE_TABLE')] +#[ORM\DiscriminatorColumn(name: 'type', type: 'string')] +#[ORM\DiscriminatorMap(['test_entity_one' => 'GH11500TestEntityOne', 'test_entity_two' => 'GH11500TestEntityTwo'])] +class GH11500AbstractTestEntity +{ + #[ORM\Id] + #[ORM\Column] + #[ORM\GeneratedValue] + public int|null $id = null; +} + + +#[ORM\Entity] +class GH11500TestEntityOne extends GH11500AbstractTestEntity +{ + #[ORM\ManyToOne(inversedBy:'testEntityOnes')] + #[ORM\JoinColumn(name:'test_entity_holder_id', referencedColumnName:'id')] + public GH11500TestEntityHolder|null $testEntityHolder = null; +} + +#[ORM\Entity] +class GH11500TestEntityTwo extends GH11500AbstractTestEntity +{ + #[ORM\ManyToOne(inversedBy:'testEntityTwos')] + #[ORM\JoinColumn(name:'test_entity_holder_id', referencedColumnName:'id')] + public GH11500TestEntityHolder|null $testEntityHolder = null; +} + +#[ORM\Entity] +class GH11500TestEntityHolder +{ + #[ORM\Id] + #[ORM\Column] + #[ORM\GeneratedValue] + public int|null $id = null; + + #[ORM\OneToMany(targetEntity: 'GH11500TestEntityOne', mappedBy: 'testEntityHolder', orphanRemoval: true)] + public Collection $testEntityOnes; + + #[ORM\OneToMany(targetEntity: 'GH11500TestEntityTwo', mappedBy: 'testEntityHolder', orphanRemoval: true)] + public Collection $testEntityTwos; + + public function __construct() + { + $this->testEntityOnes = new ArrayCollection(); + $this->testEntityTwos = new ArrayCollection(); + } +}