From a20d91e25ad1ac3e3ed573d6312b42a637109c1d Mon Sep 17 00:00:00 2001 From: Paul Klimov Date: Fri, 22 Dec 2023 16:04:50 +0200 Subject: [PATCH] add attribute types detection from DB schema --- src/AttributeTypecastBehavior.php | 97 ++++++++++++++++++++++--- tests/AttributeTypecastBehaviorTest.php | 20 +++++ 2 files changed, 107 insertions(+), 10 deletions(-) diff --git a/src/AttributeTypecastBehavior.php b/src/AttributeTypecastBehavior.php index 4685b8b..acf6b8f 100644 --- a/src/AttributeTypecastBehavior.php +++ b/src/AttributeTypecastBehavior.php @@ -5,11 +5,14 @@ use CActiveRecord; use CBehavior; use CBooleanValidator; +use CDbColumnSchema; +use CDbException; use CEvent; use CModelEvent; use CNumberValidator; use CStringValidator; use InvalidArgumentException; +use Yii; /** * @property \CModel|\CActiveRecord $owner The owner component that this behavior is attached to. @@ -64,16 +67,16 @@ class AttributeTypecastBehavior extends CBehavior /** * @var bool whether to perform typecasting before saving owner model (insert or update). * This option may be disabled in order to achieve better performance. - * For example, in case of [[\yii\db\ActiveRecord]] usage, typecasting before save - * will grant no benefit an thus can be disabled. + * For example, in case of {@see \CActiveRecord} usage, typecasting before save + * will grant no benefit and thus can be disabled. * Note that changing this option value will have no effect after this behavior has been attached to the model. */ public $typecastBeforeSave = false; /** * @var bool whether to perform typecasting after saving owner model (insert or update). * This option may be disabled in order to achieve better performance. - * For example, in case of [[\yii\db\ActiveRecord]] usage, typecasting after save - * will grant no benefit an thus can be disabled. + * For example, in case of {@see \CActiveRecord} usage, typecasting after save + * will grant no benefit and thus can be disabled. * Note that changing this option value will have no effect after this behavior has been attached to the model. * @since 2.0.14 */ @@ -82,7 +85,7 @@ class AttributeTypecastBehavior extends CBehavior * @var bool whether to perform typecasting after retrieving owner model data from * the database (after find or refresh). * This option may be disabled in order to achieve better performance. - * For example, in case of [[\yii\db\ActiveRecord]] usage, typecasting after find + * For example, in case of {@see \CActiveRecord} usage, typecasting after find * will grant no benefit in most cases and thus can be disabled. * Note that changing this option value will have no effect after this behavior has been attached to the model. */ @@ -94,7 +97,7 @@ class AttributeTypecastBehavior extends CBehavior private $_stashedAttributes = []; /** - * @var array internal static cache for auto detected [[attributeTypes]] values + * @var array internal static cache for auto detected {@see $attributeTypes} values * in format: ownerClassName => attributeTypes */ private static $autoDetectedAttributeTypes = []; @@ -111,18 +114,27 @@ public function attach($owner): void } } + /** + * Detects (guesses) the attribute types analysing owner class. + * + * @return array detected attribute types. + */ protected function detectAttributeTypes(): array { $ownerClass = get_class($this->owner); if (!isset(self::$autoDetectedAttributeTypes[$ownerClass])) { - self::$autoDetectedAttributeTypes[$ownerClass] = $this->detectAttributeTypesFromRules(); + if ($this->owner instanceof CActiveRecord) { + self::$autoDetectedAttributeTypes[$ownerClass] = $this->detectAttributeTypesFromSchema(); + } else { + self::$autoDetectedAttributeTypes[$ownerClass] = $this->detectAttributeTypesFromRules(); + } } return self::$autoDetectedAttributeTypes[$ownerClass]; } /** - * Clears internal static cache of auto detected [[attributeTypes]] values + * Clears internal static cache of auto-detected {@see $attributeTypes} values * over all affected owner classes. */ public static function clearAutoDetectedAttributeTypes(): void @@ -131,9 +143,10 @@ public static function clearAutoDetectedAttributeTypes(): void } /** - * Typecast owner attributes according to [[attributeTypes]]. + * Typecast owner attributes according to {@see $attributeTypes}. + * * @param array|null $attributeNames list of attribute names that should be type-casted. - * If this parameter is empty, it means any attribute listed in the [[attributeTypes]] + * If this parameter is empty, it means any attribute listed in the {@see $attributeTypes} * should be type-casted. * @return \CModel|\CActiveRecord owner instance. */ @@ -165,6 +178,7 @@ public function typecastAttributes($attributeNames = null) /** * Casts the given value to the specified type. + * * @param mixed $value value to be type-casted. * @param string|callable $type type name or typecast callable. * @return mixed typecast result. @@ -217,6 +231,7 @@ protected function typecastValue($value, $type) /** * Composes default value for {@see $attributeTypes} from the owner validation rules. + * * @return array attribute type map. */ protected function detectAttributeTypesFromRules(): array @@ -240,6 +255,64 @@ protected function detectAttributeTypesFromRules(): array return $attributeTypes; } + /** + * Detects attribute types from the owner's DB table schema. + * + * @return array detected attribute types. + */ + protected function detectAttributeTypesFromSchema(): array + { + $tableName = $this->owner->tableName(); + + if (($table = $this->owner->getDbConnection()->getSchema()->getTable($tableName)) === null) { + throw new CDbException( + Yii::t('yii', 'The table "{table}" for active record class "{class}" cannot be found in the database.', [ + '{class}' => get_class($this->owner), + '{table}' => $tableName, + ]) + ); + } + + $attributeTypes = []; + foreach($table->columns as $column) { + $attributeTypes[$column->name] = $this->detectTypeFromDbColumnSchema($column); + } + + return $attributeTypes; + } + + /** + * Detects the attribute type from DB column schema. + * + * @param \CDbColumnSchema $column DB column schema. + * @return string type name. + */ + protected function detectTypeFromDbColumnSchema(CDbColumnSchema $column): string + { + switch ($column->type) { + case 'integer': + return self::TYPE_INTEGER; + case 'boolean': + return self::TYPE_BOOLEAN; + case 'double': + return self::TYPE_FLOAT; + } + + if (stripos($column->dbType, 'json') !== false) { + return self::TYPE_ARRAY_OBJECT; + } + + if (stripos($column->dbType, 'date') !== false) { + return self::TYPE_DATETIME; + } + + if (stripos($column->dbType, 'timestamp') !== false) { + return self::TYPE_DATETIME; + } + + return self::TYPE_STRING; + } + /** * Stashes original raw value of attribute for the future restoration. * @@ -337,6 +410,7 @@ public function events(): array /** * Handles owner 'afterValidate' event, ensuring attribute typecasting. + * * @param \CEvent $event event instance. */ public function afterValidate(CEvent $event): void @@ -348,6 +422,7 @@ public function afterValidate(CEvent $event): void /** * Handles owner 'beforeSave' owner event, ensuring attribute typecasting. + * * @param \CModelEvent $event event instance. */ public function beforeSave(CModelEvent $event): void @@ -361,6 +436,7 @@ public function beforeSave(CModelEvent $event): void /** * Handles owner 'afterSave' event, ensuring attribute typecasting. + * * @param \CEvent $event event instance. */ public function afterSave(CEvent $event): void @@ -374,6 +450,7 @@ public function afterSave(CEvent $event): void /** * Handles owner 'afterFind' event, ensuring attribute typecasting. + * * @param \CEvent $event event instance. */ public function afterFind(CEvent $event): void diff --git a/tests/AttributeTypecastBehaviorTest.php b/tests/AttributeTypecastBehaviorTest.php index a2018aa..beb2a75 100644 --- a/tests/AttributeTypecastBehaviorTest.php +++ b/tests/AttributeTypecastBehaviorTest.php @@ -244,4 +244,24 @@ public function testDetectedAttributeTypesFromRules(): void $this->assertSame(5.5, $model->price); $this->assertSame(true, $model->isAccepted); } + + /** + * @depends testTypecast + */ + public function testDetectedAttributeTypesFromSchema(): void + { + $baseBehavior = new AttributeTypecastBehavior(); + $baseBehavior->attributeTypes = null; + + $model = new Item(); + $baseBehavior->attach($model); + + $this->assertNotEmpty($baseBehavior->attributeTypes); + + $this->assertSame(AttributeTypecastBehavior::TYPE_INTEGER, $baseBehavior->attributeTypes['category_id']); + $this->assertSame(AttributeTypecastBehavior::TYPE_STRING, $baseBehavior->attributeTypes['name']); + $this->assertSame(AttributeTypecastBehavior::TYPE_FLOAT, $baseBehavior->attributeTypes['price']); + $this->assertSame(AttributeTypecastBehavior::TYPE_DATETIME, $baseBehavior->attributeTypes['created_date']); + $this->assertSame(AttributeTypecastBehavior::TYPE_ARRAY_OBJECT, $baseBehavior->attributeTypes['data_array_object']); + } } \ No newline at end of file