Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 0 additions & 5 deletions psalm-baseline.xml
Original file line number Diff line number Diff line change
Expand Up @@ -254,11 +254,6 @@
<code><![CDATA[$column]]></code>
<code><![CDATA[$column]]></code>
</MixedAssignment>
<RedundantConditionGivenDocblockType>
<code><![CDATA[$child->getRole() !== null && $entity !== null]]></code>
<code><![CDATA[$entity !== null]]></code>
<code><![CDATA[\assert($child->getRole() !== null && $entity !== null && isset($parent))]]></code>
</RedundantConditionGivenDocblockType>
<RiskyTruthyFalsyComparison>
<code><![CDATA[$outerKey]]></code>
<code><![CDATA[empty($tableName)]]></code>
Expand Down
48 changes: 47 additions & 1 deletion src/Configurator.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use Cycle\Annotated\Annotation\Entity;
use Cycle\Annotated\Annotation\ForeignKey;
use Cycle\Annotated\Annotation\GeneratedValue;
use Cycle\Annotated\Annotation\Inheritance;
use Cycle\Annotated\Annotation\Relation as RelationAnnotation;
use Cycle\Annotated\Exception\AnnotationException;
use Cycle\Annotated\Exception\AnnotationRequiredArgumentsException;
Expand Down Expand Up @@ -124,14 +125,26 @@ 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);
}
}

public function initRelations(EntitySchema $entity, \ReflectionClass $class): void
{
// Only STI/JTI children must skip relations declared by parent entities — for them, the parent table already
// owns those relations. Entities that merely extend another entity physically (separate table) must keep them.
$isInheritanceChild = $this->reader->firstClassMetadata($class, Inheritance::class) !== null;

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 ($isInheritanceChild
&& $this->findOwningEntity($class, $property->getDeclaringClass())->getName() !== $class->getName()
) {
continue;
}

$metadata = $this->getPropertyMetadata($property, RelationAnnotation\RelationInterface::class);

foreach ($metadata as $meta) {
Expand Down Expand Up @@ -425,4 +438,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
}
}
6 changes: 5 additions & 1 deletion src/TableInheritance.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);

if (!$registry->hasEntity($child->getRole())) {
$registry->register($child);

Expand Down
9 changes: 9 additions & 0 deletions tests/Annotated/Fixtures/Fixtures16/Executive.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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, fkCreate=false) */
#[HasOne(target: Tool::class, innerKey: 'added_tool_id', outerKey: 'added_tool_id', nullable: true, fkCreate: false)]
public Tool $addedTool;
}
16 changes: 16 additions & 0 deletions tests/Annotated/Fixtures/Fixtures16/Executive2.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

declare(strict_types=1);

namespace Cycle\Annotated\Tests\Fixtures\Fixtures16;

use Cycle\Annotated\Annotation\Entity;
use Cycle\Annotated\Annotation\Inheritance\JoinedTable as InheritanceJoinedTable;

/**
* @Entity
* @InheritanceJoinedTable(outerKey="foo_id")
*/
#[Entity]
#[InheritanceJoinedTable(outerKey: 'foo_id')]
class Executive2 extends Executive {}
5 changes: 2 additions & 3 deletions tests/Annotated/Fixtures/Fixtures16/ExecutiveProxy.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,11 @@
/**
* This proxy class doesn't have an {@see Entity} annotation (attribute) declaration,
* and it shouldn't be presented in Schema.
* Note: this behavior might be improved. There will be added support for
* annotated base class columns without Entity annotation declaration.
* But all the classes that extend this class should contain all the fields from this class.
*/
class ExecutiveProxy extends Employee
{
/** @Column(type="string") */
/** @Column(type="string", name="proxy") */
#[Column(type: 'string', name: 'proxy')]
public ?string $proxyFieldWithAnnotation = null;

Expand Down
9 changes: 9 additions & 0 deletions tests/Annotated/Fixtures/Fixtures16/Person.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use Cycle\Annotated\Annotation\Column;
use Cycle\Annotated\Annotation\Entity;
use Cycle\Annotated\Annotation\Inheritance\DiscriminatorColumn;
use Cycle\Annotated\Annotation\Relation\HasOne;

/**
* @Entity
Expand All @@ -24,6 +25,14 @@ class Person
#[Column(type: 'string')]
public string $type;

/** @Column(type="int", nullable=true, typecast="int") */
#[Column(type: 'int', nullable: true, typecast: 'int')]
public ?int $tool_id;

/** @HasOne(target=Tool::class, innerKey="id", outerKey="tool_id", nullable=true, fkCreate=false) */
#[HasOne(target: Tool::class, innerKey: 'id', outerKey: 'tool_id', nullable: true, fkCreate: false)]
public Tool $tool;

/** @Column(type="primary", name="id") */
#[Column(type: 'primary', name: 'id')]
protected int $foo_id;
Expand Down
22 changes: 22 additions & 0 deletions tests/Annotated/Fixtures/Fixtures16/Tool.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

declare(strict_types=1);

namespace Cycle\Annotated\Tests\Fixtures\Fixtures16;

use Cycle\Annotated\Annotation\Column;
use Cycle\Annotated\Annotation\Entity;
use Cycle\Annotated\Annotation\Inheritance\DiscriminatorColumn;

/**
* @Entity
* @DiscriminatorColumn(name="type")
*/
#[Entity]
#[DiscriminatorColumn(name: 'type')]
class Tool
{
/** @Column(type="primary", name="id") */
#[Column(type: 'primary', name: 'id')]
public int $tool_id;
}
17 changes: 17 additions & 0 deletions tests/Annotated/Fixtures/Fixtures27/BtJoined.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

declare(strict_types=1);

namespace Cycle\Annotated\Tests\Fixtures\Fixtures27;

use Cycle\Annotated\Annotation\Column;
use Cycle\Annotated\Annotation\Entity;
use Cycle\Annotated\Annotation\Inheritance\JoinedTable;

#[Entity]
#[JoinedTable]
class BtJoined extends BtParent
{
#[Column(type: 'int')]
public int $bonus;
}
24 changes: 24 additions & 0 deletions tests/Annotated/Fixtures/Fixtures27/BtParent.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

declare(strict_types=1);

namespace Cycle\Annotated\Tests\Fixtures\Fixtures27;

use Cycle\Annotated\Annotation\Column;
use Cycle\Annotated\Annotation\Entity;
use Cycle\Annotated\Annotation\Inheritance\DiscriminatorColumn;
use Cycle\Annotated\Annotation\Relation\BelongsTo;

#[Entity]
#[DiscriminatorColumn(name: 'type')]
class BtParent
{
#[Column(type: 'primary', name: 'id')]
public int $id;

#[Column(type: 'string')]
public string $type;

#[BelongsTo(target: BtTarget::class, nullable: true, fkCreate: false)]
public ?BtTarget $target = null;
}
15 changes: 15 additions & 0 deletions tests/Annotated/Fixtures/Fixtures27/BtTarget.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

declare(strict_types=1);

namespace Cycle\Annotated\Tests\Fixtures\Fixtures27;

use Cycle\Annotated\Annotation\Column;
use Cycle\Annotated\Annotation\Entity;

#[Entity]
class BtTarget
{
#[Column(type: 'primary', name: 'id')]
public int $id;
}
17 changes: 17 additions & 0 deletions tests/Annotated/Fixtures/Fixtures27/JgLeaf.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

declare(strict_types=1);

namespace Cycle\Annotated\Tests\Fixtures\Fixtures27;

use Cycle\Annotated\Annotation\Column;
use Cycle\Annotated\Annotation\Entity;
use Cycle\Annotated\Annotation\Inheritance\JoinedTable;

#[Entity]
#[JoinedTable]
class JgLeaf extends JgMid
{
#[Column(type: 'int')]
public int $extra;
}
21 changes: 21 additions & 0 deletions tests/Annotated/Fixtures/Fixtures27/JgMid.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

declare(strict_types=1);

namespace Cycle\Annotated\Tests\Fixtures\Fixtures27;

use Cycle\Annotated\Annotation\Column;
use Cycle\Annotated\Annotation\Entity;
use Cycle\Annotated\Annotation\Inheritance\JoinedTable;
use Cycle\Annotated\Annotation\Relation\HasOne;

#[Entity]
#[JoinedTable]
class JgMid extends JgParent
{
#[Column(type: 'int', nullable: true)]
public ?int $target_id = null;

#[HasOne(target: JgTarget::class, innerKey: 'target_id', outerKey: 'id', nullable: true, fkCreate: false)]
public ?JgTarget $target = null;
}
15 changes: 15 additions & 0 deletions tests/Annotated/Fixtures/Fixtures27/JgParent.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

declare(strict_types=1);

namespace Cycle\Annotated\Tests\Fixtures\Fixtures27;

use Cycle\Annotated\Annotation\Column;
use Cycle\Annotated\Annotation\Entity;

#[Entity]
class JgParent
{
#[Column(type: 'primary', name: 'id')]
public int $id;
}
15 changes: 15 additions & 0 deletions tests/Annotated/Fixtures/Fixtures27/JgTarget.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

declare(strict_types=1);

namespace Cycle\Annotated\Tests\Fixtures\Fixtures27;

use Cycle\Annotated\Annotation\Column;
use Cycle\Annotated\Annotation\Entity;

#[Entity]
class JgTarget
{
#[Column(type: 'primary', name: 'id')]
public int $id;
}
27 changes: 27 additions & 0 deletions tests/Annotated/Fixtures/Fixtures27/SoParent.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

declare(strict_types=1);

namespace Cycle\Annotated\Tests\Fixtures\Fixtures27;

use Cycle\Annotated\Annotation\Column;
use Cycle\Annotated\Annotation\Entity;
use Cycle\Annotated\Annotation\Inheritance\DiscriminatorColumn;
use Cycle\Annotated\Annotation\Relation\HasOne;

#[Entity]
#[DiscriminatorColumn(name: 'type')]
class SoParent
{
#[Column(type: 'primary', name: 'id')]
public int $id;

#[Column(type: 'string')]
public string $type;

#[Column(type: 'int', nullable: true)]
public ?int $target_id = null;

#[HasOne(target: SoTarget::class, innerKey: 'target_id', outerKey: 'id', nullable: true, fkCreate: false)]
public ?SoTarget $target = null;
}
15 changes: 15 additions & 0 deletions tests/Annotated/Fixtures/Fixtures27/SoSeparate.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

declare(strict_types=1);

namespace Cycle\Annotated\Tests\Fixtures\Fixtures27;

use Cycle\Annotated\Annotation\Column;
use Cycle\Annotated\Annotation\Entity;

#[Entity]
class SoSeparate extends SoSti
{
#[Column(type: 'string')]
public string $extra;
}
12 changes: 12 additions & 0 deletions tests/Annotated/Fixtures/Fixtures27/SoSti.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php

declare(strict_types=1);

namespace Cycle\Annotated\Tests\Fixtures\Fixtures27;

use Cycle\Annotated\Annotation\Entity;
use Cycle\Annotated\Annotation\Inheritance\SingleTable;

#[Entity]
#[SingleTable]
class SoSti extends SoParent {}
Loading
Loading