Skip to content

Commit 37ff9fa

Browse files
authored
feat: add enum casting (#9752)
* feat: support casting values to enums fix phpstan errors more validation and testing feat: support casting values to enums * apply code suggestions * apply code suggestions
1 parent d945236 commit 37ff9fa

File tree

24 files changed

+954
-123
lines changed

24 files changed

+954
-123
lines changed

system/DataCaster/Cast/CastInterface.php

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,9 @@ interface CastInterface
1818
/**
1919
* Takes a value from DataSource, returns its value for PHP.
2020
*
21-
* @param mixed $value Data from database driver
22-
* @param list<string> $params Additional param
23-
* @param object|null $helper Helper object. E.g., database connection
21+
* @param mixed $value Data from database driver
22+
* @param array<int, string> $params Additional param
23+
* @param object|null $helper Helper object. E.g., database connection
2424
*
2525
* @return mixed PHP native value
2626
*/
@@ -33,9 +33,9 @@ public static function get(
3333
/**
3434
* Takes a PHP value, returns its value for DataSource.
3535
*
36-
* @param mixed $value PHP native value
37-
* @param list<string> $params Additional param
38-
* @param object|null $helper Helper object. E.g., database connection
36+
* @param mixed $value PHP native value
37+
* @param array<int, string> $params Additional param
38+
* @param object|null $helper Helper object. E.g., database connection
3939
*
4040
* @return mixed Data to pass to database driver
4141
*/
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* This file is part of CodeIgniter 4 framework.
7+
*
8+
* (c) CodeIgniter Foundation <admin@codeigniter.com>
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+
public static function get(
32+
mixed $value,
33+
array $params = [],
34+
?object $helper = null,
35+
): BackedEnum|UnitEnum {
36+
if (! is_string($value) && ! is_int($value)) {
37+
self::invalidTypeValueError($value);
38+
}
39+
40+
$enumClass = $params[0] ?? null;
41+
42+
if ($enumClass === null) {
43+
throw CastException::forMissingEnumClass();
44+
}
45+
46+
if (! enum_exists($enumClass)) {
47+
throw CastException::forNotEnum($enumClass);
48+
}
49+
50+
$reflection = new ReflectionEnum($enumClass);
51+
52+
// Unit enum
53+
if (! $reflection->isBacked()) {
54+
// Unit enum - match by name
55+
foreach ($enumClass::cases() as $case) {
56+
if ($case->name === $value) {
57+
return $case;
58+
}
59+
}
60+
61+
throw CastException::forInvalidEnumCaseName($enumClass, $value);
62+
}
63+
64+
// Backed enum - validate and cast the value to proper type
65+
$backingType = $reflection->getBackingType();
66+
67+
// Cast to proper type (int or string)
68+
if ($backingType->getName() === 'int') {
69+
$value = (int) $value;
70+
} elseif ($backingType->getName() === 'string') {
71+
$value = (string) $value;
72+
}
73+
74+
$enum = $enumClass::tryFrom($value);
75+
76+
if ($enum === null) {
77+
throw CastException::forInvalidEnumValue($enumClass, $value);
78+
}
79+
80+
return $enum;
81+
}
82+
83+
public static function set(
84+
mixed $value,
85+
array $params = [],
86+
?object $helper = null,
87+
): int|string {
88+
if (! is_object($value) || ! enum_exists($value::class)) {
89+
self::invalidTypeValueError($value);
90+
}
91+
92+
// Get the expected enum class
93+
$enumClass = $params[0] ?? null;
94+
95+
if ($enumClass === null) {
96+
throw CastException::forMissingEnumClass();
97+
}
98+
99+
if (! enum_exists($enumClass)) {
100+
throw CastException::forNotEnum($enumClass);
101+
}
102+
103+
// Validate that the enum is of the expected type
104+
if (! $value instanceof $enumClass) {
105+
throw CastException::forInvalidEnumType($enumClass, $value::class);
106+
}
107+
108+
$reflection = new ReflectionEnum($value::class);
109+
110+
// Backed enum - return the properly typed backing value
111+
if ($reflection->isBacked()) {
112+
/** @var BackedEnum $value */
113+
return $value->value;
114+
}
115+
116+
// Unit enum - return the case name
117+
/** @var UnitEnum $value */
118+
return $value->name;
119+
}
120+
}

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: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* This file is part of CodeIgniter 4 framework.
7+
*
8+
* (c) CodeIgniter Foundation <admin@codeigniter.com>
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+
class EnumCast extends BaseCast
22+
{
23+
public static function get($value, array $params = []): BackedEnum|UnitEnum
24+
{
25+
$enumClass = $params[0] ?? null;
26+
27+
if ($enumClass === null) {
28+
throw CastException::forMissingEnumClass();
29+
}
30+
31+
if (! enum_exists($enumClass)) {
32+
throw CastException::forNotEnum($enumClass);
33+
}
34+
35+
$reflection = new ReflectionEnum($enumClass);
36+
37+
// Backed enum - validate and cast the value to proper type
38+
if ($reflection->isBacked()) {
39+
$backingType = $reflection->getBackingType();
40+
41+
// Cast to proper type (int or string)
42+
if ($backingType->getName() === 'int') {
43+
$value = (int) $value;
44+
} elseif ($backingType->getName() === 'string') {
45+
$value = (string) $value;
46+
}
47+
48+
$enum = $enumClass::tryFrom($value);
49+
50+
if ($enum === null) {
51+
throw CastException::forInvalidEnumValue($enumClass, $value);
52+
}
53+
54+
return $enum;
55+
}
56+
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+
public static function set($value, array $params = []): int|string
68+
{
69+
// Get the expected enum class
70+
$enumClass = $params[0] ?? null;
71+
72+
if ($enumClass === null) {
73+
throw CastException::forMissingEnumClass();
74+
}
75+
76+
if (! enum_exists($enumClass)) {
77+
throw CastException::forNotEnum($enumClass);
78+
}
79+
80+
// If it's already an enum object, validate and extract its value
81+
if (is_object($value) && enum_exists($value::class)) {
82+
// Validate that the enum is of the expected type
83+
if (! $value instanceof $enumClass) {
84+
throw CastException::forInvalidEnumType($enumClass, $value::class);
85+
}
86+
87+
$reflection = new ReflectionEnum($value::class);
88+
89+
// Backed enum - return the properly typed backing value
90+
if ($reflection->isBacked()) {
91+
/** @var BackedEnum $value */
92+
return $value->value;
93+
}
94+
95+
// Unit enum - return the case name
96+
/** @var UnitEnum $value */
97+
return $value->name;
98+
}
99+
100+
$reflection = new ReflectionEnum($enumClass);
101+
102+
// Validate backed enum values
103+
if ($reflection->isBacked()) {
104+
$backingType = $reflection->getBackingType();
105+
106+
// Cast to proper type (int or string)
107+
if ($backingType->getName() === 'int') {
108+
$value = (int) $value;
109+
} elseif ($backingType->getName() === 'string') {
110+
$value = (string) $value;
111+
}
112+
113+
if ($enumClass::tryFrom($value) === null) {
114+
throw CastException::forInvalidEnumValue($enumClass, $value);
115+
}
116+
117+
return $value;
118+
}
119+
120+
// Validate unit enum case names - must be a string
121+
$value = (string) $value;
122+
123+
foreach ($enumClass::cases() as $case) {
124+
if ($case->name === $value) {
125+
return $value;
126+
}
127+
}
128+
129+
throw CastException::forInvalidEnumCaseName($enumClass, $value);
130+
}
131+
}

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)