From 217f2d0a947b08a3c98d685444ec35f8cb2e628d Mon Sep 17 00:00:00 2001
From: Paul Klimov
Date: Thu, 21 Dec 2023 17:24:44 +0200
Subject: [PATCH] initial commit
---
.gitattributes | 9 +
.github/FUNDING.yml | 4 +
.github/ISSUE_TEMPLATE.md | 14 ++
.github/PULL_REQUEST_TEMPLATE.md | 7 +
.github/workflows/build.yml | 31 +++
.gitignore | 35 +++
CHANGELOG.md | 7 +
LICENSE.md | 32 +++
README.md | 41 ++++
composer.json | 39 ++++
phpunit.xml.dist | 15 ++
src/AttributeTypecastBehavior.php | 288 ++++++++++++++++++++++++
tests/AttributeTypecastBehaviorTest.php | 11 +
tests/TestCase.php | 104 +++++++++
tests/bootstrap.php | 14 ++
tests/data/Item.php | 46 ++++
16 files changed, 697 insertions(+)
create mode 100644 .gitattributes
create mode 100644 .github/FUNDING.yml
create mode 100644 .github/ISSUE_TEMPLATE.md
create mode 100644 .github/PULL_REQUEST_TEMPLATE.md
create mode 100644 .github/workflows/build.yml
create mode 100644 .gitignore
create mode 100644 CHANGELOG.md
create mode 100644 LICENSE.md
create mode 100644 README.md
create mode 100644 composer.json
create mode 100644 phpunit.xml.dist
create mode 100644 src/AttributeTypecastBehavior.php
create mode 100644 tests/AttributeTypecastBehaviorTest.php
create mode 100644 tests/TestCase.php
create mode 100644 tests/bootstrap.php
create mode 100644 tests/data/Item.php
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..b3d9906
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,9 @@
+# Ignore all test and documentation for archive
+/.github export-ignore
+/.gitattributes export-ignore
+/.gitignore export-ignore
+/.scrutinizer.yml export-ignore
+/.travis.yml export-ignore
+/phpunit.xml.dist export-ignore
+/tests export-ignore
+/docs export-ignore
\ No newline at end of file
diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
new file mode 100644
index 0000000..99679ca
--- /dev/null
+++ b/.github/FUNDING.yml
@@ -0,0 +1,4 @@
+# These are supported funding model platforms
+
+github: [klimov-paul]
+patreon: klimov_paul
diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md
new file mode 100644
index 0000000..2cc6439
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE.md
@@ -0,0 +1,14 @@
+### What steps will reproduce the problem?
+
+### What is the expected result?
+
+### What do you get instead?
+
+### Additional info
+
+| Q | A
+|-----------------------| ---
+| This Package Version | 1.?.?
+| Yii Framework Version | 1.1.?
+| PHP version |
+| Operating system |
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
new file mode 100644
index 0000000..f4af2f3
--- /dev/null
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -0,0 +1,7 @@
+| Q | A
+| ------------- | ---
+| Is bugfix? | ✔️/❌
+| New feature? | ✔️/❌
+| Breaks BC? | ✔️/❌
+| Tests pass? | ✔️/❌
+| Fixed issues | comma-separated list of tickets # fixed by the PR, if any
\ No newline at end of file
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
new file mode 100644
index 0000000..757df1b
--- /dev/null
+++ b/.github/workflows/build.yml
@@ -0,0 +1,31 @@
+name: build
+
+on: [push, pull_request]
+
+jobs:
+ phpunit:
+ name: PHP ${{ matrix.php }} on ${{ matrix.os }}
+ runs-on: ${{ matrix.os }}
+ strategy:
+ fail-fast: false
+ matrix:
+ os: [ubuntu-latest]
+ php: ['7.1', '7.2', '7.3', '7.4', '8.0', '8.1', '8.2']
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v3
+
+ - name: Install PHP
+ uses: shivammathur/setup-php@v2
+ with:
+ php-version: ${{ matrix.php }}
+ extensions: mbstring, pdo, sqlite, pdo_sqlite
+ tools: composer:v2
+ coverage: none
+
+ - name: Install dependencies
+ run: |
+ composer update --prefer-dist --no-interaction --no-progress --optimize-autoloader --ansi
+ - name: Run unit tests
+ run: vendor/bin/phpunit --colors=always
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..304f7c8
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,35 @@
+# phpstorm project files
+.idea
+
+# netbeans project files
+nbproject
+
+# zend studio for eclipse project files
+.buildpath
+.project
+.settings
+
+# windows thumbnail cache
+Thumbs.db
+
+# composer vendor dir
+/vendor
+
+/composer.lock
+
+# composer itself is not needed
+composer.phar
+
+# Mac DS_Store Files
+.DS_Store
+
+# phpunit itself is not needed
+phpunit.phar
+# local phpunit config
+/phpunit.xml
+# phpunit cache
+.phpunit.result.cache
+
+# test runtime files
+/.phpunit.cache
+/tests/runtime
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..954717a
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,7 @@
+Yii1 Model Typecast extension
+=============================
+
+1.0.0 Under Development
+-----------------------
+
+- Initial release.
diff --git a/LICENSE.md b/LICENSE.md
new file mode 100644
index 0000000..e641e6f
--- /dev/null
+++ b/LICENSE.md
@@ -0,0 +1,32 @@
+This is free software. It is released under the terms of the
+following BSD License.
+
+Copyright © 2023 by Yii1Tech (https://github.com/yii1tech)
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions
+are met:
+
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in
+ the documentation and/or other materials provided with the
+ distribution.
+ * Neither the name of Yii1Tech nor the names of its
+ contributors may be used to endorse or promote products derived
+ from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGE.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..9ce583c
--- /dev/null
+++ b/README.md
@@ -0,0 +1,41 @@
+
+
+
+
+
Model Attributes Typecast Extension for Yii 1
+
+
+
+This extension provides support for Yii1 Model and ActiveRecord attributes typecast.
+
+For license information check the [LICENSE](LICENSE.md)-file.
+
+[![Latest Stable Version](https://img.shields.io/packagist/v/yii1tech/model-typecast.svg)](https://packagist.org/packages/yii1tech/model-typecast)
+[![Total Downloads](https://img.shields.io/packagist/dt/yii1tech/model-typecast.svg)](https://packagist.org/packages/yii1tech/model-typecast)
+[![Build Status](https://github.com/yii1tech/model-typecast/workflows/build/badge.svg)](https://github.com/yii1tech/model-typecast/actions)
+
+
+Installation
+------------
+
+The preferred way to install this extension is through [composer](http://getcomposer.org/download/).
+
+Either run
+
+```
+php composer.phar require --prefer-dist yii1tech/model-typecast
+```
+
+or add
+
+```json
+"yii1tech/model-typecast": "*"
+```
+
+to the "require" section of your composer.json.
+
+
+Usage
+-----
+
+This extension provides support for Yii1 Model and ActiveRecord attributes typecast.
\ No newline at end of file
diff --git a/composer.json b/composer.json
new file mode 100644
index 0000000..4618db4
--- /dev/null
+++ b/composer.json
@@ -0,0 +1,39 @@
+{
+ "name": "yii1tech/model-typecast",
+ "description": "Allows typecast for Model and ActiveRecord attributes in Yii1",
+ "keywords": ["yii1", "model", "active", "record", "typecast"],
+ "license": "BSD-3-Clause",
+ "support": {
+ "issues": "https://github.com/yii1tech/model-typecast/issues",
+ "wiki": "https://github.com/yii1tech/model-typecast/wiki",
+ "source": "https://github.com/yii1tech/model-typecast"
+ },
+ "authors": [
+ {
+ "name": "Paul Klimov",
+ "email": "klimov.paul@gmail.com"
+ }
+ ],
+ "require": {
+ "php": ">=7.1",
+ "yiisoft/yii": "~1.1.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^6.0 || ^7.0 || ^8.0 || ^9.3 || ^10.0.7"
+ },
+ "autoload": {
+ "psr-4": {
+ "yii1tech\\model\\typecast\\": "src"
+ }
+ },
+ "autoload-dev": {
+ "psr-4": {
+ "yii1tech\\model\\typecast\\test\\": "tests"
+ }
+ },
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0.x-dev"
+ }
+ }
+}
\ No newline at end of file
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
new file mode 100644
index 0000000..1dc5e87
--- /dev/null
+++ b/phpunit.xml.dist
@@ -0,0 +1,15 @@
+
+
+
+
+ ./tests
+
+
+
diff --git a/src/AttributeTypecastBehavior.php b/src/AttributeTypecastBehavior.php
new file mode 100644
index 0000000..c183cdc
--- /dev/null
+++ b/src/AttributeTypecastBehavior.php
@@ -0,0 +1,288 @@
+
+ * @since 1.0
+ */
+class AttributeTypecastBehavior extends CBehavior
+{
+ const TYPE_INTEGER = 'integer';
+ const TYPE_FLOAT = 'float';
+ const TYPE_BOOLEAN = 'boolean';
+ const TYPE_STRING = 'string';
+
+ /**
+ * @var array|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:
+ *
+ * ```php
+ * [
+ * 'amount' => 'integer',
+ * 'price' => 'float',
+ * 'is_active' => 'boolean',
+ * 'date' => function ($value) {
+ * return ($value instanceof \DateTime) ? $value->getTimestamp(): (int) $value;
+ * },
+ * ]
+ * ```
+ *
+ * If not set, attribute type map will be composed automatically from the owner validation rules.
+ */
+ private $_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`),
+ * otherwise it will be converted according to the type configured at [[attributeTypes]].
+ */
+ public $skipOnNull = true;
+ /**
+ * @var bool whether to perform typecasting after owner model validation.
+ * Note that typecasting will be performed only if validation was successful, e.g.
+ * owner model has no errors.
+ * Note that changing this option value will have no effect after this behavior has been attached to the model.
+ */
+ public $typecastAfterValidate = true;
+ /**
+ * @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.
+ * 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.
+ * 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;
+ /**
+ * @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
+ * will grant no benefit in most cases an thus can be disabled.
+ * Note that changing this option value will have no effect after this behavior has been attached to the model.
+ */
+ public $typecastAfterFind = false;
+
+ /**
+ * @var array internal static cache for auto detected [[attributeTypes]] values
+ * in format: ownerClassName => attributeTypes
+ */
+ private static $autoDetectedAttributeTypes = [];
+
+ /**
+ * @return array
+ */
+ public function getAttributeTypes(): array
+ {
+ if ($this->_attributeTypes === null) {
+ $this->_attributeTypes = $this->detectAttributeTypes();
+ }
+
+ return $this->_attributeTypes;
+ }
+
+ /**
+ * @param array $attributeTypes
+ */
+ public function setAttributeTypes(array $attributeTypes): self
+ {
+ $this->_attributeTypes = $attributeTypes;
+
+ return $this;
+ }
+
+ protected function detectAttributeTypes(): array
+ {
+ $ownerClass = get_class($this->owner);
+ if (!isset(self::$autoDetectedAttributeTypes[$ownerClass])) {
+ self::$autoDetectedAttributeTypes[$ownerClass] = $this->detectAttributeTypesFromRules();
+ }
+
+ return self::$autoDetectedAttributeTypes[$ownerClass];
+ }
+
+ /**
+ * Clears internal static cache of auto detected [[attributeTypes]] values
+ * over all affected owner classes.
+ */
+ public static function clearAutoDetectedAttributeTypes(): void
+ {
+ self::$autoDetectedAttributeTypes = [];
+ }
+
+ /**
+ * Typecast owner attributes according to [[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]]
+ * should be type-casted.
+ * @return \CModel|\CActiveRecord owner instance.
+ */
+ public function typecastAttributes($attributeNames = null)
+ {
+ $attributeTypes = [];
+
+ if ($attributeNames === null) {
+ $attributeTypes = $this->attributeTypes;
+ } else {
+ foreach ($attributeNames as $attribute) {
+ if (!isset($this->attributeTypes[$attribute])) {
+ throw new InvalidArgumentException("There is no type mapping for '{$attribute}'.");
+ }
+ $attributeTypes[$attribute] = $this->attributeTypes[$attribute];
+ }
+ }
+
+ foreach ($attributeTypes as $attribute => $type) {
+ $value = $this->owner->{$attribute};
+ if ($this->skipOnNull && $value === null) {
+ continue;
+ }
+ $this->owner->{$attribute} = $this->typecastValue($value, $type);
+ }
+
+ return $this->owner;
+ }
+
+ /**
+ * 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.
+ */
+ 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}'");
+ }
+ }
+
+ return call_user_func($type, $value);
+ }
+
+ /**
+ * Composes default value for {@see $attributeTypes} from the owner validation rules.
+ * @return array attribute type map.
+ */
+ protected function detectAttributeTypesFromRules(): array
+ {
+ $attributeTypes = [];
+ foreach ($this->owner->getValidators() as $validator) {
+ $type = null;
+ if ($validator instanceof CBooleanValidator) {
+ $type = self::TYPE_BOOLEAN;
+ } elseif ($validator instanceof CNumberValidator) {
+ $type = $validator->integerOnly ? self::TYPE_INTEGER : self::TYPE_FLOAT;
+ } elseif ($validator instanceof CStringValidator) {
+ $type = self::TYPE_STRING;
+ }
+
+ if ($type !== null) {
+ $attributeTypes += array_fill_keys($validator->getAttributeNames(), $type);
+ }
+ }
+
+ return $attributeTypes;
+ }
+
+ // Event Handlers:
+
+ /**
+ * {@inheritdoc}
+ */
+ public function events(): array
+ {
+ $events = [];
+
+ if ($this->typecastAfterValidate) {
+ $events['onAfterValidate'] = 'afterValidate';
+ }
+
+ if ($this->getOwner() instanceof CActiveRecord) {
+ if ($this->typecastBeforeSave) {
+ $events['onBeforeSave'] = 'beforeSave';
+ }
+
+ if ($this->typecastAfterSave) {
+ $events['onAfterSave'] = 'afterSave';
+ }
+
+ if ($this->typecastAfterFind) {
+ $events['onAfterFind'] = 'afterFind';
+ }
+ }
+
+ return $events;
+ }
+
+ /**
+ * Handles owner 'afterValidate' event, ensuring attribute typecasting.
+ * @param \CModelEvent $event event instance.
+ */
+ public function afterValidate(CModelEvent $event): void
+ {
+ if (!$this->owner->hasErrors()) {
+ $this->typecastAttributes();
+ }
+ }
+
+ /**
+ * Handles owner 'beforeSave' owner event, ensuring attribute typecasting.
+ * @param \CModelEvent $event event instance.
+ */
+ public function beforeSave(CModelEvent $event): void
+ {
+ $this->typecastAttributes();
+ }
+
+ /**
+ * Handles owner 'afterSave' event, ensuring attribute typecasting.
+ * @param \CModelEvent $event event instance.
+ */
+ public function afterSave(CModelEvent $event): void
+ {
+ $this->typecastAttributes();
+ }
+
+ /**
+ * Handles owner 'afterFind' event, ensuring attribute typecasting.
+ * @param \CModelEvent $event event instance.
+ */
+ public function afterFind(CModelEvent $event): void
+ {
+ $this->typecastAttributes();
+ }
+}
\ No newline at end of file
diff --git a/tests/AttributeTypecastBehaviorTest.php b/tests/AttributeTypecastBehaviorTest.php
new file mode 100644
index 0000000..4cb4335
--- /dev/null
+++ b/tests/AttributeTypecastBehaviorTest.php
@@ -0,0 +1,11 @@
+assertTrue(true);
+ }
+}
\ No newline at end of file
diff --git a/tests/TestCase.php b/tests/TestCase.php
new file mode 100644
index 0000000..dc475eb
--- /dev/null
+++ b/tests/TestCase.php
@@ -0,0 +1,104 @@
+mockApplication();
+
+ $this->setupTestDbData();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function tearDown(): void
+ {
+ $this->destroyApplication();
+
+ parent::tearDown();
+ }
+
+ /**
+ * Populates Yii::app() with a new application
+ * The application will be destroyed on tearDown() automatically.
+ * @param array $config The application configuration, if needed
+ * @param string $appClass name of the application class to create
+ */
+ protected function mockApplication($config = [], $appClass = CConsoleApplication::class)
+ {
+ Yii::setApplication(null);
+
+ new $appClass(CMap::mergeArray([
+ 'id' => 'testapp',
+ 'basePath' => __DIR__,
+ 'components' => [
+ 'db' => [
+ 'class' => \CDbConnection::class,
+ 'connectionString' => 'sqlite::memory:',
+ ],
+ 'cache' => [
+ 'class' => \CDummyCache::class,
+ ],
+ ],
+ ], $config));
+ }
+
+ /**
+ * Destroys Yii application by setting it to null.
+ */
+ protected function destroyApplication()
+ {
+ Yii::setApplication(null);
+ }
+
+ /**
+ * Setup tables for test ActiveRecord
+ */
+ protected function setupTestDbData()
+ {
+ $db = Yii::app()->getDb();
+
+ // Structure :
+ $db->createCommand()
+ ->createTable('item', [
+ 'id' => 'pk',
+ 'category_id' => 'integer',
+ 'name' => 'string',
+ 'is_deleted' => 'boolean DEFAULT 0',
+ 'created_at' => 'integer',
+ 'created_date' => 'datetime',
+ ]);
+
+ // Data :
+ $builder = $db->getCommandBuilder();
+
+ $builder->createMultipleInsertCommand('item', [
+ [
+ 'category_id' => 1,
+ 'name' => 'item1',
+ 'is_deleted' => 0,
+ 'created_at' => time(),
+ 'created_date' => date('Y-m-d H:i:s'),
+ ],
+ [
+ 'category_id' => 2,
+ 'name' => 'item2',
+ 'is_deleted' => 1,
+ 'created_at' => time(),
+ 'created_date' => date('Y-m-d H:i:s'),
+ ],
+ ])->execute();
+ }
+}
\ No newline at end of file
diff --git a/tests/bootstrap.php b/tests/bootstrap.php
new file mode 100644
index 0000000..f238b2d
--- /dev/null
+++ b/tests/bootstrap.php
@@ -0,0 +1,14 @@
+ [
+ 'class' => AttributeTypecastBehavior::class,
+ ],
+ ];
+ }
+}
\ No newline at end of file