Skip to content

Commit

Permalink
add attribute types detection from DB schema
Browse files Browse the repository at this point in the history
  • Loading branch information
klimov-paul committed Dec 22, 2023
1 parent 281564e commit a20d91e
Show file tree
Hide file tree
Showing 2 changed files with 107 additions and 10 deletions.
97 changes: 87 additions & 10 deletions src/AttributeTypecastBehavior.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
*/
Expand All @@ -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.
*/
Expand All @@ -94,7 +97,7 @@ class AttributeTypecastBehavior extends CBehavior
private $_stashedAttributes = [];

/**
* @var array<string, array> internal static cache for auto detected [[attributeTypes]] values
* @var array<string, array> internal static cache for auto detected {@see $attributeTypes} values
* in format: ownerClassName => attributeTypes
*/
private static $autoDetectedAttributeTypes = [];
Expand All @@ -111,18 +114,27 @@ public function attach($owner): void
}
}

/**
* Detects (guesses) the attribute types analysing owner class.
*
* @return array<string, string> 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
Expand All @@ -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.
*/
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand All @@ -240,6 +255,64 @@ protected function detectAttributeTypesFromRules(): array
return $attributeTypes;
}

/**
* Detects attribute types from the owner's DB table schema.
*
* @return array<string, string> 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.
*
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down
20 changes: 20 additions & 0 deletions tests/AttributeTypecastBehaviorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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']);
}
}

0 comments on commit a20d91e

Please sign in to comment.