Skip to content

JTI: updating a child entity dispatches OnCreate (never OnUpdate) to the parent role's listeners #42

Open

Description

@roxblnfk

Description

With Joined Table Inheritance, when a loaded child entity is updated, EventDrivenCommandGenerator dispatches OnCreate to the parent role's listeners instead of OnUpdate. As a result, parent-level OnUpdate hooks never fire, and #[CreatedAt] on the parent overwrites created_at on every update of the child.

Reproduction

Runs on in-memory SQLite, ORM built with new EventDrivenCommandGenerator($schema, $container).

#[Entity(role: 'person', table: 'persons')]
#[CreatedAt(field: 'createdAt')]
#[Hook(callable: [Person::class, 'hook'], events: [OnCreate::class, OnUpdate::class])]
class Person
{
    #[Column(type: 'primary')]
    public int $id;
    #[Column(type: 'string')]
    public string $name = 'n';
    public \DateTimeImmutable $createdAt;

    public static array $log = [];
    public static function hook(OnCreate|OnUpdate $e): void
    {
        self::$log[] = (new \ReflectionClass($e))->getShortName() . '(role=' . $e->role . ')';
    }
}

#[Entity(role: 'employee', table: 'employees')]
#[JoinedTable]
class Employee extends Person
{
    #[Column(type: 'string')]
    public string $department = 'd';
}

$e = new Employee();
(new EntityManager($orm))->persist($e)->run();
echo 'insert: ', implode(', ', Person::$log), "\n";

$db->update('persons', ['createdAt' => '2000-01-01 00:00:00'], ['id' => $e->id])->run();
$orm->getHeap()->clean();
Person::$log = [];

$e = $orm->getRepository(Employee::class)->findByPK(1);
echo 'loaded createdAt: ', $e->createdAt->format('Y-m-d'), "\n";
$e->name = 'changed';
$e->department = 'changed';
(new EntityManager($orm))->persist($e)->run();
echo 'update: ', implode(', ', Person::$log), "\n";
echo 'createdAt in DB after update: ', $db->select('createdAt')->from('persons')->run()->fetchColumn(), "\n";

Expected

update: OnUpdate(role=person)
createdAt in DB after update: 2000-01-01 00:00:00

Actual

insert: OnCreate(role=person)
loaded createdAt: 2000-01-01
update: OnCreate(role=person)
createdAt in DB after update: 2026-09-29 13:24:31

Cause

src/EventDrivenCommandGenerator.php, generateParentStoreCommand() (lines 73-92): the event is always new OnCreate($parentRole, ...) (line 83), even when $isNew === false and the command is $mapper->queueUpdate(...). It should pick OnCreate/OnUpdate based on $isNew, as storeEntity() already does.

Versions

  • cycle/entity-behavior 1.7.1
  • cycle/orm v2.18.0, cycle/annotated v4.6.0, cycle/schema-builder 2.12.1
  • PHP 8.4.23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions