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
6 changes: 5 additions & 1 deletion src/Mapper/Traits/SingleTableTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,11 @@ protected function resolveClass(array $data, ?string $role = null): string
}
$class = $this->entity;
if ($this->children !== [] && isset($data[$this->discriminator])) {
$class = $this->children[$data[$this->discriminator]] ?? $this->entity;
$key = $data[$this->discriminator];
if ($key instanceof \BackedEnum) {
$key = $key->value;
}
$class = $this->children[$key] ?? $this->entity;
}

return $class;
Expand Down
4 changes: 4 additions & 0 deletions src/Parser/Typecast.php
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,10 @@ public function setRules(array $rules): array
? $rule::tryFrom((int) $value)
: null;

$this->uncasters[$key] = static fn(mixed $value): mixed => $value instanceof \BackedEnum
? $value->value
: $value;

unset($rules[$key]);
continue;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

declare(strict_types=1);

namespace Cycle\ORM\Tests\Functional\Driver\Common\Inheritance\Fixture;

class BossWithKind extends WorkerWithKind
{
public ?int $level = null;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

declare(strict_types=1);

namespace Cycle\ORM\Tests\Functional\Driver\Common\Inheritance\Fixture;

enum EmployeeKind: int
{
case Employee = 1;
case Manager = 2;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

declare(strict_types=1);

namespace Cycle\ORM\Tests\Functional\Driver\Common\Inheritance\Fixture;

enum EmployeeType: string
{
case Employee = 'employee';
case Manager = 'manager';
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

declare(strict_types=1);

namespace Cycle\ORM\Tests\Functional\Driver\Common\Inheritance\Fixture;

class WorkerWithKind extends Human
{
public ?EmployeeType $type = null;
public ?string $name = null;
public ?string $email = null;
public ?int $age = 0;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
<?php

declare(strict_types=1);

namespace Cycle\ORM\Tests\Functional\Driver\Common\Inheritance\STI;

use Cycle\ORM\Heap\Heap;
use Cycle\ORM\Mapper\Mapper;
use Cycle\ORM\Schema;
use Cycle\ORM\SchemaInterface;
use Cycle\ORM\Select;
use Cycle\ORM\Tests\Functional\Driver\Common\Inheritance\Fixture\Employee;
use Cycle\ORM\Tests\Functional\Driver\Common\Inheritance\Fixture\EmployeeType;
use Cycle\ORM\Tests\Functional\Driver\Common\Inheritance\Fixture\Manager;

abstract class EnumDiscriminatorTest extends StiBaseTest
{
protected const BASE_ROLE = 'employee';
protected const MANAGER_ROLE = 'manager';

protected static string $discriminator = 'discriminator_value';

public function testChildClassResolvedFromScalarInDatabase(): void
{
$selector = new Select($this->orm, Employee::class);
[$first, $second] = $selector->orderBy('id')->fetchAll();

$this->assertInstanceOf(Manager::class, $first);
$this->assertInstanceOf(Employee::class, $second);
$this->assertNotInstanceOf(Manager::class, $second);
}

public function testFetchedDataExposesEnumCaseAtDiscriminatorColumn(): void
{
$rows = (new Select($this->orm, Employee::class))->orderBy('id')->fetchData();

$this->assertSame(EmployeeType::Manager, $rows[0]['_type']);
$this->assertSame(EmployeeType::Employee, $rows[1]['_type']);
}

public function testPersistedChildWritesScalarToDatabase(): void
{
$manager = new Manager();
$manager->name = 'Manager';
$manager->email = 'admin@email.com';
$manager->age = 69;

$this->save($manager);

$row = $this->getDatabase()
->table('employee_table')
->select()
->where('id', $manager->id)
->fetchAll()[0];

$this->assertSame('manager', $row[static::$discriminator]);
}

public function testRoundTripChildEntity(): void
{
$manager = new Manager();
$manager->name = 'Manager';
$manager->email = 'admin@email.com';
$manager->age = 69;

$this->save($manager);

$loaded = (new Select($this->orm->withHeap(new Heap()), Employee::class))
->wherePK($manager->id)
->fetchOne();

$this->assertInstanceOf(Manager::class, $loaded);
}

public function testMakeResolvesChildClassFromEnumInputData(): void
{
// Defensive: explicit BackedEnum passed as discriminator value should still resolve.
$entity = $this->orm->make(static::BASE_ROLE, [
'_type' => EmployeeType::Manager,
'name' => 'Senya',
'email' => 'sene4ka@hamster.me',
'age' => 12,
]);

$this->assertInstanceOf(Manager::class, $entity);
}

public function testNoExtraWritesAfterLoadAndResave(): void
{
/** @var Manager $manager */
$manager = (new Select($this->orm, Employee::class))->orderBy('id')->fetchOne();
$this->assertInstanceOf(Manager::class, $manager);

$this->captureWriteQueries();
$this->save($manager);
$this->assertNumWrites(0);
}

public function testUpdateChildEntityKeepsScalarDiscriminatorInDatabase(): void
{
/** @var Manager $manager */
$manager = (new Select($this->orm, Employee::class))->orderBy('id')->fetchOne();
$this->assertInstanceOf(Manager::class, $manager);

$manager->name = 'Renamed';
$this->save($manager);

$row = $this->getDatabase()
->table('employee_table')
->select()
->where('id', $manager->id)
->fetchAll()[0];

$this->assertSame('manager', $row[static::$discriminator]);
$this->assertSame('Renamed', $row['name']);
}

public function setUp(): void
{
parent::setUp();

$this->makeTable('employee_table', [
static::$discriminator => 'string,nullable',
'id' => 'primary',
'name' => 'string',
'email' => 'string',
'age' => 'int',
]);

$this->getDatabase()->table('employee_table')->insertMultiple(
[static::$discriminator, 'name', 'email', 'age'],
[
['_type' => 'manager', 'name' => 'John', 'email' => 'captain@black.sea', 'age' => 38],
['_type' => 'employee', 'name' => 'Anton', 'email' => 'antonio@mail.org', 'age' => 35],
],
);

$this->orm = $this->withSchema(new Schema($this->getSchemaArray()));
}

protected function getSchemaArray(): array
{
return [
static::BASE_ROLE => [
SchemaInterface::ENTITY => Employee::class,
SchemaInterface::CHILDREN => [
'manager' => Manager::class,
],
SchemaInterface::MAPPER => Mapper::class,
SchemaInterface::DATABASE => 'default',
SchemaInterface::TABLE => 'employee_table',
SchemaInterface::PRIMARY_KEY => 'id',
SchemaInterface::COLUMNS => ['id', '_type' => static::$discriminator, 'name', 'email', 'age'],
SchemaInterface::TYPECAST => ['id' => 'int', 'age' => 'int', '_type' => EmployeeType::class],
SchemaInterface::SCHEMA => [],
SchemaInterface::RELATIONS => [],
],
self::MANAGER_ROLE => [
SchemaInterface::ENTITY => Manager::class,
],
];
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
<?php

declare(strict_types=1);

namespace Cycle\ORM\Tests\Functional\Driver\Common\Inheritance\STI;

use Cycle\ORM\Heap\Heap;
use Cycle\ORM\Mapper\Mapper;
use Cycle\ORM\Schema;
use Cycle\ORM\SchemaInterface;
use Cycle\ORM\Select;
use Cycle\ORM\Tests\Functional\Driver\Common\Inheritance\Fixture\BossWithKind;
use Cycle\ORM\Tests\Functional\Driver\Common\Inheritance\Fixture\EmployeeType;
use Cycle\ORM\Tests\Functional\Driver\Common\Inheritance\Fixture\WorkerWithKind;

/**
* STI scenario where the entity exposes the discriminator as a BackedEnum-typed property
* (so `extractData` returns an enum case in the discriminator slot). Exercises the
* enum -> scalar conversion performed by the BackedEnum uncaster in {@see \Cycle\ORM\Parser\Typecast::uncast()}.
*/
abstract class EnumPropertyDiscriminatorTest extends StiBaseTest
{
protected const WORKER_ROLE = 'worker';
protected const BOSS_ROLE = 'boss';

public function testInsertChildWritesScalarDespiteEnumOnProperty(): void
{
$boss = new BossWithKind();
$boss->type = EmployeeType::Manager;
$boss->name = 'Boss';
$boss->email = 'boss@corp.example';
$boss->age = 50;
$boss->level = 10;

$this->save($boss);

$row = $this->getDatabase()
->table('worker_table')
->select()
->where('id', $boss->id)
->fetchAll()[0];

$this->assertSame('manager', $row['type']);
}

public function testInsertBaseWithEnumPropertyWritesScalar(): void
{
// Base entity does NOT match any child in CHILDREN -> getDiscriminatorValues() returns [].
// The enum on $entity->type is converted to a scalar by the BackedEnum uncaster in Typecast::uncast().
$worker = new WorkerWithKind();
$worker->type = EmployeeType::Employee;
$worker->name = 'Plain Worker';
$worker->email = 'plain@corp.example';
$worker->age = 30;

$this->save($worker);

$row = $this->getDatabase()
->table('worker_table')
->select()
->where('id', $worker->id)
->fetchAll()[0];

$this->assertSame('employee', $row['type']);
}

public function testRoundTripPreservesEnumOnEntity(): void
{
$boss = new BossWithKind();
$boss->type = EmployeeType::Manager;
$boss->name = 'Boss';
$boss->email = 'boss@corp.example';
$boss->age = 50;
$boss->level = 10;
$this->save($boss);

/** @var BossWithKind $loaded */
$loaded = (new Select($this->orm->withHeap(new Heap()), WorkerWithKind::class))
->wherePK($boss->id)
->fetchOne();

$this->assertInstanceOf(BossWithKind::class, $loaded);
$this->assertSame(EmployeeType::Manager, $loaded->type);
}

public function testNoExtraWritesAfterRoundTrip(): void
{
$boss = new BossWithKind();
$boss->type = EmployeeType::Manager;
$boss->name = 'Boss';
$boss->email = 'boss@corp.example';
$boss->age = 50;
$boss->level = 10;
$this->save($boss);

$this->orm = $this->orm->withHeap(new Heap());

/** @var BossWithKind $loaded */
$loaded = (new Select($this->orm, WorkerWithKind::class))
->wherePK($boss->id)
->fetchOne();

$this->captureWriteQueries();
$this->save($loaded);
$this->assertNumWrites(0);
}

public function setUp(): void
{
parent::setUp();

$this->makeTable('worker_table', [
'type' => 'string,nullable',
'id' => 'primary',
'name' => 'string',
'email' => 'string',
'age' => 'int',
'level' => 'int,nullable',
]);

$this->orm = $this->withSchema(new Schema($this->getSchemaArray()));
}

protected function getSchemaArray(): array
{
return [
self::WORKER_ROLE => [
SchemaInterface::ENTITY => WorkerWithKind::class,
SchemaInterface::CHILDREN => [
'manager' => BossWithKind::class,
],
SchemaInterface::MAPPER => Mapper::class,
SchemaInterface::DATABASE => 'default',
SchemaInterface::TABLE => 'worker_table',
SchemaInterface::PRIMARY_KEY => 'id',
SchemaInterface::DISCRIMINATOR => 'type',
SchemaInterface::COLUMNS => ['id', 'type', 'name', 'email', 'age', 'level'],
SchemaInterface::TYPECAST => [
'id' => 'int',
'age' => 'int',
'level' => 'int',
'type' => EmployeeType::class,
],
SchemaInterface::SCHEMA => [],
SchemaInterface::RELATIONS => [],
],
self::BOSS_ROLE => [
SchemaInterface::ENTITY => BossWithKind::class,
],
];
}
}
Loading
Loading