From 078fa0c9a60250ce770cf60a413d160d57e888ce Mon Sep 17 00:00:00 2001 From: michalsn Date: Thu, 9 Oct 2025 10:23:32 +0200 Subject: [PATCH 1/3] feat: support casting values to enums fix phpstan errors more validation and testing feat: support casting values to enums --- system/DataCaster/Cast/EnumCast.php | 126 +++++++++ system/DataCaster/DataCaster.php | 2 + system/Entity/Cast/BaseCast.php | 16 -- system/Entity/Cast/CastInterface.php | 4 +- system/Entity/Cast/EnumCast.php | 142 ++++++++++ system/Entity/Entity.php | 2 + system/Entity/Exceptions/CastException.php | 50 ++++ system/Language/en/Cast.php | 5 + tests/_support/Enum/ColorEnum.php | 21 ++ tests/_support/Enum/RoleEnum.php | 21 ++ tests/_support/Enum/StatusEnum.php | 21 ++ .../DataConverter/DataConverterTest.php | 256 ++++++++++++++++++ tests/system/Entity/EntityTest.php | 202 ++++++++++++++ user_guide_src/source/changelogs/v4.7.0.rst | 1 + user_guide_src/source/models/entities.rst | 32 ++- user_guide_src/source/models/entities/024.php | 10 + user_guide_src/source/models/entities/025.php | 12 + user_guide_src/source/models/entities/026.php | 17 ++ user_guide_src/source/models/entities/027.php | 12 + user_guide_src/source/models/model.rst | 15 + utils/phpstan-baseline/loader.neon | 2 +- .../missingType.iterableValue.neon | 103 +------ .../staticMethod.notFound.neon | 12 +- 23 files changed, 969 insertions(+), 115 deletions(-) create mode 100644 system/DataCaster/Cast/EnumCast.php create mode 100644 system/Entity/Cast/EnumCast.php create mode 100644 tests/_support/Enum/ColorEnum.php create mode 100644 tests/_support/Enum/RoleEnum.php create mode 100644 tests/_support/Enum/StatusEnum.php create mode 100644 user_guide_src/source/models/entities/024.php create mode 100644 user_guide_src/source/models/entities/025.php create mode 100644 user_guide_src/source/models/entities/026.php create mode 100644 user_guide_src/source/models/entities/027.php diff --git a/system/DataCaster/Cast/EnumCast.php b/system/DataCaster/Cast/EnumCast.php new file mode 100644 index 000000000000..07717a2301eb --- /dev/null +++ b/system/DataCaster/Cast/EnumCast.php @@ -0,0 +1,126 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\DataCaster\Cast; + +use BackedEnum; +use CodeIgniter\DataCaster\Exceptions\CastException; +use ReflectionEnum; +use UnitEnum; + +/** + * Class EnumCast + * + * Handles casting for PHP enums (both backed and unit enums) + * + * (PHP) [enum --> value/name] --> (DB driver) --> (DB column) int|string + * [ <-- value/name] <-- (DB driver) <-- (DB column) int|string + */ +class EnumCast extends BaseCast implements CastInterface +{ + /** + * @param array $params + */ + public static function get( + mixed $value, + array $params = [], + ?object $helper = null, + ): mixed { + if (! is_string($value) && ! is_int($value)) { + self::invalidTypeValueError($value); + } + + $enumClass = $params[0] ?? null; + + if ($enumClass === null) { + throw CastException::forMissingEnumClass(); + } + + if (! enum_exists($enumClass)) { + throw CastException::forNotEnum($enumClass); + } + + $reflection = new ReflectionEnum($enumClass); + + // Unit enum + if (! $reflection->isBacked()) { + // Unit enum - match by name + foreach ($enumClass::cases() as $case) { + if ($case->name === $value) { + return $case; + } + } + + throw CastException::forInvalidEnumCaseName($enumClass, $value); + } + + // Backed enum - validate and cast the value to proper type + $backingType = $reflection->getBackingType(); + + // Cast to proper type (int or string) + if ($backingType->getName() === 'int') { + $value = (int) $value; + } elseif ($backingType->getName() === 'string') { + $value = (string) $value; + } + + $enum = $enumClass::tryFrom($value); + + if ($enum === null) { + throw CastException::forInvalidEnumValue($enumClass, $value); + } + + return $enum; + } + + /** + * @param array $params + */ + public static function set( + mixed $value, + array $params = [], + ?object $helper = null, + ): int|string { + if (! is_object($value) || ! enum_exists($value::class)) { + self::invalidTypeValueError($value); + } + + // Get the expected enum class + $enumClass = $params[0] ?? null; + + if ($enumClass === null) { + throw CastException::forMissingEnumClass(); + } + + if (! enum_exists($enumClass)) { + throw CastException::forNotEnum($enumClass); + } + + // Validate that the enum is of the expected type + if (! $value instanceof $enumClass) { + throw CastException::forInvalidEnumType($enumClass, $value::class); + } + + $reflection = new ReflectionEnum($value::class); + + // Backed enum - return the properly typed backing value + if ($reflection->isBacked()) { + /** @var BackedEnum $value */ + return $value->value; + } + + // Unit enum - return the case name + /** @var UnitEnum $value */ + return $value->name; + } +} diff --git a/system/DataCaster/DataCaster.php b/system/DataCaster/DataCaster.php index 81ebe233c125..3f2604ddc902 100644 --- a/system/DataCaster/DataCaster.php +++ b/system/DataCaster/DataCaster.php @@ -18,6 +18,7 @@ use CodeIgniter\DataCaster\Cast\CastInterface; use CodeIgniter\DataCaster\Cast\CSVCast; use CodeIgniter\DataCaster\Cast\DatetimeCast; +use CodeIgniter\DataCaster\Cast\EnumCast; use CodeIgniter\DataCaster\Cast\FloatCast; use CodeIgniter\DataCaster\Cast\IntBoolCast; use CodeIgniter\DataCaster\Cast\IntegerCast; @@ -48,6 +49,7 @@ final class DataCaster 'boolean' => BooleanCast::class, 'csv' => CSVCast::class, 'datetime' => DatetimeCast::class, + 'enum' => EnumCast::class, 'double' => FloatCast::class, 'float' => FloatCast::class, 'int' => IntegerCast::class, diff --git a/system/Entity/Cast/BaseCast.php b/system/Entity/Cast/BaseCast.php index 34cb28e787c5..41a19e570d9b 100644 --- a/system/Entity/Cast/BaseCast.php +++ b/system/Entity/Cast/BaseCast.php @@ -18,27 +18,11 @@ */ abstract class BaseCast implements CastInterface { - /** - * Get - * - * @param array|bool|float|int|object|string|null $value Data - * @param array $params Additional param - * - * @return array|bool|float|int|object|string|null - */ public static function get($value, array $params = []) { return $value; } - /** - * Set - * - * @param array|bool|float|int|object|string|null $value Data - * @param array $params Additional param - * - * @return array|bool|float|int|object|string|null - */ public static function set($value, array $params = []) { return $value; diff --git a/system/Entity/Cast/CastInterface.php b/system/Entity/Cast/CastInterface.php index 9d790e8edbb9..d2c6950ee927 100644 --- a/system/Entity/Cast/CastInterface.php +++ b/system/Entity/Cast/CastInterface.php @@ -26,7 +26,7 @@ interface CastInterface * Takes a raw value from Entity, returns its value for PHP. * * @param array|bool|float|int|object|string|null $value Data - * @param array $params Additional param + * @param array $params Additional param * * @return array|bool|float|int|object|string|null */ @@ -36,7 +36,7 @@ public static function get($value, array $params = []); * Takes a PHP value, returns its raw value for Entity. * * @param array|bool|float|int|object|string|null $value Data - * @param array $params Additional param + * @param array $params Additional param * * @return array|bool|float|int|object|string|null */ diff --git a/system/Entity/Cast/EnumCast.php b/system/Entity/Cast/EnumCast.php new file mode 100644 index 000000000000..5bb4ac6a9ecf --- /dev/null +++ b/system/Entity/Cast/EnumCast.php @@ -0,0 +1,142 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Entity\Cast; + +use BackedEnum; +use CodeIgniter\Entity\Exceptions\CastException; +use ReflectionEnum; +use UnitEnum; + +/** + * Class EnumCast + * + * Handles casting for PHP enums (both backed and unit enums) + */ +class EnumCast extends BaseCast +{ + /** + * {@inheritDoc} + */ + public static function get($value, array $params = []) + { + $enumClass = $params[0] ?? null; + + if ($enumClass === null) { + throw CastException::forMissingEnumClass(); + } + + if (! enum_exists($enumClass)) { + throw CastException::forNotEnum($enumClass); + } + + $reflection = new ReflectionEnum($enumClass); + + // Backed enum - validate and cast the value to proper type + if ($reflection->isBacked()) { + $backingType = $reflection->getBackingType(); + + // Cast to proper type (int or string) + if ($backingType->getName() === 'int') { + $value = (int) $value; + } elseif ($backingType->getName() === 'string') { + $value = (string) $value; + } + + $enum = $enumClass::tryFrom($value); + + if ($enum === null) { + throw CastException::forInvalidEnumValue($enumClass, $value); + } + + return $enum; + } + + // Unit enum - match by name + foreach ($enumClass::cases() as $case) { + if ($case->name === $value) { + return $case; + } + } + + throw CastException::forInvalidEnumCaseName($enumClass, $value); + } + + /** + * {@inheritDoc} + */ + public static function set($value, array $params = []): int|string|null + { + // Get the expected enum class + $enumClass = $params[0] ?? null; + + if ($enumClass === null) { + throw CastException::forMissingEnumClass(); + } + + if (! enum_exists($enumClass)) { + throw CastException::forNotEnum($enumClass); + } + + // If it's already an enum object, validate and extract its value + if (is_object($value) && enum_exists($value::class)) { + // Validate that the enum is of the expected type + if (! $value instanceof $enumClass) { + throw CastException::forInvalidEnumType($enumClass, $value::class); + } + + $reflection = new ReflectionEnum($value::class); + + // Backed enum - return the properly typed backing value + if ($reflection->isBacked()) { + /** @var BackedEnum $value */ + return $value->value; + } + + // Unit enum - return the case name + /** @var UnitEnum $value */ + return $value->name; + } + + $reflection = new ReflectionEnum($enumClass); + + // Validate backed enum values + if ($reflection->isBacked()) { + $backingType = $reflection->getBackingType(); + + // Cast to proper type (int or string) + if ($backingType->getName() === 'int') { + $value = (int) $value; + } elseif ($backingType->getName() === 'string') { + $value = (string) $value; + } + + if ($enumClass::tryFrom($value) === null) { + throw CastException::forInvalidEnumValue($enumClass, $value); + } + + return $value; + } + + // Validate unit enum case names - must be a string + $value = (string) $value; + + foreach ($enumClass::cases() as $case) { + if ($case->name === $value) { + return $value; + } + } + + throw CastException::forInvalidEnumCaseName($enumClass, $value); + } +} diff --git a/system/Entity/Entity.php b/system/Entity/Entity.php index 0e54c722e354..0cbcdef00904 100644 --- a/system/Entity/Entity.php +++ b/system/Entity/Entity.php @@ -18,6 +18,7 @@ use CodeIgniter\Entity\Cast\BooleanCast; use CodeIgniter\Entity\Cast\CSVCast; use CodeIgniter\Entity\Cast\DatetimeCast; +use CodeIgniter\Entity\Cast\EnumCast; use CodeIgniter\Entity\Cast\FloatCast; use CodeIgniter\Entity\Cast\IntBoolCast; use CodeIgniter\Entity\Cast\IntegerCast; @@ -92,6 +93,7 @@ class Entity implements JsonSerializable 'csv' => CSVCast::class, 'datetime' => DatetimeCast::class, 'double' => FloatCast::class, + 'enum' => EnumCast::class, 'float' => FloatCast::class, 'int' => IntegerCast::class, 'integer' => IntegerCast::class, diff --git a/system/Entity/Exceptions/CastException.php b/system/Entity/Exceptions/CastException.php index 90d3885d9b62..033f1ced478e 100644 --- a/system/Entity/Exceptions/CastException.php +++ b/system/Entity/Exceptions/CastException.php @@ -72,4 +72,54 @@ public static function forInvalidTimestamp() { return new static(lang('Cast.invalidTimestamp')); } + + /** + * Thrown when the enum class is not specified in cast parameters. + * + * @return static + */ + public static function forMissingEnumClass() + { + return new static(lang('Cast.enumMissingClass')); + } + + /** + * Thrown when the specified class is not an enum. + * + * @return static + */ + public static function forNotEnum(string $class) + { + return new static(lang('Cast.enumNotEnum', [$class])); + } + + /** + * Thrown when an invalid value is provided for an enum. + * + * @return static + */ + public static function forInvalidEnumValue(string $enumClass, mixed $value) + { + return new static(lang('Cast.enumInvalidValue', [$enumClass, $value])); + } + + /** + * Thrown when an invalid case name is provided for a unit enum. + * + * @return static + */ + public static function forInvalidEnumCaseName(string $enumClass, string $caseName) + { + return new static(lang('Cast.enumInvalidCaseName', [$caseName, $enumClass])); + } + + /** + * Thrown when an enum instance of wrong type is provided. + * + * @return static + */ + public static function forInvalidEnumType(string $expectedClass, string $actualClass) + { + return new static(lang('Cast.enumInvalidType', [$actualClass, $expectedClass])); + } } diff --git a/system/Language/en/Cast.php b/system/Language/en/Cast.php index 04ff1ef9e11f..63d9fba01b7c 100644 --- a/system/Language/en/Cast.php +++ b/system/Language/en/Cast.php @@ -14,6 +14,11 @@ // Cast language settings return [ 'baseCastMissing' => 'The "{0}" class must inherit the "CodeIgniter\Entity\Cast\BaseCast" class.', + 'enumInvalidCaseName' => 'Invalid case name "{0}" for enum "{1}".', + 'enumInvalidType' => 'Expected enum of type "{1}", but received "{0}".', + 'enumInvalidValue' => 'Invalid value "{1}" for enum "{0}".', + 'enumMissingClass' => 'Enum class must be specified for enum casting.', + 'enumNotEnum' => 'The "{0}" is not a valid enum class.', 'invalidCastMethod' => 'The "{0}" is invalid cast method, valid methods are: ["get", "set"].', 'invalidTimestamp' => 'Type casting "timestamp" expects a correct timestamp.', 'jsonErrorCtrlChar' => 'Unexpected control character found.', diff --git a/tests/_support/Enum/ColorEnum.php b/tests/_support/Enum/ColorEnum.php new file mode 100644 index 000000000000..3e529ffc386c --- /dev/null +++ b/tests/_support/Enum/ColorEnum.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace Tests\Support\Enum; + +enum ColorEnum +{ + case RED; + case GREEN; + case BLUE; +} diff --git a/tests/_support/Enum/RoleEnum.php b/tests/_support/Enum/RoleEnum.php new file mode 100644 index 000000000000..9984f76451eb --- /dev/null +++ b/tests/_support/Enum/RoleEnum.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace Tests\Support\Enum; + +enum RoleEnum: int +{ + case GUEST = 0; + case USER = 1; + case ADMIN = 2; +} diff --git a/tests/_support/Enum/StatusEnum.php b/tests/_support/Enum/StatusEnum.php new file mode 100644 index 000000000000..0c02aaaa4704 --- /dev/null +++ b/tests/_support/Enum/StatusEnum.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace Tests\Support\Enum; + +enum StatusEnum: string +{ + case PENDING = 'pending'; + case ACTIVE = 'active'; + case INACTIVE = 'inactive'; +} diff --git a/tests/system/DataConverter/DataConverterTest.php b/tests/system/DataConverter/DataConverterTest.php index 10a35e6909fc..220e101ba762 100644 --- a/tests/system/DataConverter/DataConverterTest.php +++ b/tests/system/DataConverter/DataConverterTest.php @@ -14,6 +14,7 @@ namespace CodeIgniter\DataConverter; use Closure; +use CodeIgniter\DataCaster\Exceptions\CastException; use CodeIgniter\HTTP\URI; use CodeIgniter\I18n\Time; use CodeIgniter\Test\CIUnitTestCase; @@ -22,6 +23,9 @@ use PHPUnit\Framework\Attributes\Group; use Tests\Support\Entity\CustomUser; use Tests\Support\Entity\User; +use Tests\Support\Enum\ColorEnum; +use Tests\Support\Enum\RoleEnum; +use Tests\Support\Enum\StatusEnum; /** * @internal @@ -193,6 +197,76 @@ public static function provideConvertDataFromDB(): iterable 'temp' => 15.9, ], ], + 'enum string-backed' => [ + [ + 'id' => 'int', + 'status' => 'enum[' . StatusEnum::class . ']', + ], + [ + 'id' => '1', + 'status' => 'active', + ], + [ + 'id' => 1, + 'status' => StatusEnum::ACTIVE, + ], + ], + 'enum int-backed' => [ + [ + 'id' => 'int', + 'role' => 'enum[' . RoleEnum::class . ']', + ], + [ + 'id' => '1', + 'role' => '2', + ], + [ + 'id' => 1, + 'role' => RoleEnum::ADMIN, + ], + ], + 'enum unit' => [ + [ + 'id' => 'int', + 'color' => 'enum[' . ColorEnum::class . ']', + ], + [ + 'id' => '1', + 'color' => 'RED', + ], + [ + 'id' => 1, + 'color' => ColorEnum::RED, + ], + ], + 'enum nullable null' => [ + [ + 'id' => 'int', + 'status' => '?enum[' . StatusEnum::class . ']', + ], + [ + 'id' => '1', + 'status' => null, + ], + [ + 'id' => 1, + 'status' => null, + ], + ], + 'enum nullable not null' => [ + [ + 'id' => 'int', + 'status' => '?enum[' . StatusEnum::class . ']', + ], + [ + 'id' => '1', + 'status' => 'pending', + ], + [ + 'id' => 1, + 'status' => StatusEnum::PENDING, + ], + ], ]; } @@ -321,6 +395,76 @@ public static function provideConvertDataToDB(): iterable 'temp' => 15.9, ], ], + 'enum string-backed' => [ + [ + 'id' => 'int', + 'status' => 'enum[' . StatusEnum::class . ']', + ], + [ + 'id' => 1, + 'status' => StatusEnum::ACTIVE, + ], + [ + 'id' => 1, + 'status' => 'active', + ], + ], + 'enum int-backed' => [ + [ + 'id' => 'int', + 'role' => 'enum[' . RoleEnum::class . ']', + ], + [ + 'id' => 1, + 'role' => RoleEnum::ADMIN, + ], + [ + 'id' => 1, + 'role' => 2, + ], + ], + 'enum unit' => [ + [ + 'id' => 'int', + 'color' => 'enum[' . ColorEnum::class . ']', + ], + [ + 'id' => 1, + 'color' => ColorEnum::RED, + ], + [ + 'id' => 1, + 'color' => 'RED', + ], + ], + 'enum nullable null' => [ + [ + 'id' => 'int', + 'status' => '?enum[' . StatusEnum::class . ']', + ], + [ + 'id' => 1, + 'status' => null, + ], + [ + 'id' => 1, + 'status' => null, + ], + ], + 'enum nullable not null' => [ + [ + 'id' => 'int', + 'status' => '?enum[' . StatusEnum::class . ']', + ], + [ + 'id' => 1, + 'status' => StatusEnum::PENDING, + ], + [ + 'id' => 1, + 'status' => 'pending', + ], + ], ]; } @@ -728,4 +872,116 @@ public function testExtractWithClosure(): void 'created_at' => '2023-12-02 07:35:57', ], $array); } + + /** + * @param array $types + * @param array $data + */ + #[DataProvider('provideEnumExceptions')] + public function testEnumExceptions(array $types, array $data, string $message, bool $useToDataSource): void + { + $this->expectException(CastException::class); + $this->expectExceptionMessage($message); + + $converter = $this->createDataConverter($types); + + if ($useToDataSource) { + $converter->toDataSource($data); + } else { + $converter->fromDataSource($data); + } + } + + /** + * @return iterable|bool|string>> + */ + public static function provideEnumExceptions(): iterable + { + return [ + 'get invalid backed enum value' => [ + 'types' => [ + 'id' => 'int', + 'status' => 'enum[' . StatusEnum::class . ']', + ], + 'data' => [ + 'id' => '1', + 'status' => 'invalid_status', + ], + 'message' => 'Invalid value "invalid_status" for enum "Tests\Support\Enum\StatusEnum"', + 'useToDataSource' => false, + ], + 'get invalid unit enum case name' => [ + 'types' => [ + 'id' => 'int', + 'color' => 'enum[' . ColorEnum::class . ']', + ], + 'data' => [ + 'id' => '1', + 'color' => 'YELLOW', + ], + 'message' => 'Invalid case name "YELLOW" for enum "Tests\Support\Enum\ColorEnum"', + 'useToDataSource' => false, + ], + 'get missing class' => [ + 'types' => [ + 'id' => 'int', + 'status' => 'enum', + ], + 'data' => [ + 'id' => '1', + 'status' => 'active', + ], + 'message' => 'Enum class must be specified for enum casting', + 'useToDataSource' => false, + ], + 'get not enum' => [ + 'types' => [ + 'id' => 'int', + 'status' => 'enum[stdClass]', + ], + 'data' => [ + 'id' => '1', + 'status' => 'active', + ], + 'message' => 'The "stdClass" is not a valid enum class', + 'useToDataSource' => false, + ], + 'set invalid type' => [ + 'types' => [ + 'id' => 'int', + 'status' => 'enum[' . StatusEnum::class . ']', + ], + 'data' => [ + 'id' => 1, + 'status' => ColorEnum::RED, + ], + 'message' => 'Expected enum of type "Tests\Support\Enum\StatusEnum", but received "Tests\Support\Enum\ColorEnum"', + 'useToDataSource' => true, + ], + 'set missing class' => [ + 'types' => [ + 'id' => 'int', + 'status' => 'enum', + ], + 'data' => [ + 'id' => 1, + 'status' => StatusEnum::ACTIVE, + ], + 'message' => 'Enum class must be specified for enum casting', + 'useToDataSource' => true, + ], + 'set not enum' => [ + 'types' => [ + 'id' => 'int', + 'status' => 'enum[stdClass]', + ], + 'data' => [ + 'id' => 1, + 'status' => StatusEnum::ACTIVE, + ], + 'message' => 'The "stdClass" is not a valid enum class', + 'useToDataSource' => true, + ], + ]; + } } diff --git a/tests/system/Entity/EntityTest.php b/tests/system/Entity/EntityTest.php index 92ed11aa36b6..8abc287956b4 100644 --- a/tests/system/Entity/EntityTest.php +++ b/tests/system/Entity/EntityTest.php @@ -21,11 +21,15 @@ use CodeIgniter\Test\ReflectionHelper; use DateTime; use DateTimeInterface; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Group; use ReflectionException; use Tests\Support\Entity\Cast\CastBase64; use Tests\Support\Entity\Cast\CastPassParameters; use Tests\Support\Entity\Cast\NotExtendsBaseCast; +use Tests\Support\Enum\ColorEnum; +use Tests\Support\Enum\RoleEnum; +use Tests\Support\Enum\StatusEnum; use Tests\Support\SomeEntity; /** @@ -811,6 +815,204 @@ public function testCustomCastParams(): void $this->assertSame('test_nullable_type:["nullable"]', $entity->fourth); } + public function testCastEnumStringBacked(): void + { + $entity = new class () extends Entity { + protected $casts = [ + 'status' => 'enum[' . StatusEnum::class . ']', + ]; + }; + + $entity->status = 'active'; + + $this->assertInstanceOf(StatusEnum::class, $entity->status); + $this->assertSame(StatusEnum::ACTIVE, $entity->status); + $this->assertSame(['status' => 'active'], $entity->toRawArray()); + } + + public function testCastEnumIntBacked(): void + { + $entity = new class () extends Entity { + protected $casts = [ + 'role' => 'enum[' . RoleEnum::class . ']', + ]; + }; + + $entity->role = 2; + + $this->assertInstanceOf(RoleEnum::class, $entity->role); + $this->assertSame(RoleEnum::ADMIN, $entity->role); + $this->assertSame(['role' => 2], $entity->toRawArray()); + } + + public function testCastEnumUnit(): void + { + $entity = new class () extends Entity { + protected $casts = [ + 'color' => 'enum[' . ColorEnum::class . ']', + ]; + }; + + $entity->color = 'RED'; + + $this->assertInstanceOf(ColorEnum::class, $entity->color); + $this->assertSame(ColorEnum::RED, $entity->color); + $this->assertSame(['color' => 'RED'], $entity->toRawArray()); + } + + public function testCastEnumNullable(): void + { + $entity = new class () extends Entity { + protected $casts = [ + 'status' => '?enum[' . StatusEnum::class . ']', + ]; + }; + + $entity->status = null; + + $this->assertNull($entity->status); + + $entity->status = 'pending'; + + $this->assertInstanceOf(StatusEnum::class, $entity->status); + $this->assertSame(StatusEnum::PENDING, $entity->status); + } + + #[DataProvider('provideCastEnumExceptions')] + public function testCastEnumExceptions(string $castType, mixed $value, string $property, string $message, bool $useInject): void + { + $this->expectException(CastException::class); + $this->expectExceptionMessage($message); + + $entity = new class ($castType, $property) extends Entity { + protected $casts = []; + + public function __construct(string $castType, string $property) + { + $this->casts[$property] = $castType; + parent::__construct(); + } + }; + + if ($useInject) { + // Inject raw data to bypass set() validation and test get() method + $entity->injectRawData([$property => $value]); + // Trigger get() method by accessing property + $entity->{$property}; // @phpstan-ignore expr.resultUnused + } else { + // Test set() method directly + $entity->{$property} = $value; + } + } + + /** + * @return iterable> + */ + public static function provideCastEnumExceptions(): iterable + { + return [ + 'missing class' => [ + 'castType' => 'enum', + 'value' => 'active', + 'property' => 'status', + 'message' => 'Enum class must be specified for enum casting', + 'useInject' => false, + ], + 'not enum' => [ + 'castType' => 'enum[stdClass]', + 'value' => 'active', + 'property' => 'status', + 'message' => 'The "stdClass" is not a valid enum class', + 'useInject' => false, + ], + 'invalid backed enum value' => [ + 'castType' => 'enum[' . StatusEnum::class . ']', + 'value' => 'invalid_status', + 'property' => 'status', + 'message' => 'Invalid value "invalid_status" for enum "Tests\Support\Enum\StatusEnum"', + 'useInject' => false, + ], + 'invalid unit enum case name' => [ + 'castType' => 'enum[' . ColorEnum::class . ']', + 'value' => 'YELLOW', + 'property' => 'color', + 'message' => 'Invalid case name "YELLOW" for enum "Tests\Support\Enum\ColorEnum"', + 'useInject' => false, + ], + 'invalid enum type' => [ + 'castType' => 'enum[' . StatusEnum::class . ']', + 'value' => ColorEnum::RED, + 'property' => 'status', + 'message' => 'Expected enum of type "Tests\Support\Enum\StatusEnum", but received "Tests\Support\Enum\ColorEnum"', + 'useInject' => false, + ], + 'get missing class' => [ + 'castType' => 'enum', + 'value' => 'active', + 'property' => 'status', + 'message' => 'Enum class must be specified for enum casting', + 'useInject' => true, + ], + 'get not enum' => [ + 'castType' => 'enum[stdClass]', + 'value' => 'active', + 'property' => 'status', + 'message' => 'The "stdClass" is not a valid enum class', + 'useInject' => true, + ], + 'get invalid backed enum value' => [ + 'castType' => 'enum[' . StatusEnum::class . ']', + 'value' => 'invalid_status', + 'property' => 'status', + 'message' => 'Invalid value "invalid_status" for enum "Tests\Support\Enum\StatusEnum"', + 'useInject' => true, + ], + 'get invalid unit enum case name' => [ + 'castType' => 'enum[' . ColorEnum::class . ']', + 'value' => 'YELLOW', + 'property' => 'color', + 'message' => 'Invalid case name "YELLOW" for enum "Tests\Support\Enum\ColorEnum"', + 'useInject' => true, + ], + ]; + } + + public function testCastEnumSetWithBackedEnumObject(): void + { + $entity = new class () extends Entity { + protected $casts = [ + 'status' => 'enum[' . StatusEnum::class . ']', + ]; + }; + + // Assign an enum object directly + $entity->status = StatusEnum::ACTIVE; + + // Should extract the backing value for storage + $this->assertSame(['status' => 'active'], $entity->toRawArray()); + // Should return the enum object when accessed + $this->assertInstanceOf(StatusEnum::class, $entity->status); + $this->assertSame(StatusEnum::ACTIVE, $entity->status); + } + + public function testCastEnumSetWithUnitEnumObject(): void + { + $entity = new class () extends Entity { + protected $casts = [ + 'color' => 'enum[' . ColorEnum::class . ']', + ]; + }; + + // Assign a unit enum object directly + $entity->color = ColorEnum::RED; + + // Should extract the case name for storage + $this->assertSame(['color' => 'RED'], $entity->toRawArray()); + // Should return the enum object when accessed + $this->assertInstanceOf(ColorEnum::class, $entity->color); + $this->assertSame(ColorEnum::RED, $entity->color); + } + public function testAsArray(): void { $entity = $this->getEntity(); diff --git a/user_guide_src/source/changelogs/v4.7.0.rst b/user_guide_src/source/changelogs/v4.7.0.rst index 3438e832d55c..d91e9e2fd604 100644 --- a/user_guide_src/source/changelogs/v4.7.0.rst +++ b/user_guide_src/source/changelogs/v4.7.0.rst @@ -77,6 +77,7 @@ Libraries - **CURLRequest:** Added ``shareConnection`` config item to change default share connection. - **CURLRequest:** Added ``dns_cache_timeout`` option to change default DNS cache timeout. - **CURLRequest:** Added ``fresh_connect`` options to enable/disable request fresh connection. +- **DataConverter:** Added ``EnumCast`` caster for database and entity. - **Email:** Added support for choosing the SMTP authorization method. You can change it via ``Config\Email::$SMTPAuthMethod`` option. - **Image:** The ``ImageMagickHandler`` has been rewritten to rely solely on the PHP ``imagick`` extension. - **Image:** Added ``ImageMagickHandler::clearMetadata()`` method to remove image metadata for privacy protection. diff --git a/user_guide_src/source/models/entities.rst b/user_guide_src/source/models/entities.rst index ce5313f4115d..316800238d7a 100644 --- a/user_guide_src/source/models/entities.rst +++ b/user_guide_src/source/models/entities.rst @@ -243,10 +243,11 @@ Scalar Type Casting ------------------- Properties can be cast to any of the following data types: -**integer**, **float**, **double**, **string**, **boolean**, **object**, **array**, **datetime**, **timestamp**, **uri** and **int-bool**. +**integer**, **float**, **double**, **string**, **boolean**, **object**, **array**, **datetime**, **timestamp**, **uri**, **int-bool** and **enum**. Add a question mark at the beginning of type to mark property as nullable, i.e., **?string**, **?integer**. .. note:: **int-bool** can be used since v4.3.0. +.. note:: **enum** can be used since v4.7.0. For example, if you had a User entity with an ``is_banned`` property, you can cast it as a boolean: @@ -289,6 +290,35 @@ Stored in the database as "red,yellow,green": .. note:: Casting as CSV uses PHP's internal ``implode`` and ``explode`` methods and assumes all values are string-safe and free of commas. For more complex data casts try ``array`` or ``json``. +Enum Casting +------------ + +.. versionadded:: 4.7.0 + +You can cast properties to PHP enums. You must specify the enum class name as a parameter. + +Enum casting supports: + +* **Backed enums** (string or int) - The backing value is stored in the database +* **Unit enums** - The case name is stored in the database as a string +* **Nullable enums** - Use the ``?`` prefix + +For example, if you had a User entity with a ``status`` property using a backed enum: + +.. literalinclude:: entities/024.php + +You can cast it in your Entity: + +.. literalinclude:: entities/025.php + +Now, when you access the ``status`` property, it will automatically be converted to a ``UserStatus`` enum instance: + +.. literalinclude:: entities/026.php + +For nullable enums: + +.. literalinclude:: entities/027.php + Custom Casting -------------- diff --git a/user_guide_src/source/models/entities/024.php b/user_guide_src/source/models/entities/024.php new file mode 100644 index 000000000000..c5730ea04b99 --- /dev/null +++ b/user_guide_src/source/models/entities/024.php @@ -0,0 +1,10 @@ + 'enum[App\Enums\UserStatus]', + ]; +} diff --git a/user_guide_src/source/models/entities/026.php b/user_guide_src/source/models/entities/026.php new file mode 100644 index 000000000000..d289af310151 --- /dev/null +++ b/user_guide_src/source/models/entities/026.php @@ -0,0 +1,17 @@ +find(1); + +// Returns a UserStatus enum instance +echo $user->status->value; // 'active' + +// Set using enum +$user->status = UserStatus::Inactive; + +// Or set using the backing value (will be converted to enum on read) +$user->status = 'pending'; + +// Note: Internally, enums are always stored as their backing value (string/int) +// in the entity's $attributes array diff --git a/user_guide_src/source/models/entities/027.php b/user_guide_src/source/models/entities/027.php new file mode 100644 index 000000000000..15f88f7b5e61 --- /dev/null +++ b/user_guide_src/source/models/entities/027.php @@ -0,0 +1,12 @@ + '?enum[App\Enums\UserStatus]', + ]; +} diff --git a/user_guide_src/source/models/model.rst b/user_guide_src/source/models/model.rst index 57fb52a024c3..182aed11ac0b 100644 --- a/user_guide_src/source/models/model.rst +++ b/user_guide_src/source/models/model.rst @@ -377,6 +377,8 @@ of type to mark the field as nullable, i.e., ``?int``, ``?datetime``. +---------------+----------------+---------------------------+ |``uri`` | URI | string type | +---------------+----------------+---------------------------+ +|``enum`` | Enum | string/int type | ++---------------+----------------+---------------------------+ csv --- @@ -413,6 +415,19 @@ timestamp The timezone of the ``Time`` instance created will be the default timezone (app's timezone), not UTC. +enum +---- + +.. versionadded:: 4.7.0 + +You can cast fields to PHP enums. You must specify the enum class name as a parameter, +like ``enum[App\Enums\StatusEnum]``. + +Enum casting supports: + +* **Backed enums** (string or int) - The backing value is stored in the database +* **Unit enums** - The case name is stored in the database as a string + Custom Casting ============== diff --git a/utils/phpstan-baseline/loader.neon b/utils/phpstan-baseline/loader.neon index 126e9b0b4f95..8459dac65197 100644 --- a/utils/phpstan-baseline/loader.neon +++ b/utils/phpstan-baseline/loader.neon @@ -1,4 +1,4 @@ -# total 2782 errors +# total 2768 errors includes: - argument.type.neon diff --git a/utils/phpstan-baseline/missingType.iterableValue.neon b/utils/phpstan-baseline/missingType.iterableValue.neon index a28b63cc43e6..3624a4d661e0 100644 --- a/utils/phpstan-baseline/missingType.iterableValue.neon +++ b/utils/phpstan-baseline/missingType.iterableValue.neon @@ -1,4 +1,4 @@ -# total 1379 errors +# total 1362 errors parameters: ignoreErrors: @@ -2332,11 +2332,6 @@ parameters: count: 1 path: ../../system/Encryption/Handlers/SodiumHandler.php - - - message: '#^Method CodeIgniter\\Entity\\Cast\\ArrayCast\:\:get\(\) has parameter \$params with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Entity/Cast/ArrayCast.php - - message: '#^Method CodeIgniter\\Entity\\Cast\\ArrayCast\:\:get\(\) has parameter \$value with no value type specified in iterable type array\.$#' count: 1 @@ -2347,21 +2342,11 @@ parameters: count: 1 path: ../../system/Entity/Cast/ArrayCast.php - - - message: '#^Method CodeIgniter\\Entity\\Cast\\ArrayCast\:\:set\(\) has parameter \$params with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Entity/Cast/ArrayCast.php - - message: '#^Method CodeIgniter\\Entity\\Cast\\ArrayCast\:\:set\(\) has parameter \$value with no value type specified in iterable type array\.$#' count: 1 path: ../../system/Entity/Cast/ArrayCast.php - - - message: '#^Method CodeIgniter\\Entity\\Cast\\BaseCast\:\:get\(\) has parameter \$params with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Entity/Cast/BaseCast.php - - message: '#^Method CodeIgniter\\Entity\\Cast\\BaseCast\:\:get\(\) has parameter \$value with no value type specified in iterable type array\.$#' count: 1 @@ -2372,11 +2357,6 @@ parameters: count: 1 path: ../../system/Entity/Cast/BaseCast.php - - - message: '#^Method CodeIgniter\\Entity\\Cast\\BaseCast\:\:set\(\) has parameter \$params with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Entity/Cast/BaseCast.php - - message: '#^Method CodeIgniter\\Entity\\Cast\\BaseCast\:\:set\(\) has parameter \$value with no value type specified in iterable type array\.$#' count: 1 @@ -2387,21 +2367,11 @@ parameters: count: 1 path: ../../system/Entity/Cast/BaseCast.php - - - message: '#^Method CodeIgniter\\Entity\\Cast\\BooleanCast\:\:get\(\) has parameter \$params with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Entity/Cast/BooleanCast.php - - message: '#^Method CodeIgniter\\Entity\\Cast\\BooleanCast\:\:get\(\) has parameter \$value with no value type specified in iterable type array\.$#' count: 1 path: ../../system/Entity/Cast/BooleanCast.php - - - message: '#^Method CodeIgniter\\Entity\\Cast\\CSVCast\:\:get\(\) has parameter \$params with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Entity/Cast/CSVCast.php - - message: '#^Method CodeIgniter\\Entity\\Cast\\CSVCast\:\:get\(\) has parameter \$value with no value type specified in iterable type array\.$#' count: 1 @@ -2412,21 +2382,11 @@ parameters: count: 1 path: ../../system/Entity/Cast/CSVCast.php - - - message: '#^Method CodeIgniter\\Entity\\Cast\\CSVCast\:\:set\(\) has parameter \$params with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Entity/Cast/CSVCast.php - - message: '#^Method CodeIgniter\\Entity\\Cast\\CSVCast\:\:set\(\) has parameter \$value with no value type specified in iterable type array\.$#' count: 1 path: ../../system/Entity/Cast/CSVCast.php - - - message: '#^Method CodeIgniter\\Entity\\Cast\\CastInterface\:\:get\(\) has parameter \$params with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Entity/Cast/CastInterface.php - - message: '#^Method CodeIgniter\\Entity\\Cast\\CastInterface\:\:get\(\) has parameter \$value with no value type specified in iterable type array\.$#' count: 1 @@ -2437,11 +2397,6 @@ parameters: count: 1 path: ../../system/Entity/Cast/CastInterface.php - - - message: '#^Method CodeIgniter\\Entity\\Cast\\CastInterface\:\:set\(\) has parameter \$params with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Entity/Cast/CastInterface.php - - message: '#^Method CodeIgniter\\Entity\\Cast\\CastInterface\:\:set\(\) has parameter \$value with no value type specified in iterable type array\.$#' count: 1 @@ -2452,51 +2407,36 @@ parameters: count: 1 path: ../../system/Entity/Cast/CastInterface.php - - - message: '#^Method CodeIgniter\\Entity\\Cast\\DatetimeCast\:\:get\(\) has parameter \$params with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Entity/Cast/DatetimeCast.php - - message: '#^Method CodeIgniter\\Entity\\Cast\\DatetimeCast\:\:get\(\) has parameter \$value with no value type specified in iterable type array\.$#' count: 1 path: ../../system/Entity/Cast/DatetimeCast.php - - message: '#^Method CodeIgniter\\Entity\\Cast\\FloatCast\:\:get\(\) has parameter \$params with no value type specified in iterable type array\.$#' + message: '#^Method CodeIgniter\\Entity\\Cast\\EnumCast\:\:get\(\) has parameter \$value with no value type specified in iterable type array\.$#' count: 1 - path: ../../system/Entity/Cast/FloatCast.php - - - - message: '#^Method CodeIgniter\\Entity\\Cast\\FloatCast\:\:get\(\) has parameter \$value with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Entity/Cast/FloatCast.php + path: ../../system/Entity/Cast/EnumCast.php - - message: '#^Method CodeIgniter\\Entity\\Cast\\IntBoolCast\:\:get\(\) has parameter \$params with no value type specified in iterable type array\.$#' + message: '#^Method CodeIgniter\\Entity\\Cast\\EnumCast\:\:get\(\) return type has no value type specified in iterable type array\.$#' count: 1 - path: ../../system/Entity/Cast/IntBoolCast.php + path: ../../system/Entity/Cast/EnumCast.php - - message: '#^Method CodeIgniter\\Entity\\Cast\\IntBoolCast\:\:set\(\) has parameter \$params with no value type specified in iterable type array\.$#' + message: '#^Method CodeIgniter\\Entity\\Cast\\EnumCast\:\:set\(\) has parameter \$value with no value type specified in iterable type array\.$#' count: 1 - path: ../../system/Entity/Cast/IntBoolCast.php + path: ../../system/Entity/Cast/EnumCast.php - - message: '#^Method CodeIgniter\\Entity\\Cast\\IntegerCast\:\:get\(\) has parameter \$params with no value type specified in iterable type array\.$#' + message: '#^Method CodeIgniter\\Entity\\Cast\\FloatCast\:\:get\(\) has parameter \$value with no value type specified in iterable type array\.$#' count: 1 - path: ../../system/Entity/Cast/IntegerCast.php + path: ../../system/Entity/Cast/FloatCast.php - message: '#^Method CodeIgniter\\Entity\\Cast\\IntegerCast\:\:get\(\) has parameter \$value with no value type specified in iterable type array\.$#' count: 1 path: ../../system/Entity/Cast/IntegerCast.php - - - message: '#^Method CodeIgniter\\Entity\\Cast\\JsonCast\:\:get\(\) has parameter \$params with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Entity/Cast/JsonCast.php - - message: '#^Method CodeIgniter\\Entity\\Cast\\JsonCast\:\:get\(\) has parameter \$value with no value type specified in iterable type array\.$#' count: 1 @@ -2507,41 +2447,21 @@ parameters: count: 1 path: ../../system/Entity/Cast/JsonCast.php - - - message: '#^Method CodeIgniter\\Entity\\Cast\\JsonCast\:\:set\(\) has parameter \$params with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Entity/Cast/JsonCast.php - - message: '#^Method CodeIgniter\\Entity\\Cast\\JsonCast\:\:set\(\) has parameter \$value with no value type specified in iterable type array\.$#' count: 1 path: ../../system/Entity/Cast/JsonCast.php - - - message: '#^Method CodeIgniter\\Entity\\Cast\\ObjectCast\:\:get\(\) has parameter \$params with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Entity/Cast/ObjectCast.php - - message: '#^Method CodeIgniter\\Entity\\Cast\\ObjectCast\:\:get\(\) has parameter \$value with no value type specified in iterable type array\.$#' count: 1 path: ../../system/Entity/Cast/ObjectCast.php - - - message: '#^Method CodeIgniter\\Entity\\Cast\\StringCast\:\:get\(\) has parameter \$params with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Entity/Cast/StringCast.php - - message: '#^Method CodeIgniter\\Entity\\Cast\\StringCast\:\:get\(\) has parameter \$value with no value type specified in iterable type array\.$#' count: 1 path: ../../system/Entity/Cast/StringCast.php - - - message: '#^Method CodeIgniter\\Entity\\Cast\\TimestampCast\:\:get\(\) has parameter \$params with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Entity/Cast/TimestampCast.php - - message: '#^Method CodeIgniter\\Entity\\Cast\\TimestampCast\:\:get\(\) has parameter \$value with no value type specified in iterable type array\.$#' count: 1 @@ -2552,11 +2472,6 @@ parameters: count: 1 path: ../../system/Entity/Cast/TimestampCast.php - - - message: '#^Method CodeIgniter\\Entity\\Cast\\URICast\:\:get\(\) has parameter \$params with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Entity/Cast/URICast.php - - message: '#^Method CodeIgniter\\Entity\\Cast\\URICast\:\:get\(\) has parameter \$value with no value type specified in iterable type array\.$#' count: 1 diff --git a/utils/phpstan-baseline/staticMethod.notFound.neon b/utils/phpstan-baseline/staticMethod.notFound.neon index e8bc56bf5968..cec945e226cf 100644 --- a/utils/phpstan-baseline/staticMethod.notFound.neon +++ b/utils/phpstan-baseline/staticMethod.notFound.neon @@ -1,7 +1,17 @@ -# total 20 errors +# total 23 errors parameters: ignoreErrors: + - + message: '#^Call to an undefined static method UnitEnum\:\:tryFrom\(\)\.$#' + count: 1 + path: ../../system/DataCaster/Cast/EnumCast.php + + - + message: '#^Call to an undefined static method UnitEnum\:\:tryFrom\(\)\.$#' + count: 2 + path: ../../system/Entity/Cast/EnumCast.php + - message: '#^Call to an undefined static method CodeIgniter\\Config\\Factories\:\:cells\(\)\.$#' count: 1 From fd086cfa334a0652aa3a4fbbd6bb90e4b4d70076 Mon Sep 17 00:00:00 2001 From: michalsn Date: Thu, 9 Oct 2025 19:11:27 +0200 Subject: [PATCH 2/3] apply code suggestions --- system/DataCaster/Cast/CastInterface.php | 12 ++++++------ system/DataCaster/Cast/EnumCast.php | 6 ------ system/Entity/Cast/EnumCast.php | 13 +------------ user_guide_src/source/models/entities.rst | 1 - 4 files changed, 7 insertions(+), 25 deletions(-) diff --git a/system/DataCaster/Cast/CastInterface.php b/system/DataCaster/Cast/CastInterface.php index f90f2b227e1d..7f5b05102a7d 100644 --- a/system/DataCaster/Cast/CastInterface.php +++ b/system/DataCaster/Cast/CastInterface.php @@ -18,9 +18,9 @@ interface CastInterface /** * Takes a value from DataSource, returns its value for PHP. * - * @param mixed $value Data from database driver - * @param list $params Additional param - * @param object|null $helper Helper object. E.g., database connection + * @param mixed $value Data from database driver + * @param array $params Additional param + * @param object|null $helper Helper object. E.g., database connection * * @return mixed PHP native value */ @@ -33,9 +33,9 @@ public static function get( /** * Takes a PHP value, returns its value for DataSource. * - * @param mixed $value PHP native value - * @param list $params Additional param - * @param object|null $helper Helper object. E.g., database connection + * @param mixed $value PHP native value + * @param array $params Additional param + * @param object|null $helper Helper object. E.g., database connection * * @return mixed Data to pass to database driver */ diff --git a/system/DataCaster/Cast/EnumCast.php b/system/DataCaster/Cast/EnumCast.php index 07717a2301eb..4a5fe3a31a9a 100644 --- a/system/DataCaster/Cast/EnumCast.php +++ b/system/DataCaster/Cast/EnumCast.php @@ -28,9 +28,6 @@ */ class EnumCast extends BaseCast implements CastInterface { - /** - * @param array $params - */ public static function get( mixed $value, array $params = [], @@ -83,9 +80,6 @@ public static function get( return $enum; } - /** - * @param array $params - */ public static function set( mixed $value, array $params = [], diff --git a/system/Entity/Cast/EnumCast.php b/system/Entity/Cast/EnumCast.php index 5bb4ac6a9ecf..035a40614953 100644 --- a/system/Entity/Cast/EnumCast.php +++ b/system/Entity/Cast/EnumCast.php @@ -18,16 +18,8 @@ use ReflectionEnum; use UnitEnum; -/** - * Class EnumCast - * - * Handles casting for PHP enums (both backed and unit enums) - */ class EnumCast extends BaseCast { - /** - * {@inheritDoc} - */ public static function get($value, array $params = []) { $enumClass = $params[0] ?? null; @@ -72,10 +64,7 @@ public static function get($value, array $params = []) throw CastException::forInvalidEnumCaseName($enumClass, $value); } - /** - * {@inheritDoc} - */ - public static function set($value, array $params = []): int|string|null + public static function set($value, array $params = []): int|string { // Get the expected enum class $enumClass = $params[0] ?? null; diff --git a/user_guide_src/source/models/entities.rst b/user_guide_src/source/models/entities.rst index 316800238d7a..32f848941327 100644 --- a/user_guide_src/source/models/entities.rst +++ b/user_guide_src/source/models/entities.rst @@ -301,7 +301,6 @@ Enum casting supports: * **Backed enums** (string or int) - The backing value is stored in the database * **Unit enums** - The case name is stored in the database as a string -* **Nullable enums** - Use the ``?`` prefix For example, if you had a User entity with a ``status`` property using a backed enum: From a0cb18521c3021862df059166e734ada5b3f889a Mon Sep 17 00:00:00 2001 From: michalsn Date: Thu, 9 Oct 2025 20:45:56 +0200 Subject: [PATCH 3/3] apply code suggestions --- system/DataCaster/Cast/EnumCast.php | 2 +- system/Entity/Cast/EnumCast.php | 2 +- utils/phpstan-baseline/loader.neon | 2 +- utils/phpstan-baseline/missingType.iterableValue.neon | 7 +------ 4 files changed, 4 insertions(+), 9 deletions(-) diff --git a/system/DataCaster/Cast/EnumCast.php b/system/DataCaster/Cast/EnumCast.php index 4a5fe3a31a9a..454251dcba6b 100644 --- a/system/DataCaster/Cast/EnumCast.php +++ b/system/DataCaster/Cast/EnumCast.php @@ -32,7 +32,7 @@ public static function get( mixed $value, array $params = [], ?object $helper = null, - ): mixed { + ): BackedEnum|UnitEnum { if (! is_string($value) && ! is_int($value)) { self::invalidTypeValueError($value); } diff --git a/system/Entity/Cast/EnumCast.php b/system/Entity/Cast/EnumCast.php index 035a40614953..7ea82d6f24a5 100644 --- a/system/Entity/Cast/EnumCast.php +++ b/system/Entity/Cast/EnumCast.php @@ -20,7 +20,7 @@ class EnumCast extends BaseCast { - public static function get($value, array $params = []) + public static function get($value, array $params = []): BackedEnum|UnitEnum { $enumClass = $params[0] ?? null; diff --git a/utils/phpstan-baseline/loader.neon b/utils/phpstan-baseline/loader.neon index 8459dac65197..c1fa33e7414b 100644 --- a/utils/phpstan-baseline/loader.neon +++ b/utils/phpstan-baseline/loader.neon @@ -1,4 +1,4 @@ -# total 2768 errors +# total 2767 errors includes: - argument.type.neon diff --git a/utils/phpstan-baseline/missingType.iterableValue.neon b/utils/phpstan-baseline/missingType.iterableValue.neon index 3624a4d661e0..964c022fc992 100644 --- a/utils/phpstan-baseline/missingType.iterableValue.neon +++ b/utils/phpstan-baseline/missingType.iterableValue.neon @@ -1,4 +1,4 @@ -# total 1362 errors +# total 1361 errors parameters: ignoreErrors: @@ -2417,11 +2417,6 @@ parameters: count: 1 path: ../../system/Entity/Cast/EnumCast.php - - - message: '#^Method CodeIgniter\\Entity\\Cast\\EnumCast\:\:get\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Entity/Cast/EnumCast.php - - message: '#^Method CodeIgniter\\Entity\\Cast\\EnumCast\:\:set\(\) has parameter \$value with no value type specified in iterable type array\.$#' count: 1