From 9696c3434d8cc8d4f0f54b170cba65772bb5eb6d Mon Sep 17 00:00:00 2001 From: Alix Mauro Date: Fri, 19 Jan 2024 14:16:24 +0100 Subject: [PATCH 1/9] Consider usage of setFetchMode when checking for simultaneous usage of fetch-mode EAGER and WITH condition. This fixes a bug that arises when an entity relation is mapped with fetch-mode EAGER but setFetchMode LAZY (or anything that is not EAGER) has been used on the query. If the query use WITH condition, an exception is incorrectly raised (Associations with fetch-mode=EAGER may not be using WITH conditions). Fixes #11128 Co-Authored-By: Albert Prat --- src/Query/SqlWalker.php | 4 +++- tests/Tests/ORM/Functional/EagerFetchCollectionTest.php | 8 ++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/Query/SqlWalker.php b/src/Query/SqlWalker.php index 4948be46536..4c25fb63a68 100644 --- a/src/Query/SqlWalker.php +++ b/src/Query/SqlWalker.php @@ -1062,7 +1062,9 @@ public function walkJoinAssociationDeclaration($joinAssociationDeclaration, $joi } } - 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/ORM/Functional/EagerFetchCollectionTest.php b/tests/Tests/ORM/Functional/EagerFetchCollectionTest.php index ff0eab56d63..88397c6a12f 100644 --- a/tests/Tests/ORM/Functional/EagerFetchCollectionTest.php +++ b/tests/Tests/ORM/Functional/EagerFetchCollectionTest.php @@ -88,6 +88,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); From 75bc22980ef85a5774696d9660b91b0006ea89a0 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Thu, 23 May 2024 18:27:17 +0200 Subject: [PATCH 2/9] Fix cloning entities --- psalm-baseline.xml | 4 +- src/Proxy/ProxyFactory.php | 13 ++--- .../Models/ECommerce/ECommerceProduct2.php | 52 +++++++++++++++++++ .../Functional/ProxiesLikeEntitiesTest.php | 2 +- .../ORM/Functional/ReferenceProxyTest.php | 19 +++++++ 5 files changed, 82 insertions(+), 8 deletions(-) create mode 100644 tests/Tests/Models/ECommerce/ECommerceProduct2.php diff --git a/psalm-baseline.xml b/psalm-baseline.xml index 7a3b729e849..d191c3782ff 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -1502,7 +1502,9 @@ - + diff --git a/src/Proxy/ProxyFactory.php b/src/Proxy/ProxyFactory.php index 5b2d2eca0c9..dc8a72bfcea 100644 --- a/src/Proxy/ProxyFactory.php +++ b/src/Proxy/ProxyFactory.php @@ -354,15 +354,14 @@ private function createInitializer(ClassMetadata $classMetadata, EntityPersister /** * 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( @@ -378,7 +377,7 @@ private function createLazyInitializer(ClassMetadata $classMetadata, EntityPersi $class = $entityPersister->getClassMetadata(); foreach ($class->getReflectionProperties() as $property) { - if (! $class->hasField($property->name) && ! $class->hasAssociation($property->name)) { + if (isset($identifier[$property->name]) || ! $class->hasField($property->name) && ! $class->hasAssociation($property->name)) { continue; } @@ -468,7 +467,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/tests/Tests/Models/ECommerce/ECommerceProduct2.php b/tests/Tests/Models/ECommerce/ECommerceProduct2.php new file mode 100644 index 00000000000..89f37417d2b --- /dev/null +++ b/tests/Tests/Models/ECommerce/ECommerceProduct2.php @@ -0,0 +1,52 @@ +id; + } + + public function getName(): string + { + return $this->name; + } + + public function __clone() + { + $this->id = null; + $this->name = 'Clone of ' . $this->name; + } +} diff --git a/tests/Tests/ORM/Functional/ProxiesLikeEntitiesTest.php b/tests/Tests/ORM/Functional/ProxiesLikeEntitiesTest.php index 01f82c8de7d..1cd05c3fc5a 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 88c14253e20..bb4a2cfb36c 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; @@ -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 { From 93c2dd9d4b74dd78fe17834b2be70df88cca55c9 Mon Sep 17 00:00:00 2001 From: Indra Gunawan Date: Mon, 20 May 2024 18:41:40 +0800 Subject: [PATCH 3/9] update EntityManager#transactional to EntityManager#wrapInTransaction One has been deprecated in favor of the other. --- .../transactions-and-concurrency.rst | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/docs/en/reference/transactions-and-concurrency.rst b/docs/en/reference/transactions-and-concurrency.rst index 9e477474280..afcbb216bce 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 From 06eca401340d3a4387a940f5777c68b8f0b0b3fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20Paris?= Date: Sun, 5 May 2024 23:22:29 +0200 Subject: [PATCH 4/9] Use ramsey/composer-install in PHPBench workflow It will handle caching for us. --- .github/workflows/phpbench.yml | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/.github/workflows/phpbench.yml b/.github/workflows/phpbench.yml index 1e7ad8c10d1..b223e635930 100644 --- a/.github/workflows/phpbench.yml +++ b/.github/workflows/phpbench.yml @@ -47,15 +47,8 @@ jobs: coverage: "pcov" ini-values: "zend.assertions=1, apc.enable_cli=1" - - name: "Cache dependencies installed with composer" - uses: "actions/cache@v3" - with: - path: "~/.composer/cache" - key: "php-${{ matrix.php-version }}-composer-locked-${{ hashFiles('composer.lock') }}" - restore-keys: "php-${{ matrix.php-version }}-composer-locked-" - - - name: "Install dependencies with composer" - run: "composer update --no-interaction --no-progress" + - name: "Install dependencies with Composer" + uses: "ramsey/composer-install@v3" - name: "Run PHPBench" run: "vendor/bin/phpbench run --report=default" From 59c8bc09abf1f878694346986dc0755a6aed9fd2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20Paris?= Date: Mon, 3 Jun 2024 23:08:27 +0200 Subject: [PATCH 5/9] Replace assertion with exception (#11489) --- src/Query/Parser.php | 5 ++- .../ORM/Functional/Ticket/GH11487Test.php | 40 +++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 tests/Tests/ORM/Functional/Ticket/GH11487Test.php diff --git a/src/Query/Parser.php b/src/Query/Parser.php index 949a8f4ebdd..10cb008d12a 100644 --- a/src/Query/Parser.php +++ b/src/Query/Parser.php @@ -2924,7 +2924,10 @@ public function ArithmeticPrimary() 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/tests/Tests/ORM/Functional/Ticket/GH11487Test.php b/tests/Tests/ORM/Functional/Ticket/GH11487Test.php new file mode 100644 index 00000000000..ef036e48ada --- /dev/null +++ b/tests/Tests/ORM/Functional/Ticket/GH11487Test.php @@ -0,0 +1,40 @@ +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 +{ + /** + * @var int|null + * @Column(type="integer") + * @Id + * @GeneratedValue + */ + public $id; + + /** + * @var bool + * @Column(type="boolean") + */ + public $default = false; +} From 87a8ee21c9eb88bb7037ad66b339816c24f6f2d7 Mon Sep 17 00:00:00 2001 From: Sam Mousa Date: Tue, 11 Jun 2024 16:21:28 +0200 Subject: [PATCH 6/9] fix(docs): use string value in `addAttribute` --- docs/en/tutorials/composite-primary-keys.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/en/tutorials/composite-primary-keys.rst b/docs/en/tutorials/composite-primary-keys.rst index 456adeaf5de..386f8f140c0 100644 --- a/docs/en/tutorials/composite-primary-keys.rst +++ b/docs/en/tutorials/composite-primary-keys.rst @@ -188,7 +188,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); } From 39153fd88a8c3cbc008b7ab6ff19899d48c07aa1 Mon Sep 17 00:00:00 2001 From: Daniel Black Date: Fri, 14 Jun 2024 03:34:46 +1000 Subject: [PATCH 7/9] ci: maintained and stable mariadb version (11.4 current lts) (#11490) Also use MARIADB env names and the healthcheck.sh included in the container. --- .github/workflows/continuous-integration.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 47453fdae52..c6c3cb752aa 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -190,7 +190,7 @@ jobs: - "default" - "3@dev" mariadb-version: - - "10.9" + - "11.4" extension: - "mysqli" - "pdo_mysql" @@ -204,11 +204,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" From 3b499132d9560263c4d93ea623fbf66abd186ba6 Mon Sep 17 00:00:00 2001 From: Noemi Salaun Date: Sun, 28 Jan 2024 18:32:39 +0100 Subject: [PATCH 8/9] Skip joined entity creation for empty relation (#10889) --- src/Internal/Hydration/ObjectHydrator.php | 12 +- .../ORM/Functional/Ticket/GH10889Test.php | 107 ++++++++++++++++++ 2 files changed, 115 insertions(+), 4 deletions(-) create mode 100644 tests/Tests/ORM/Functional/Ticket/GH10889Test.php diff --git a/src/Internal/Hydration/ObjectHydrator.php b/src/Internal/Hydration/ObjectHydrator.php index a5d97d30966..c01496a5ca8 100644 --- a/src/Internal/Hydration/ObjectHydrator.php +++ b/src/Internal/Hydration/ObjectHydrator.php @@ -367,11 +367,15 @@ protected function hydrateRowData(array $row, array &$result) $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/tests/Tests/ORM/Functional/Ticket/GH10889Test.php b/tests/Tests/ORM/Functional/Ticket/GH10889Test.php new file mode 100644 index 00000000000..451fc887d20 --- /dev/null +++ b/tests/Tests/ORM/Functional/Ticket/GH10889Test.php @@ -0,0 +1,107 @@ +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(type="integer") + * @ORM\GeneratedValue + * + * @var int + */ + public $id; +} + +/** + * @ORM\Entity + */ +class GH10889Company +{ + /** + * @ORM\Id + * @ORM\Column(type="integer") + * @ORM\GeneratedValue + * + * @var int + */ + public $id; +} + +/** + * @ORM\Entity + */ +class GH10889Resume +{ + /** + * @ORM\Id + * @ORM\OneToOne(targetEntity="GH10889Person") + * + * @var GH10889Person + */ + public $person; + + /** + * @ORM\ManyToOne(targetEntity="GH10889Company") + * + * @var GH10889Company|null + */ + public $currentCompany; + + public function __construct(GH10889Person $person, ?GH10889Company $currentCompany) + { + $this->person = $person; + $this->currentCompany = $currentCompany; + } +} From e4d46c4276b48cece92443c69990a6d12bd4effa Mon Sep 17 00:00:00 2001 From: Kyron Taylor Date: Sat, 15 Jun 2024 00:45:36 +0100 Subject: [PATCH 9/9] Fix OneToManyPersister::deleteEntityCollection missing discriminator column/value. (GH-11500) --- .../Collection/OneToManyPersister.php | 15 +- .../ORM/Functional/Ticket/GH11500Test.php | 137 ++++++++++++++++++ 2 files changed, 151 insertions(+), 1 deletion(-) create mode 100644 tests/Tests/ORM/Functional/Ticket/GH11500Test.php diff --git a/src/Persisters/Collection/OneToManyPersister.php b/src/Persisters/Collection/OneToManyPersister.php index f39415fc0c9..6769acca909 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\PersistentCollection; use Doctrine\ORM\Utility\PersisterHelper; @@ -166,7 +168,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 = $collection->getMapping(); @@ -186,6 +192,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/tests/Tests/ORM/Functional/Ticket/GH11500Test.php b/tests/Tests/ORM/Functional/Ticket/GH11500Test.php new file mode 100644 index 00000000000..4be3bf2b76e --- /dev/null +++ b/tests/Tests/ORM/Functional/Ticket/GH11500Test.php @@ -0,0 +1,137 @@ +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(type="integer") + * @ORM\GeneratedValue + * + * @var int + */ + public $id; +} + + +/** @ORM\Entity */ +class GH11500TestEntityOne extends GH11500AbstractTestEntity +{ + /** + * @ORM\ManyToOne(targetEntity="GH11500TestEntityHolder", inversedBy="testEntityOnes") + * @ORM\JoinColumn(name="test_entity_holder_id", referencedColumnName="id") + * + * @var GH11500TestEntityHolder + */ + public $testEntityHolder; +} + +/** @ORM\Entity */ +class GH11500TestEntityTwo extends GH11500AbstractTestEntity +{ + /** + * @ORM\ManyToOne(targetEntity="GH11500TestEntityHolder", inversedBy="testEntityTwos") + * @ORM\JoinColumn(name="test_entity_holder_id", referencedColumnName="id") + * + * @var GH11500TestEntityHolder + */ + public $testEntityHolder; +} + +/** @ORM\Entity */ +class GH11500TestEntityHolder +{ + /** + * @ORM\Id + * @ORM\Column(type="integer") + * @ORM\GeneratedValue + * + * @var int + */ + public $id; + + /** + * @ORM\OneToMany(targetEntity="GH11500TestEntityOne", mappedBy="testEntityHolder", orphanRemoval=true) + * + * @var Collection + */ + public $testEntityOnes; + + /** + * @ORM\OneToMany(targetEntity="GH11500TestEntityTwo", mappedBy="testEntityHolder", orphanRemoval=true) + * + * @var Collection + */ + public $testEntityTwos; + + public function __construct() + { + $this->testEntityOnes = new ArrayCollection(); + $this->testEntityTwos = new ArrayCollection(); + } +}