diff --git a/src/Configurator.php b/src/Configurator.php index e5b06eb..4a1d9a4 100644 --- a/src/Configurator.php +++ b/src/Configurator.php @@ -112,7 +112,7 @@ public function initFields(EntitySchema $entity, \ReflectionClass $class, string } $field = $this->initField($property->getName(), $column, $class, $columnPrefix); - $field->setEntityClass($property->getDeclaringClass()->getName()); + $field->setEntityClass($this->findOwningEntity($class, $property->getDeclaringClass())->getName()); $entity->getFields()->set($property->getName(), $field); } } @@ -120,6 +120,12 @@ public function initFields(EntitySchema $entity, \ReflectionClass $class, string public function initRelations(EntitySchema $entity, \ReflectionClass $class): void { foreach ($class->getProperties() as $property) { + // ignore properties declared by parent entities + // otherwise all the relation columns declared in parent would be duplicated across all child tables in JTI + if ($this->findOwningEntity($class, $property->getDeclaringClass())->getName() !== $class->getName()) { + continue; + } + $metadata = $this->getPropertyMetadata($property, RelationAnnotation\RelationInterface::class); foreach ($metadata as $meta) { @@ -413,4 +419,37 @@ private function isOnInsertGeneratedField(Field $field): bool default => $field->isPrimary() }; } + + /** + * Function to find an owning entity class in the inheritance hierarchy. + * + * Entity classes may extend a base class and this function is needed route the properties from declaring class to the entity class. + * The function stops only when the declaring class is truly found, it does not naively stop on first entity. + * This behaviour makes it also functional in cases of Joined Table Inheritance on theoretically any number of nesting levels. + */ + private function findOwningEntity(\ReflectionClass $currentClass, \ReflectionClass $declaringClass): \ReflectionClass + { + // latest found entityClass before declaringClass + $latestEntityClass = $currentClass; + + do { + // we found declaringClass in the hierarchy + // in most cases the execution will stop here in first loop + if ($currentClass->getName() === $declaringClass->getName()) { + return $latestEntityClass; + } + + $currentClass = $currentClass->getParentClass(); + + // not possible to happen for logical reasons, but defensively check anyway + if (!$currentClass instanceof \ReflectionClass) { + return $latestEntityClass; + } + + // if a currentClass in hierarchy is an entity on its own, the property belongs to that entity + if (\count($currentClass->getAttributes(Entity::class)) > 0) { + $latestEntityClass = $currentClass; + } + } while (true); // the inheritance hierarchy cannot be infinite + } } diff --git a/src/TableInheritance.php b/src/TableInheritance.php index 176a36f..3152c15 100644 --- a/src/TableInheritance.php +++ b/src/TableInheritance.php @@ -66,7 +66,11 @@ public function run(Registry $registry): Registry // All child should be presented in a schema as separated entity // Every child will be handled according its table inheritance type - \assert($child->getRole() !== null && $entity !== null && isset($parent)); + // todo should $parent be not null? + // \assert(isset($parent)); + + \assert($child->getRole() !== null && $entity !== null); + if (!$registry->hasEntity($child->getRole())) { $registry->register($child); diff --git a/tests/Annotated/Fixtures/Fixtures16/Executive.php b/tests/Annotated/Fixtures/Fixtures16/Executive.php index 4e9f4f9..b09be11 100644 --- a/tests/Annotated/Fixtures/Fixtures16/Executive.php +++ b/tests/Annotated/Fixtures/Fixtures16/Executive.php @@ -7,6 +7,7 @@ use Cycle\Annotated\Annotation\Column; use Cycle\Annotated\Annotation\Entity; use Cycle\Annotated\Annotation\Inheritance\JoinedTable as InheritanceJoinedTable; +use Cycle\Annotated\Annotation\Relation\HasOne; /** * @Entity @@ -21,4 +22,12 @@ class Executive extends ExecutiveProxy /** @Column(type="int") */ #[Column(type: 'int')] public int $bonus; + + /** @Column(type="int", nullable=true, typecast="int") */ + #[Column(type: 'int', nullable: true, typecast: 'int')] + public ?int $added_tool_id; + + /** @HasOne(target=Tool::class, innerKey="added_tool_id", outerKey="added_tool_id", nullable=true) */ + #[HasOne(target: Tool::class, innerKey: 'added_tool_id', outerKey: 'added_tool_id', nullable: true)] + public Tool $addedTool; } diff --git a/tests/Annotated/Fixtures/Fixtures16/Executive2.php b/tests/Annotated/Fixtures/Fixtures16/Executive2.php new file mode 100644 index 0000000..571707f --- /dev/null +++ b/tests/Annotated/Fixtures/Fixtures16/Executive2.php @@ -0,0 +1,18 @@ +foo_id; diff --git a/tests/Annotated/Fixtures/Fixtures16/Tool.php b/tests/Annotated/Fixtures/Fixtures16/Tool.php new file mode 100644 index 0000000..5a19424 --- /dev/null +++ b/tests/Annotated/Fixtures/Fixtures16/Tool.php @@ -0,0 +1,22 @@ + new AnnotationReader()]; - yield ['Attribute reader' => new AttributeReader()]; + yield 'Annotation reader' => [new AnnotationReader()]; + yield 'Attribute reader' => [new AttributeReader()]; } public static function allReadersProvider(): \Traversable { yield from static::singularReadersProvider(); - yield ['Selective reader' => new SelectiveReader([new AttributeReader(), new AnnotationReader()])]; + yield 'Selective reader' => [new SelectiveReader([new AttributeReader(), new AnnotationReader()])]; } protected function getDatabase(): Database diff --git a/tests/Annotated/Functional/Driver/Common/Inheritance/JoinedTableTestCase.php b/tests/Annotated/Functional/Driver/Common/Inheritance/JoinedTableTestCase.php index 52ec0e6..64d32f5 100644 --- a/tests/Annotated/Functional/Driver/Common/Inheritance/JoinedTableTestCase.php +++ b/tests/Annotated/Functional/Driver/Common/Inheritance/JoinedTableTestCase.php @@ -125,7 +125,7 @@ public function testTableInheritance(ReaderInterface $reader): void $this->assertSame('secret', $loadedExecutive->hidden); $this->assertSame(15000, $loadedExecutive->bonus); $this->assertSame('executive', $loadedExecutive->getType()); - $this->assertNull($loadedExecutive->proxyFieldWithAnnotation); + $this->assertSame('value', $loadedExecutive->proxyFieldWithAnnotation); } #[DataProvider('allReadersProvider')] diff --git a/tests/Annotated/Functional/Driver/Common/InheritanceTestCase.php b/tests/Annotated/Functional/Driver/Common/InheritanceTestCase.php index 515fd52..5ae008e 100644 --- a/tests/Annotated/Functional/Driver/Common/InheritanceTestCase.php +++ b/tests/Annotated/Functional/Driver/Common/InheritanceTestCase.php @@ -16,6 +16,7 @@ use Cycle\Annotated\Tests\Fixtures\Fixtures16\Employee; use Cycle\Annotated\Tests\Fixtures\Fixtures16\Executive; use Cycle\Annotated\Tests\Fixtures\Fixtures16\Person; +use Cycle\Annotated\Tests\Fixtures\Fixtures16\Tool; use Cycle\ORM\SchemaInterface; use Cycle\Schema\Compiler; use Cycle\Schema\Generator\GenerateRelations; @@ -69,6 +70,9 @@ public function testTableInheritance(ReaderInterface $reader): void // Ceo - Single table inheritance {value: ceo} // Beaver - Separate table + // Tool + $this->assertArrayHasKey('tool', $schema); + // Person $this->assertCount(3, $schema['person'][SchemaInterface::CHILDREN]); $this->assertEquals([ @@ -86,38 +90,56 @@ public function testTableInheritance(ReaderInterface $reader): void // 'bonus' => 'bonus', // JTI 'preferences' => 'preferences', 'stocks' => 'stocks', + 'tool_id' => 'tool_id', // 'teethAmount' => 'teeth_amount', // Not child ], $schema['person'][SchemaInterface::COLUMNS]); $this->assertEmpty($schema['person'][SchemaInterface::PARENT] ?? null); $this->assertEmpty($schema['person'][SchemaInterface::PARENT_KEY] ?? null); $this->assertSame('people', $schema['person'][SchemaInterface::TABLE]); + $this->assertCount(1, $schema['person'][SchemaInterface::RELATIONS]); // Employee $this->assertArrayHasKey('employee', $schema); $this->assertCount(1, $schema['employee']); $this->assertSame(Employee::class, $schema['employee'][SchemaInterface::ENTITY]); $this->assertNull($schema['employee'][SchemaInterface::TABLE] ?? null); + $this->assertCount(0, $schema['employee'][SchemaInterface::RELATIONS] ?? []); // Customer $this->assertArrayHasKey('customer', $schema); $this->assertCount(1, $schema['customer']); $this->assertSame(Customer::class, $schema['customer'][SchemaInterface::ENTITY]); $this->assertNull($schema['customer'][SchemaInterface::TABLE] ?? null); + $this->assertCount(0, $schema['customer'][SchemaInterface::RELATIONS] ?? []); // Executive $this->assertSame('employee', $schema['executive'][SchemaInterface::PARENT]); $this->assertSame('foo_id', $schema['executive'][SchemaInterface::PARENT_KEY]); $this->assertSame('executives', $schema['executive'][SchemaInterface::TABLE]); - $this->assertSame( - ['bonus' => 'bonus', 'foo_id' => 'id', 'hidden' => 'hidden'], - $schema['executive'][SchemaInterface::COLUMNS] + $this->assertEquals( + [ + 'bonus' => 'bonus', + 'proxyFieldWithAnnotation' => 'proxy', + 'foo_id' => 'id', + 'hidden' => 'hidden', + 'added_tool_id' => 'added_tool_id', + ], + $schema['executive'][SchemaInterface::COLUMNS], ); + $this->assertCount(1, $schema['executive'][SchemaInterface::RELATIONS]); + + // Executive2 + $this->assertSame('executive', $schema['executive2'][SchemaInterface::PARENT]); + $this->assertSame('foo_id', $schema['executive2'][SchemaInterface::PARENT_KEY]); + $this->assertEquals(['foo_id' => 'id'], $schema['executive2'][SchemaInterface::COLUMNS]); + $this->assertCount(0, $schema['executive2'][SchemaInterface::RELATIONS]); // Ceo $this->assertArrayHasKey('ceo', $schema); $this->assertCount(1, $schema['ceo']); $this->assertSame(Ceo::class, $schema['ceo'][SchemaInterface::ENTITY]); $this->assertNull($schema['ceo'][SchemaInterface::TABLE] ?? null); + $this->assertCount(0, $schema['ceo'][SchemaInterface::RELATIONS] ?? []); // Beaver $this->assertEmpty($schema['beaver'][SchemaInterface::DISCRIMINATOR] ?? null); @@ -125,13 +147,15 @@ public function testTableInheritance(ReaderInterface $reader): void $this->assertEmpty($schema['beaver'][SchemaInterface::PARENT_KEY] ?? null); $this->assertEmpty($schema['beaver'][SchemaInterface::CHILDREN] ?? null); $this->assertSame('beavers', $schema['beaver'][SchemaInterface::TABLE]); - $this->assertSame([ + $this->assertEquals([ 'teethAmount' => 'teeth_amount', 'foo_id' => 'id', 'name' => 'name', 'type' => 'type', 'hidden' => 'hidden', + 'tool_id' => 'tool_id', ], $schema['beaver'][SchemaInterface::COLUMNS]); + $this->assertCount(1, $schema['beaver'][SchemaInterface::RELATIONS] ?? []); } public function testTableInheritanceWithIncorrectClassesOrder(): void @@ -145,6 +169,7 @@ public function testTableInheritanceWithIncorrectClassesOrder(): void new \ReflectionClass(Employee::class), new \ReflectionClass(Executive::class), new \ReflectionClass(Person::class), + new \ReflectionClass(Tool::class), ]); $schema = (new Compiler())->compile($r, [ diff --git a/tests/docker-compose.yml b/tests/docker-compose.yml index 5ee2054..2c2cf73 100644 --- a/tests/docker-compose.yml +++ b/tests/docker-compose.yml @@ -11,7 +11,7 @@ services: ACCEPT_EULA: "Y" mysql_latest: - image: mysql:latest + image: mysql:8.0 restart: always command: --default-authentication-plugin=mysql_native_password ports: