cycle / orm

PHP DataMapper, ORM
https://cycle-orm.dev
MIT License
1.23k stars 72 forks source link

🐛 Issue with Single Table Inheritance (STI) and Type Casting in Child Entities #440

Open butschster opened 1 year ago

butschster commented 1 year ago

No duplicates 🥲.

What happened?

I've encountered an issue regarding Single Table Inheritance (STI) when working with CycleORM. Specifically, the problem arises with type casting in child entities. The ORM seems to be utilizing the parent entity's type cast rules instead of the child's, which is leading to incorrect behavior.

Here's a simplified example to demonstrate the issue:

// Parent entity
#[Entity]
class Asset {
    #[Column(type: 'jsonb', nullable: true, typecast: Data::class)]
    public ?object $data = null;
}

// Child entity
#[Entity]
#[SingleTable(value: 'domain')]
class Domain extends Asset {
    #[Column(type: 'jsonb', nullable: true, typecast: DomainData::class)]
    public ?object $data = null;
}

In this setup, whenever I attempt to fetch a Domain entity from the database, the data property is always being cast using the Data class from the parent Asset entity, ignoring the specified DomainData class in the child entity.

The root of the problem seems to lie in the Cycle\ORM\Service\Implementation\EntityFactory::make method:

blic function make(
    string $role,
    array $data = [],
    int $status = Node::NEW,
    bool $typecast = false
): object {
    // ... snip ...
    $role = $data[LoaderInterface::ROLE_KEY] ?? $role;
    // ... snip ...
    $rRole = $this->resolveRole($role);
    $mapper = $this->mapperProvider->getMapper($rRole);
    $castedData = $typecast ? $mapper->cast($data) : $data;
    // ... snip ...
    $e = $mapper->init($data, $role);
    return $mapper->hydrate($e, $relMap->init($this, $node, $castedData));
}

As seen in the code above, it resolves the role to asset initially, then proceeds to cast the data using this resolved role, which in turn picks up the typecast rules from the Asset class instead of the Domain class.

Moreover, it appears that the ORM schema does not accumulate type cast rules for child entities. Here

I propose a modified approach for the make method to handle STI with type casting correctly, somewhat along the following lines:

public function make(
    string $role,
    array $data = [],
    int $status = Node::NEW,
    bool $typecast = false
): object {
    // 1. Resolve parent role [asset]
    // 2. Find the proper child role [asset_domain] using the discriminator
    // 3. Retrieve the schema with type casters for the child [asset_domain] role
    // 4. Cast data using the proper child [asset_domain] role based on the discriminator
    // 5. Create the child [Domain] object
    // 6. Hydrate the casted data into the child object
}

This modified approach ensures that the correct child role is utilized when casting data, hence respecting the type cast rules defined in the child entities.

wolfy-j commented 9 months ago

Valid usecase.