Skip to content

Commit

Permalink
add datetime and array processing
Browse files Browse the repository at this point in the history
  • Loading branch information
klimov-paul committed Dec 22, 2023
1 parent 0187008 commit 129fe90
Show file tree
Hide file tree
Showing 5 changed files with 226 additions and 51 deletions.
187 changes: 140 additions & 47 deletions src/AttributeTypecastBehavior.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@

/**
* @property \CModel|\CActiveRecord $owner The owner component that this behavior is attached to.
* @property array $attributeTypes
*
* @author Paul Klimov <klimov.paul@gmail.com>
* @since 1.0
Expand All @@ -24,9 +23,13 @@ class AttributeTypecastBehavior extends CBehavior
const TYPE_FLOAT = 'float';
const TYPE_BOOLEAN = 'boolean';
const TYPE_STRING = 'string';
const TYPE_ARRAY = 'array';
const TYPE_ARRAY_OBJECT = 'array-object';
const TYPE_DATETIME = 'datetime';
const TYPE_TIMESTAMP = 'timestamp';

/**
* @var array|null attribute typecast map in format: attributeName => type.
* @var array<string, string|callable>|null attribute typecast map in format: attributeName => type.
* Type can be set via PHP callable, which accept raw value as an argument and should return
* typecast result.
* For example:
Expand All @@ -44,7 +47,7 @@ class AttributeTypecastBehavior extends CBehavior
*
* If not set, attribute type map will be composed automatically from the owner validation rules.
*/
private $_attributeTypes;
public $attributeTypes;
/**
* @var bool whether to skip typecasting of `null` values.
* If enabled attribute value which equals to `null` will not be type-casted (e.g. `null` remains `null`),
Expand Down Expand Up @@ -74,7 +77,7 @@ class AttributeTypecastBehavior extends CBehavior
* Note that changing this option value will have no effect after this behavior has been attached to the model.
* @since 2.0.14
*/
public $typecastAfterSave = false;
public $typecastAfterSave = true;
/**
* @var bool whether to perform typecasting after retrieving owner model data from
* the database (after find or refresh).
Expand All @@ -86,31 +89,26 @@ class AttributeTypecastBehavior extends CBehavior
public $typecastAfterFind = true;

/**
* @var array internal static cache for auto detected [[attributeTypes]] values
* in format: ownerClassName => attributeTypes
* @var array<string, mixed> stashed raw attributes, used to transfer raw non-scalar values from {@see beforeSave()} to {@see afterSave()}.
*/
private static $autoDetectedAttributeTypes = [];
private $_stashedAttributes = [];

/**
* @return array
* @var array<string, array> internal static cache for auto detected [[attributeTypes]] values
* in format: ownerClassName => attributeTypes
*/
public function getAttributeTypes(): array
{
if ($this->_attributeTypes === null) {
$this->_attributeTypes = $this->detectAttributeTypes();
}

return $this->_attributeTypes;
}
private static $autoDetectedAttributeTypes = [];

/**
* @param array $attributeTypes
* {@inheritdoc}
*/
public function setAttributeTypes(array $attributeTypes): self
public function attach($owner): void
{
$this->_attributeTypes = $attributeTypes;
parent::attach($owner);

return $this;
if ($this->attributeTypes === null) {
$this->attributeTypes = $this->detectAttributeTypes();
}
}

