Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
* scalarization branch in `Mapper::fetchFields`.
Comment thread
roxblnfk marked this conversation as resolved.
Outdated
*/
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 only thing that scalarizes the enum from $entity->type is the fetchFields() branch.
Comment thread
roxblnfk marked this conversation as resolved.
Outdated
$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