diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index 600a3420746..1044a3fecd1 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -19,6 +19,7 @@ Yii Framework 2 Change Log - Enh #20279: Add to the `\yii\web\Request` `csrfHeader` property to configure a custom HTTP header for CSRF validation (olegbaturin) - Enh #20279: Add to the `\yii\web\Request` `csrfTokenSafeMethods` property to configure a custom safe HTTP methods list (olegbaturin) - Bug #20140: Fix compatibility with PHP 8.4: calling `session_set_save_handler()` (Izumi-kun) +- New #20185: Add `BackedEnum` support to `AttributeTypecastBehavior` (briedis) 2.0.51 July 18, 2024 -------------------- diff --git a/framework/behaviors/AttributeTypecastBehavior.php b/framework/behaviors/AttributeTypecastBehavior.php index b313cac1b61..d3a177b3d9e 100644 --- a/framework/behaviors/AttributeTypecastBehavior.php +++ b/framework/behaviors/AttributeTypecastBehavior.php @@ -267,9 +267,16 @@ protected function typecastValue($value, $type) return StringHelper::floatToString($value); } return (string) $value; - default: - throw new InvalidArgumentException("Unsupported type '{$type}'"); } + + if (PHP_VERSION_ID >= 80100 && is_subclass_of($type, \BackedEnum::class)) { + if ($value instanceof $type) { + return $value; + } + return $type::from($value); + } + + throw new InvalidArgumentException("Unsupported type '{$type}'"); } return call_user_func($type, $value); diff --git a/tests/framework/behaviors/AttributeTypecastBehaviorTest.php b/tests/framework/behaviors/AttributeTypecastBehaviorTest.php index 5af290672e1..27c228dea2c 100644 --- a/tests/framework/behaviors/AttributeTypecastBehaviorTest.php +++ b/tests/framework/behaviors/AttributeTypecastBehaviorTest.php @@ -7,11 +7,13 @@ namespace yiiunit\framework\behaviors; +use ValueError; use Yii; use yii\base\DynamicModel; use yii\base\Event; use yii\behaviors\AttributeTypecastBehavior; use yii\db\ActiveRecord; +use yiiunit\framework\db\enums\StatusTypeString; use yiiunit\TestCase; /** @@ -47,6 +49,7 @@ protected function setUp(): void 'price' => 'float', 'isActive' => 'boolean', 'callback' => 'string', + 'status' => 'string', ]; Yii::$app->getDb()->createCommand()->createTable('test_attribute_typecast', $columns)->execute(); } @@ -80,6 +83,55 @@ public function testTypecast() $this->assertSame('callback: foo', $model->callback); } + public function testTypecastEnum() + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Can not be tested on PHP < 8.1'); + } + + $model = new ActiveRecordAttributeTypecastWithEnum(); + + $model->status = StatusTypeString::Active; + + $model->getAttributeTypecastBehavior()->typecastAttributes(); + + $this->assertSame(StatusTypeString::Active, $model->status); + } + + /** + * @depends testTypecastEnum + */ + public function testTypecastEnumFromString() + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Can not be tested on PHP < 8.1'); + } + + $model = new ActiveRecordAttributeTypecastWithEnum(); + $model->status = 'active'; // Same as StatusTypeString::ACTIVE->value; + + $model->getAttributeTypecastBehavior()->typecastAttributes(); + + $this->assertSame(StatusTypeString::Active, $model->status); + } + + /** + * @depends testTypecastEnum + */ + public function testTypecastEnumFailWithInvalidValue() + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Can not be tested on PHP < 8.1'); + } + + $model = new ActiveRecordAttributeTypecastWithEnum(); + $model->status = 'invalid'; + + self::expectException(ValueError::class); + + $model->getAttributeTypecastBehavior()->typecastAttributes(); + } + /** * @depends testTypecast */ @@ -339,3 +391,37 @@ public function getAttributeTypecastBehavior() return $this->getBehavior('attributeTypecast'); } } + +/** + * Test Active Record class with [[AttributeTypecastBehavior]] behavior attached with an enum field. + * + * @property StatusTypeString $status + */ +class ActiveRecordAttributeTypecastWithEnum extends ActiveRecord +{ + public function behaviors() + { + return [ + 'attributeTypecast' => [ + 'class' => AttributeTypecastBehavior::className(), + 'attributeTypes' => [ + 'status' => StatusTypeString::class, + ], + 'typecastBeforeSave' => true, + ], + ]; + } + + public static function tableName() + { + return 'test_attribute_typecast'; + } + + /** + * @return AttributeTypecastBehavior + */ + public function getAttributeTypecastBehavior() + { + return $this->getBehavior('attributeTypecast'); + } +} diff --git a/tests/framework/db/CommandTest.php b/tests/framework/db/CommandTest.php index cd3ecc6d686..cc30d748860 100644 --- a/tests/framework/db/CommandTest.php +++ b/tests/framework/db/CommandTest.php @@ -1538,13 +1538,13 @@ public function testBindValuesSupportsEnums() $db = $this->getConnection(); $command = $db->createCommand(); - $command->setSql('SELECT :p1')->bindValues([':p1' => enums\Status::ACTIVE]); + $command->setSql('SELECT :p1')->bindValues([':p1' => enums\Status::Active]); $this->assertSame('ACTIVE', $command->params[':p1']); - $command->setSql('SELECT :p1')->bindValues([':p1' => enums\StatusTypeString::ACTIVE]); + $command->setSql('SELECT :p1')->bindValues([':p1' => enums\StatusTypeString::Active]); $this->assertSame('active', $command->params[':p1']); - $command->setSql('SELECT :p1')->bindValues([':p1' => enums\StatusTypeInt::ACTIVE]); + $command->setSql('SELECT :p1')->bindValues([':p1' => enums\StatusTypeInt::Active]); $this->assertSame(1, $command->params[':p1']); } else { $this->markTestSkipped('Enums are not supported in PHP < 8.1'); diff --git a/tests/framework/db/enums/Status.php b/tests/framework/db/enums/Status.php index 799a552319b..13235e730bc 100644 --- a/tests/framework/db/enums/Status.php +++ b/tests/framework/db/enums/Status.php @@ -4,6 +4,6 @@ enum Status { - case ACTIVE; - case INACTIVE; + case Active; + case Inactive; } diff --git a/tests/framework/db/enums/StatusTypeInt.php b/tests/framework/db/enums/StatusTypeInt.php index ed4739c0227..7a8d539c523 100644 --- a/tests/framework/db/enums/StatusTypeInt.php +++ b/tests/framework/db/enums/StatusTypeInt.php @@ -4,6 +4,6 @@ enum StatusTypeInt: int { - case ACTIVE = 1; - case INACTIVE = 0; + case Active = 1; + case Inactive = 0; } diff --git a/tests/framework/db/enums/StatusTypeString.php b/tests/framework/db/enums/StatusTypeString.php index 019f4273a7f..9f8f2af9979 100644 --- a/tests/framework/db/enums/StatusTypeString.php +++ b/tests/framework/db/enums/StatusTypeString.php @@ -4,6 +4,6 @@ enum StatusTypeString: string { - case ACTIVE = 'active'; - case INACTIVE = 'inactive'; + case Active = 'active'; + case Inactive = 'inactive'; }