Skip to content

Commit 078fa0c

Browse files
committed
feat: support casting values to enums
fix phpstan errors more validation and testing feat: support casting values to enums
1 parent d945236 commit 078fa0c

File tree

23 files changed

+969
-115
lines changed

23 files changed

+969
-115
lines changed
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* This file is part of CodeIgniter 4 framework.
7+
*
8+
* (c) CodeIgniter Foundation <[email protected]>
9+
*
10+
* For the full copyright and license information, please view
11+
* the LICENSE file that was distributed with this source code.
12+
*/
13+
14+
namespace CodeIgniter\DataCaster\Cast;
15+
16+
use BackedEnum;
17+
use CodeIgniter\DataCaster\Exceptions\CastException;
18+
use ReflectionEnum;
19+
use UnitEnum;
20+
21+
/**
22+
* Class EnumCast
23+
*
24+
* Handles casting for PHP enums (both backed and unit enums)
25+
*
26+
* (PHP) [enum --> value/name] --> (DB driver) --> (DB column) int|string
27+
* [ <-- value/name] <-- (DB driver) <-- (DB column) int|string
28+
*/
29+
class EnumCast extends BaseCast implements CastInterface
30+
{
31+
/**
32+
* @param array<int, string> $params
33+
*/
34+
public static function get(
35+
mixed $value,
36+
array $params = [],
37+
?object $helper = null,
38+
): mixed {
39+
if (! is_string($value) && ! is_int($value)) {
40+
self::invalidTypeValueError($value);
41+
}
42+
43+
$enumClass = $params[0] ?? null;
44+
45+
if ($enumClass === null) {
46+
throw CastException::forMissingEnumClass();
47+
}
48+
49+
if (! enum_exists($enumClass)) {
50+
throw CastException::forNotEnum($enumClass);
51+
}
52+
53+
$reflection = new ReflectionEnum($enumClass);
54+
55+
// Unit enum
56+
if (! $reflection->isBacked()) {
57+
// Unit enum - match by name
58+
foreach ($enumClass::cases() as $case) {
59+
if ($case->name === $value) {
60+
return $case;
61+
}
62+
}
63+
64+
throw CastException::forInvalidEnumCaseName($enumClass, $value);
65+
}
66+
67+
// Backed enum - validate and cast the value to proper type
68+
$backingType = $reflection->getBackingType();
69+
70+
// Cast to proper type (int or string)
71+
if ($backingType->getName() === 'int') {
72+
$value = (int) $value;
73+
} elseif ($backingType->getName() === 'string') {
74+
$value = (string) $value;
75+
}
76+
77+
$enum = $enumClass::tryFrom($value);
78+
79+
if ($enum === null) {
80+
throw CastException::forInvalidEnumValue($enumClass, $value);
81+
}
82+
83+
return $enum;
84+
}
85+
86+
/**
87+
* @param array<int, string> $params
88+
*/
89+
public static function set(
90+
mixed $value,
91+
array $params = [],
92+
?object $helper = null,
93+
): int|string {
94+
if (! is_object($value) || ! enum_exists($value::class)) {
95+
self::invalidTypeValueError($value);
96+
}
97+
98+
// Get the expected enum class
99+
$enumClass = $params[0] ?? null;
100+
101+
if ($enumClass === null) {
102+
throw CastException::forMissingEnumClass();
103+
}
104+
105+
if (! enum_exists($enumClass)) {
106+
throw CastException::forNotEnum($enumClass);
107+
}
108+
109+
// Validate that the enum is of the expected type
110+
if (! $value instanceof $enumClass) {
111+
throw CastException::forInvalidEnumType($enumClass, $value::class);
112+
}
113+
114+
$reflection = new ReflectionEnum($value::class);
115+
116+
// Backed enum - return the properly typed backing value
117+
if ($reflection->isBacked()) {
118+
/** @var BackedEnum $value */
119+
return $value->value;
120+
}
121+
122+
// Unit enum - return the case name
123+
/** @var UnitEnum $value */
124+
return $value->name;
125+
}
126+
}

system/DataCaster/DataCaster.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
use CodeIgniter\DataCaster\Cast\CastInterface;
1919
use CodeIgniter\DataCaster\Cast\CSVCast;
2020
use CodeIgniter\DataCaster\Cast\DatetimeCast;
21+
use CodeIgniter\DataCaster\Cast\EnumCast;
2122
use CodeIgniter\DataCaster\Cast\FloatCast;
2223
use CodeIgniter\DataCaster\Cast\IntBoolCast;
2324
use CodeIgniter\DataCaster\Cast\IntegerCast;
@@ -48,6 +49,7 @@ final class DataCaster
4849
'boolean' => BooleanCast::class,
4950
'csv' => CSVCast::class,
5051
'datetime' => DatetimeCast::class,
52+
'enum' => EnumCast::class,
5153
'double' => FloatCast::class,
5254
'float' => FloatCast::class,
5355
'int' => IntegerCast::class,

system/Entity/Cast/BaseCast.php

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -18,27 +18,11 @@
1818
*/
1919
abstract class BaseCast implements CastInterface
2020
{
21-
/**
22-
* Get
23-
*
24-
* @param array|bool|float|int|object|string|null $value Data
25-
* @param array $params Additional param
26-
*
27-
* @return array|bool|float|int|object|string|null
28-
*/
2921
public static function get($value, array $params = [])
3022
{
3123
return $value;
3224
}
3325

34-
/**
35-
* Set
36-
*
37-
* @param array|bool|float|int|object|string|null $value Data
38-
* @param array $params Additional param
39-
*
40-
* @return array|bool|float|int|object|string|null
41-
*/
4226
public static function set($value, array $params = [])
4327
{
4428
return $value;

system/Entity/Cast/CastInterface.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ interface CastInterface
2626
* Takes a raw value from Entity, returns its value for PHP.
2727
*
2828
* @param array|bool|float|int|object|string|null $value Data
29-
* @param array $params Additional param
29+
* @param array<int, string> $params Additional param
3030
*
3131
* @return array|bool|float|int|object|string|null
3232
*/
@@ -36,7 +36,7 @@ public static function get($value, array $params = []);
3636
* Takes a PHP value, returns its raw value for Entity.
3737
*
3838
* @param array|bool|float|int|object|string|null $value Data
39-
* @param array $params Additional param
39+
* @param array<int, string> $params Additional param
4040
*
4141
* @return array|bool|float|int|object|string|null
4242
*/

system/Entity/Cast/EnumCast.php

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* This file is part of CodeIgniter 4 framework.
7+
*
8+
* (c) CodeIgniter Foundation <[email protected]>
9+
*
10+
* For the full copyright and license information, please view
11+
* the LICENSE file that was distributed with this source code.
12+
*/
13+
14+
namespace CodeIgniter\Entity\Cast;
15+
16+
use BackedEnum;
17+
use CodeIgniter\Entity\Exceptions\CastException;
18+
use ReflectionEnum;
19+
use UnitEnum;
20+
21+
/**
22+
* Class EnumCast
23+
*
24+
* Handles casting for PHP enums (both backed and unit enums)
25+
*/
26+
class EnumCast extends BaseCast
27+
{
28+
/**
29+
* {@inheritDoc}
30+
*/
31+
public static function get($value, array $params = [])
32+
{
33+
$enumClass = $params[0] ?? null;
34+
35+
if ($enumClass === null) {
36+
throw CastException::forMissingEnumClass();
37+
}
38+
39+
if (! enum_exists($enumClass)) {
40+
throw CastException::forNotEnum($enumClass);
41+
}
42+
43+
$reflection = new ReflectionEnum($enumClass);
44+
45+
// Backed enum - validate and cast the value to proper type
46+
if ($reflection->isBacked()) {
47+
$backingType = $reflection->getBackingType();
48+
49+
// Cast to proper type (int or string)
50+
if ($backingType->getName() === 'int') {
51+
$value = (int) $value;
52+
} elseif ($backingType->getName() === 'string') {
53+
$value = (string) $value;
54+
}
55+
56+
$enum = $enumClass::tryFrom($value);
57+
58+
if ($enum === null) {
59+
throw CastException::forInvalidEnumValue($enumClass, $value);
60+
}
61+
62+
return $enum;
63+
}
64+
65+
// Unit enum - match by name
66+
foreach ($enumClass::cases() as $case) {
67+
if ($case->name === $value) {
68+
return $case;
69+
}
70+
}
71+
72+
throw CastException::forInvalidEnumCaseName($enumClass, $value);
73+
}
74+
75+
/**
76+
* {@inheritDoc}
77+
*/
78+
public static function set($value, array $params = []): int|string|null
79+
{
80+
// Get the expected enum class
81+
$enumClass = $params[0] ?? null;
82+
83+
if ($enumClass === null) {
84+
throw CastException::forMissingEnumClass();
85+
}
86+
87+
if (! enum_exists($enumClass)) {
88+
throw CastException::forNotEnum($enumClass);
89+
}
90+
91+
// If it's already an enum object, validate and extract its value
92+
if (is_object($value) && enum_exists($value::class)) {
93+
// Validate that the enum is of the expected type
94+
if (! $value instanceof $enumClass) {
95+
throw CastException::forInvalidEnumType($enumClass, $value::class);
96+
}
97+
98+
$reflection = new ReflectionEnum($value::class);
99+
100+
// Backed enum - return the properly typed backing value
101+
if ($reflection->isBacked()) {
102+
/** @var BackedEnum $value */
103+
return $value->value;
104+
}
105+
106+
// Unit enum - return the case name
107+
/** @var UnitEnum $value */
108+
return $value->name;
109+
}
110+
111+
$reflection = new ReflectionEnum($enumClass);
112+
113+
// Validate backed enum values
114+
if ($reflection->isBacked()) {
115+
$backingType = $reflection->getBackingType();
116+
117+
// Cast to proper type (int or string)
118+
if ($backingType->getName() === 'int') {
119+
$value = (int) $value;
120+
} elseif ($backingType->getName() === 'string') {
121+
$value = (string) $value;
122+
}
123+
124+
if ($enumClass::tryFrom($value) === null) {
125+
throw CastException::forInvalidEnumValue($enumClass, $value);
126+
}
127+
128+
return $value;
129+
}
130+
131+
// Validate unit enum case names - must be a string
132+
$value = (string) $value;
133+
134+
foreach ($enumClass::cases() as $case) {
135+
if ($case->name === $value) {
136+
return $value;
137+
}
138+
}
139+
140+
throw CastException::forInvalidEnumCaseName($enumClass, $value);
141+
}
142+
}

system/Entity/Entity.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
use CodeIgniter\Entity\Cast\BooleanCast;
1919
use CodeIgniter\Entity\Cast\CSVCast;
2020
use CodeIgniter\Entity\Cast\DatetimeCast;
21+
use CodeIgniter\Entity\Cast\EnumCast;
2122
use CodeIgniter\Entity\Cast\FloatCast;
2223
use CodeIgniter\Entity\Cast\IntBoolCast;
2324
use CodeIgniter\Entity\Cast\IntegerCast;
@@ -92,6 +93,7 @@ class Entity implements JsonSerializable
9293
'csv' => CSVCast::class,
9394
'datetime' => DatetimeCast::class,
9495
'double' => FloatCast::class,
96+
'enum' => EnumCast::class,
9597
'float' => FloatCast::class,
9698
'int' => IntegerCast::class,
9799
'integer' => IntegerCast::class,

0 commit comments

Comments
 (0)