Skip to content

Commit

Permalink
Fix @SequenceGeneratorDefinition inheritance, take 2
Browse files Browse the repository at this point in the history
This is an alternative implementation to doctrine#11050. The difference is that with this PR here, once `@SequenceGeneratorDefinition` is used in an inheritance tree of entities and/or mapped superclasses, this definition (and in particular, the sequence name) will be inherited by all child entity/mapped superclasses.

Before doctrine#10455, the rules how `@SequenceGeneratorDefinition` is inherited from parent entity/mapped superclasses were as follows:

* Entity and mapped superclasses inherit the ID generator type (as given by `@GeneratedValue`) from their parent classes
* `@SequenceGeneratorDefinition`, however, is not generally inherited
* ... instead, a default sequence generator definition is created for every class when no explicit configuration is given. In this case, sequence names are based on the current class' table name.
* Once a root entity has been identified, all subclasses inherit its sequence generator definition unchanged.

But, this has to be considered together with the quirky mapping driver behaviour that was deprecated in doctrine#10455: The mapping driver would not report public and protected fields from mapped superclasses, so these were virtually "pushed down" to the next entity classes. 

That means `@SequenceGeneratorDefinition` on mapped superclasses would, in fact, be effective as-is for inheriting entity classes. This is what was covered by the tests in `BasicInheritanceMappingTest` that I marked with `@group doctrineGH-10927`.

