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
97 changes: 97 additions & 0 deletions src/Behavior/EnumCast.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
<?php

namespace ipl\Orm\Behavior;

use BackedEnum;
use InvalidArgumentException;
use ipl\Orm\Contract\PropertyBehavior;
use ipl\Orm\Exception\ValueConversionException;
use ValueError;

/**
* Convert backed-enum database values to and from PHP enum instances
*
* The enum's backing value (string|int) is stored in the database. On retrieval the raw scalar
* is converted to the corresponding enum case via {@see BackedEnum::from()}; on persistence the
* enum instance is unwrapped back to its backing value. Null is preserved as null in both directions.
*
* ```php
* $behaviors->add(new EnumCast(Status::class, ['status']));
* ```
Comment thread
sukhwinder33445 marked this conversation as resolved.
*/
class EnumCast extends PropertyBehavior
Comment thread
sukhwinder33445 marked this conversation as resolved.
{
/**
* The fully qualified class name of the target-backed enum
*
* @var class-string<BackedEnum>
*/
protected string $enum;

/**
* @param class-string<BackedEnum> $enum Fully-qualified class name of the target backed enum
* @param array<mixed> $properties Property names to process, as values
*/
public function __construct(string $enum, array $properties)
{
if (! is_subclass_of($enum, BackedEnum::class)) {
throw new InvalidArgumentException("$enum must be a BackedEnum");
}

$this->enum = $enum;

parent::__construct($properties);
}

/**
* @param int|string|null $value
*
* @return ?BackedEnum
*
* @throws ValueConversionException
*/
public function fromDb($value, $key, $_)
{
if ($value === null || $value instanceof $this->enum) {

Check failure on line 55 in src/Behavior/EnumCast.php

View workflow job for this annotation

GitHub Actions / PHP / Static analysis (8.5) / PHPStan 8.5

Instanceof between int|string and class-string<BackedEnum> will always evaluate to false.

Check failure on line 55 in src/Behavior/EnumCast.php

View workflow job for this annotation

GitHub Actions / PHP / Static analysis (8.3) / PHPStan 8.3

Instanceof between int|string and class-string<BackedEnum> will always evaluate to false.

Check failure on line 55 in src/Behavior/EnumCast.php

View workflow job for this annotation

GitHub Actions / PHP / Static analysis (8.2) / PHPStan 8.2

Instanceof between int|string and class-string<BackedEnum> will always evaluate to false.

Check failure on line 55 in src/Behavior/EnumCast.php

View workflow job for this annotation

GitHub Actions / PHP / Static analysis (8.4) / PHPStan 8.4

Instanceof between int|string and class-string<BackedEnum> will always evaluate to false.
return $value;
}

try {
return $this->enum::from($value);
} catch (ValueError $e) {
throw new ValueConversionException(sprintf(
"%s (key: '%s')",
$e->getMessage(),
$key
));
}
Comment thread
sukhwinder33445 marked this conversation as resolved.
}

/**
* @param BackedEnum|int|string|null $value
*
* @return int|string|null
*
* @throws ValueConversionException
*/
public function toDb($value, $key, $_)
{
if ($value === null) {
return null;
}

if ($value instanceof $this->enum) {
return $value->value;
}

try {
return $this->enum::from($value)->value;

Check failure on line 88 in src/Behavior/EnumCast.php

View workflow job for this annotation

GitHub Actions / PHP / Static analysis (8.5) / PHPStan 8.5

Parameter #1 $value of static method BackedEnum::from() expects int|string, BackedEnum|int|string given.

Check failure on line 88 in src/Behavior/EnumCast.php

View workflow job for this annotation

GitHub Actions / PHP / Static analysis (8.3) / PHPStan 8.3

Parameter #1 $value of static method BackedEnum::from() expects int|string, BackedEnum|int|string given.

Check failure on line 88 in src/Behavior/EnumCast.php

View workflow job for this annotation

GitHub Actions / PHP / Static analysis (8.2) / PHPStan 8.2

Parameter #1 $value of static method BackedEnum::from() expects int|string, BackedEnum|int|string given.

Check failure on line 88 in src/Behavior/EnumCast.php

View workflow job for this annotation

GitHub Actions / PHP / Static analysis (8.4) / PHPStan 8.4

Parameter #1 $value of static method BackedEnum::from() expects int|string, BackedEnum|int|string given.
} catch (ValueError $e) {
throw new ValueConversionException(sprintf(
"%s (key: '%s')",
$e->getMessage(),
$key
));
}
Comment thread
sukhwinder33445 marked this conversation as resolved.
}
}
109 changes: 109 additions & 0 deletions tests/Behavior/EnumCastTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
<?php

namespace ipl\Tests\Orm;

use InvalidArgumentException;
use ipl\Orm\Behavior\EnumCast;
use ipl\Orm\Exception\ValueConversionException;
use stdClass;
use PHPUnit\Framework\TestCase;
use TypeError;

// phpcs:disable PSR1.Classes.ClassDeclaration.MultipleClasses
enum Color: string
{
case Red = 'red';
case Green = 'green';
case Blue = 'blue';
}

enum Priority: int
{
case Low = 1;
case High = 2;
}

class EnumCastTest extends TestCase
{
private EnumCast $behavior;

protected function setUp(): void
{
$this->behavior = new EnumCast(Color::class, ['color']);
}

public function testConstructorThrowsForNonBackedEnumTypes(): void
{
$this->expectException(InvalidArgumentException::class);

new EnumCast(stdClass::class, ['col']);
}

public function testFromDbConvertsScalarToEnumCase(): void
{
$this->assertSame(Color::Red, $this->behavior->fromDb('red', 'color', null));
}

public function testFromDbPreservesNull(): void
{
$this->assertNull($this->behavior->fromDb(null, 'color', null));
}

public function testToDbUnwrapsEnumCaseToBackingValue(): void
{
$this->assertSame('red', $this->behavior->toDb(Color::Red, 'color', null));
}

public function testToDbPassesThroughValidBackingValue(): void
{
$this->assertSame('red', $this->behavior->toDb('red', 'color', null));
}

public function testToDbThrowsForInvalidBackingValue(): void
{
$this->expectException(ValueConversionException::class);

$this->behavior->toDb('invalid', 'color', null);
}

public function testToDbThrowsForIncompatibleEnumInstance(): void
{
$this->expectException(TypeError::class);
$this->behavior->toDb(Priority::Low, 'color', null);
}

public function testToDbPassesThroughNull(): void
{
$this->assertNull($this->behavior->toDb(null, 'color', null));
}

public function testFromDbThrowsForUnknownBackingValue(): void
{
$this->expectException(ValueConversionException::class);

$this->behavior->fromDb('purple', 'color', null);
}

public function testFromDbIsIdempotentWhenValueIsAlreadyAnEnumInstance(): void
{
$this->assertSame(Color::Red, $this->behavior->fromDb(Color::Red, 'color', null));
}

public function testToDbIsIdempotentWhenValueIsAlreadyABackingScalar(): void
{
$scalar = $this->behavior->toDb(Color::Red, 'color', null);
$this->assertSame($scalar, $this->behavior->toDb($scalar, 'color', null));
}

public function testFromDbConvertsIntBackedEnumCase(): void
{
$behavior = new EnumCast(Priority::class, ['priority']);
$this->assertSame(Priority::Low, $behavior->fromDb(1, 'priority', null));
}

public function testToDbUnwrapsIntBackedEnumCaseToBackingValue(): void
{
$behavior = new EnumCast(Priority::class, ['priority']);
$this->assertSame(1, $behavior->toDb(Priority::Low, 'priority', null));
}
}
Loading