From a4ecd76a539f46ef502a9088ff68dc630e8b6947 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?M=C4=81rti=C5=86=C5=A1=20Briedis?= <mbriedis@printful.com>
Date: Tue, 4 Jun 2024 15:52:32 +0300
Subject: [PATCH] Support BackedEnum attribute typecast behaviour.

---
 .../behaviors/AttributeTypecastBehavior.php   | 11 ++-
 .../AttributeTypecastBehaviorTest.php         | 86 +++++++++++++++++++
 2 files changed, 95 insertions(+), 2 deletions(-)

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..124dd483f74 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');
+    }
+}