protected function detectAttributeTypes(): array
Expand Down Expand Up @@ -173,26 +171,48 @@ public function typecastAttributes($attributeNames = null)
*/
protected function typecastValue($value, $type)
{
if (is_scalar($type)) {
if (is_object($value) && method_exists($value, '__toString')) {
$value = $value->__toString();
}

switch ($type) {
case self::TYPE_INTEGER:
return (int) $value;
case self::TYPE_FLOAT:
return (float) $value;
case self::TYPE_BOOLEAN:
return (bool) $value;
case self::TYPE_STRING:
return (string) $value;
default:
throw new InvalidArgumentException("Unsupported type '{$type}'");
}
if (!is_scalar($type)) {
return call_user_func($type, $value);
}

return call_user_func($type, $value);
switch ($type) {
case self::TYPE_INTEGER:
case 'int':
return (int) $value;
case self::TYPE_FLOAT:
return (float) $value;
case self::TYPE_BOOLEAN:
case 'bool':
return (bool) $value;
case self::TYPE_STRING:
return (string) $value;
case self::TYPE_ARRAY:
if ($value === null || is_iterable($value)) {
return $value;
}

return json_decode($value, true);
case self::TYPE_ARRAY_OBJECT:
if ($value === null || is_iterable($value)) {
return $value;
}

return new \ArrayObject(json_decode($value, true));
case self::TYPE_DATETIME:
if ($value === null || $value instanceof \DateTime) {
return $value;
}

return \DateTime::createFromFormat('Y-m-d H:i:s', (string) $value);
case self::TYPE_TIMESTAMP:
if ($value === null || $value instanceof \DateTime) {
return $value;
}

return (new \DateTime())->setTimestamp((int) $value);
default:
throw new InvalidArgumentException("Unsupported attribute type '{$type}'");
}
}

/**
Expand Down Expand Up @@ -220,6 +240,76 @@ protected function detectAttributeTypesFromRules(): array
return $attributeTypes;
}

/**
* Stashes original raw value of attribute for the future restoration.
*
* @param string $name attribute name.
* @param mixed $value attribute raw value.
* @return void
*/
private function stashAttribute(string $name, $value): void
{
$this->_stashedAttributes[$name] = $value;
}

/**
* Applies all stashed attribute values to the owner.
*
* @return void
*/
private function applyStashedAttributes(): void
{
foreach ($this->_stashedAttributes as $name => $value) {
$this->owner->setAttribute($name, $value);
unset($this->_stashedAttributes[$name]);
}
}

/**
* Performs typecast for attributes values in the way they are suitable for the saving in database.
* E.g. convert objects and arrays to scalars.
*
* @return void
*/
protected function typecastAttributesForSaving(): void
{
foreach ($this->owner->getAttributes() as $name => $value) {
if ($value === null || is_scalar($value)) {
continue;
}

if ($value instanceof \CDbExpression) {
continue;
}

$this->stashAttribute($name, $value);

if (is_array($value) || $value instanceof \JsonSerializable) {
$this->owner->setAttribute($name, json_encode($value));

continue;
}

if ($value instanceof \DateTime) {
if (isset($this->attributeTypes[$name]) && $this->attributeTypes[$name] === self::TYPE_TIMESTAMP) {
$this->owner->setAttribute($name, $value->getTimestamp());
} else {
$this->owner->setAttribute($name, $value->format('Y-m-d H:i:s'));
}

continue;
}

if ($value instanceof \Traversable) {
$this->owner->setAttribute($name, json_encode(iterator_to_array($value)));

continue;
}

$this->owner->setAttribute($name, (string) $value);
}
}

// Event Handlers:

/**
Expand All @@ -234,13 +324,8 @@ public function events(): array
}

if ($this->getOwner() instanceof CActiveRecord) {
if ($this->typecastBeforeSave) {
$events['onBeforeSave'] = 'beforeSave';
}

if ($this->typecastAfterSave) {
$events['onAfterSave'] = 'afterSave';
}
$events['onBeforeSave'] = 'beforeSave';
$events['onAfterSave'] = 'afterSave';

if ($this->typecastAfterFind) {
$events['onAfterFind'] = 'afterFind';
Expand All @@ -267,7 +352,11 @@ public function afterValidate(CEvent $event): void
*/
public function beforeSave(CModelEvent $event): void
{
$this->typecastAttributes();
if ($this->typecastBeforeSave) {
$this->typecastAttributes();
}

$this->typecastAttributesForSaving();
}

/**
Expand All @@ -276,7 +365,11 @@ public function beforeSave(CModelEvent $event): void
*/
public function afterSave(CEvent $event): void
{
$this->typecastAttributes();
$this->applyStashedAttributes();

if ($this->typecastAfterSave) {
$this->typecastAttributes();
}
}

/**
Expand Down
74 changes: 74 additions & 0 deletions tests/AttributeTypecastBehaviorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

namespace yii1tech\model\typecast\test;

use ArrayObject;
use DateTime;
use yii1tech\model\typecast\AttributeTypecastBehavior;
use yii1tech\model\typecast\test\data\Item;
use yii1tech\model\typecast\test\data\ItemWithTypecast;
Expand Down Expand Up @@ -151,4 +153,76 @@ public function testSkipNotSelectedAttribute()
$model->refresh();
$this->assertSame(58, $model->category_id);
}

/**
* @depends testTypecast
*/
public function testDateTime(): void
{
$createdDateTime = new DateTime('yesterday');

$model = new ItemWithTypecast();
$model->created_date = $createdDateTime;
$model->created_timestamp = $createdDateTime;
$model->save(false);

$this->assertSame($createdDateTime, $model->created_date);
$this->assertSame($createdDateTime, $model->created_timestamp);

$model = ItemWithTypecast::model()->findByPk($model->id);

$this->assertSame($createdDateTime->getTimestamp(), $model->created_date->getTimestamp());
$this->assertSame($createdDateTime->getTimestamp(), $model->created_timestamp->getTimestamp());
}

/**
* @depends testTypecast
*/
public function testArray(): void
{
$array = [
'foo' => 'bar',
];

$model = new ItemWithTypecast();
$model->data_array = $array;
$model->save(false);

$this->assertSame($array, $model->data_array);

$model = ItemWithTypecast::model()->findByPk($model->id);

$this->assertSame($array, $model->data_array);
}

/**
* @depends testTypecast
*/
public function testArrayObject(): void
{
$array = [
'foo' => 'bar',
];
$arrayObject = new ArrayObject($array);

$model = new ItemWithTypecast();
$model->data_array_object = $arrayObject;
$model->save(false);

$this->assertSame($arrayObject, $model->data_array_object);

$model = ItemWithTypecast::model()->findByPk($model->id);

$this->assertNotSame($arrayObject, $model->data_array_object);
$this->assertSame($arrayObject->getArrayCopy(), $model->data_array_object->getArrayCopy());

$model = new ItemWithTypecast();
$model->data_array_object = $array;
$model->save(false);

$this->assertSame($array, $model->data_array_object);

$model = ItemWithTypecast::model()->findByPk($model->id);
$this->assertSame($array, $model->data_array_object->getArrayCopy());
}
}
8 changes: 5 additions & 3 deletions tests/TestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -81,9 +81,11 @@ protected function setupTestDbData()
'name' => 'string',
'price' => 'float',
'is_active' => 'boolean DEFAULT 0',
'created_at' => 'integer',
'created_timestamp' => 'integer',
'created_date' => 'datetime',
'callback' => 'string',
'data_array' => 'json',
'data_array_object' => 'json',
]);

// Data :
Expand All @@ -94,14 +96,14 @@ protected function setupTestDbData()
'category_id' => 1,
'name' => 'item1',
'is_active' => 0,
'created_at' => time(),
'created_timestamp' => time(),
'created_date' => date('Y-m-d H:i:s'),
],
[
'category_id' => 2,
'name' => 'item2',
'is_active' => 1,
'created_at' => time(),
'created_timestamp' => time(),
'created_date' => date('Y-m-d H:i:s'),
],
])->execute();
Expand Down
4 changes: 3 additions & 1 deletion tests/data/Item.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@
* @property string $name
* @property float $price
* @property bool $is_active
* @property int $created_at
* @property int $created_timestamp
* @property string $created_date
* @property string $callback
* @property array|string $data_array
* @property \ArrayObject|string $data_array_object
*/
class Item extends CActiveRecord
{
Expand Down

0 comments on commit 129fe90

Please sign in to comment.