My guess is that this PR will make it possible to opt-in to `reportFieldsWhereDeclared` (see doctrine#10455) and still get the same behaviour for mapped superclasses using `@SequenceGeneratorDefinition` as before. But maybe I don't see the full picture and all edge cases, so 👀 requested.

The `GH10927Test` test case validates the sequence names generated in a few cases. In fact, I wrote this test against the `2.15.x` branch to make sure we get results that are consistent with the previous behaviour.
  • Loading branch information
mpdude committed Nov 7, 2023
1 parent 609647a commit 8098da2
Show file tree
Hide file tree
Showing 5 changed files with 142 additions and 11 deletions.
8 changes: 5 additions & 3 deletions lib/Doctrine/ORM/Mapping/ClassMetadataFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ protected function doLoadMetadata($class, $parent, $rootEntityFound, array $nonS
if ($parent) {
$class->setInheritanceType($parent->inheritanceType);
$class->setDiscriminatorColumn($parent->discriminatorColumn);
$this->inheritIdGeneratorMapping($class, $parent);
$class->setIdGeneratorType($parent->generatorType);
$this->addInheritedFields($class, $parent);
$this->addInheritedRelations($class, $parent);
$this->addInheritedEmbeddedClasses($class, $parent);
Expand Down Expand Up @@ -141,8 +141,9 @@ protected function doLoadMetadata($class, $parent, $rootEntityFound, array $nonS
throw MappingException::reflectionFailure($class->getName(), $e);
}

// Complete id generator mapping when the generator was declared/added in this class
if ($class->identifier && (! $parent || ! $parent->identifier)) {
if ($parent && ($rootEntityFound || ($parent->sequenceGeneratorDefinition && $parent->sequenceGeneratorDefinition['explicit']))) {
$this->inheritIdGeneratorMapping($class, $parent);
} else {
$this->completeIdGeneratorMapping($class);
}

Expand Down Expand Up @@ -677,6 +678,7 @@ private function completeIdGeneratorMapping(ClassMetadataInfo $class): void
'sequenceName' => $this->truncateSequenceName($sequenceName),
'allocationSize' => 1,
'initialValue' => 1,
'explicit' => false,
];

if ($quoted) {
Expand Down
8 changes: 6 additions & 2 deletions lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php
Original file line number Diff line number Diff line change
Expand Up @@ -721,7 +721,7 @@ class ClassMetadataInfo implements ClassMetadata
* </code>
*
* @var array<string, mixed>|null
* @psalm-var array{sequenceName: string, allocationSize: string, initialValue: string, quoted?: mixed}|null
* @psalm-var array{sequenceName: string, allocationSize: string, initialValue: string, quoted?: mixed, explicit?: bool}|null
* @todo Merge with tableGeneratorDefinition into generic generatorDefinition
*/
public $sequenceGeneratorDefinition;
Expand Down Expand Up @@ -3433,14 +3433,18 @@ public function setCustomGeneratorDefinition(array $definition)
* )
* </code>
*
* @psalm-param array{sequenceName?: string, allocationSize?: int|string, initialValue?: int|string, quoted?: mixed} $definition
* @psalm-param array{sequenceName?: string, allocationSize?: int|string, initialValue?: int|string, quoted?: mixed, explicit?: bool} $definition
*
* @return void
*
* @throws MappingException
*/
public function setSequenceGeneratorDefinition(array $definition)
{
if (! isset($definition['explicit'])) {
$definition['explicit'] = true;
}

if (! isset($definition['sequenceName']) || trim($definition['sequenceName']) === '') {
throw MappingException::missingSequenceName($this->name);
}
Expand Down
6 changes: 3 additions & 3 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -536,12 +536,12 @@ parameters:
path: lib/Doctrine/ORM/Tools/Console/Command/MappingDescribeCommand.php

-
message: "#^Offset 'allocationSize' on array\\{sequenceName\\: string, allocationSize\\: string, initialValue\\: string, quoted\\?\\: mixed\\} in isset\\(\\) always exists and is not nullable\\.$#"
message: "#^Offset 'allocationSize' on array\\{sequenceName\\: string, allocationSize\\: string, initialValue\\: string, quoted\\?\\: mixed, explicit\\?\\: bool\\} in isset\\(\\) always exists and is not nullable\\.$#"
count: 1
path: lib/Doctrine/ORM/Tools/EntityGenerator.php

-
message: "#^Offset 'initialValue' on array\\{sequenceName\\: string, allocationSize\\: string, initialValue\\: string, quoted\\?\\: mixed\\} in isset\\(\\) always exists and is not nullable\\.$#"
message: "#^Offset 'initialValue' on array\\{sequenceName\\: string, allocationSize\\: string, initialValue\\: string, quoted\\?\\: mixed, explicit\\?\\: bool\\} in isset\\(\\) always exists and is not nullable\\.$#"
count: 1
path: lib/Doctrine/ORM/Tools/EntityGenerator.php

Expand All @@ -551,7 +551,7 @@ parameters:
path: lib/Doctrine/ORM/Tools/EntityGenerator.php

-
message: "#^Offset 'sequenceName' on array\\{sequenceName\\: string, allocationSize\\: string, initialValue\\: string, quoted\\?\\: mixed\\} in isset\\(\\) always exists and is not nullable\\.$#"
message: "#^Offset 'sequenceName' on array\\{sequenceName\\: string, allocationSize\\: string, initialValue\\: string, quoted\\?\\: mixed, explicit\\?\\: bool\\} in isset\\(\\) always exists and is not nullable\\.$#"
count: 1
path: lib/Doctrine/ORM/Tools/EntityGenerator.php

Expand Down
122 changes: 122 additions & 0 deletions tests/Doctrine/Tests/ORM/Functional/Ticket/GH10927Test.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
<?php

declare(strict_types=1);

namespace Doctrine\Tests\ORM\Functional\Ticket;

use Doctrine\DBAL\Platforms\PostgreSQLPlatform;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Tests\OrmFunctionalTestCase;

/**
* @group GH-10927
*/
class GH10927Test extends OrmFunctionalTestCase
{
protected function setUp(): void
{
parent::setUp();

$platform = $this->_em->getConnection()->getDatabasePlatform();
if (! $platform instanceof PostgreSQLPlatform) {
self::markTestSkipped('The ' . self::class . ' requires the use of postgresql.');
}

$this->setUpEntitySchema([
GH10927RootMappedSuperclass::class,
GH10927InheritedMappedSuperclass::class,
GH10927EntityA::class,
GH10927EntityB::class,
GH10927EntityC::class,
]);
}

public function testSequenceGeneratorDefinitionForRootMappedSuperclass(): void
{
$metadata = $this->_em->getClassMetadata(GH10927RootMappedSuperclass::class);

self::assertNull($metadata->sequenceGeneratorDefinition);
}

public function testSequenceGeneratorDefinitionForEntityA(): void
{
$metadata = $this->_em->getClassMetadata(GH10927EntityA::class);

self::assertSame('GH10927EntityA_id_seq', $metadata->sequenceGeneratorDefinition['sequenceName']);
}

public function testSequenceGeneratorDefinitionForInheritedMappedSuperclass(): void
{
$metadata = $this->_em->getClassMetadata(GH10927InheritedMappedSuperclass::class);

self::assertSame('GH10927InheritedMappedSuperclass_id_seq', $metadata->sequenceGeneratorDefinition['sequenceName']);
}

public function testSequenceGeneratorDefinitionForEntityB(): void
{
$metadata = $this->_em->getClassMetadata(GH10927EntityB::class);

self::assertSame('GH10927EntityB_id_seq', $metadata->sequenceGeneratorDefinition['sequenceName']);
}

public function testSequenceGeneratorDefinitionForEntityC(): void
{
$metadata = $this->_em->getClassMetadata(GH10927EntityC::class);

self::assertSame('GH10927EntityB_id_seq', $metadata->sequenceGeneratorDefinition['sequenceName']);
}
}

/**
* @ORM\MappedSuperclass()
*/
class GH10927RootMappedSuperclass
{
}

/**
* @ORM\Entity()
*/
class GH10927EntityA extends GH10927RootMappedSuperclass
{
/**
* @ORM\Id
* @ORM\GeneratedValue(strategy="SEQUENCE")
* @ORM\Column(type="integer")
*
* @var int|null
*/
private $id = null;
}

/**
* @ORM\MappedSuperclass()
*/
class GH10927InheritedMappedSuperclass extends GH10927RootMappedSuperclass
{
/**
* @ORM\Id
* @ORM\GeneratedValue(strategy="SEQUENCE")
* @ORM\Column(type="integer")
*
* @var int|null
*/
private $id = null;
}

/**
* @ORM\Entity()
* @ORM\InheritanceType("JOINED")
* @ORM\DiscriminatorColumn(name="discr", type="string")
* @ORM\DiscriminatorMap({"B" = "GH10927EntityB", "C" = "GH10927EntityC"})
*/
class GH10927EntityB extends GH10927InheritedMappedSuperclass
{
}

/**
* @ORM\Entity()
*/
class GH10927EntityC extends GH10927EntityB
{
}
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ public function testMappedSuperclassWithId(): void
/**
* @group DDC-1156
* @group DDC-1218
* @group GH-10927
*/
public function testGeneratedValueFromMappedSuperclass(): void
{
Expand All @@ -171,14 +172,15 @@ public function testGeneratedValueFromMappedSuperclass(): void

self::assertInstanceOf(IdSequenceGenerator::class, $class->idGenerator);
self::assertEquals(
['allocationSize' => 1, 'initialValue' => 10, 'sequenceName' => 'foo'],
['allocationSize' => 1, 'initialValue' => 10, 'sequenceName' => 'foo', 'explicit' => true],
$class->sequenceGeneratorDefinition
);
}

/**
* @group DDC-1156
* @group DDC-1218
* @group GH-10927
*/
public function testSequenceDefinitionInHierarchyWithSandwichMappedSuperclass(): void
{
Expand All @@ -187,14 +189,15 @@ public function testSequenceDefinitionInHierarchyWithSandwichMappedSuperclass():

self::assertInstanceOf(IdSequenceGenerator::class, $class->idGenerator);
self::assertEquals(
['allocationSize' => 1, 'initialValue' => 10, 'sequenceName' => 'foo'],
['allocationSize' => 1, 'initialValue' => 10, 'sequenceName' => 'foo', 'explicit' => true],
$class->sequenceGeneratorDefinition
);
}

/**
* @group DDC-1156
* @group DDC-1218
* @group GH-10927
*/
public function testMultipleMappedSuperclasses(): void
{
Expand All @@ -203,7 +206,7 @@ public function testMultipleMappedSuperclasses(): void

self::assertInstanceOf(IdSequenceGenerator::class, $class->idGenerator);
self::assertEquals(
['allocationSize' => 1, 'initialValue' => 10, 'sequenceName' => 'foo'],
['allocationSize' => 1, 'initialValue' => 10, 'sequenceName' => 'foo', 'explicit' => true],
$class->sequenceGeneratorDefinition
);
}
Expand Down

0 comments on commit 8098da2

Please sign in to comment.