├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── composer.json └── src ├── CallbackSerializer.php ├── DynamicAttributeBehavior.php ├── JsonExpressionSerializer.php ├── JsonSerializer.php ├── PhpSerializer.php └── SerializerInterface.php /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | Yii 2 ActiveRecord Dynamic Attribute extension Change Log 2 | ========================================================= 3 | 4 | 1.0.2, September 19, 2018 5 | ------------------------- 6 | 7 | - Enh: Usage of deprecated `yii\base\InvalidParamException` changed to `yii\base\InvalidArgumentException` one (klimov-paul) 8 | - Enh #1: `JsonExpressionSerializer` created providing support for `yii\db\JsonExpression` usage (klimov-paul) 9 | 10 | 11 | 1.0.1, November 3, 2017 12 | ----------------------- 13 | 14 | - Bug: Usage of deprecated `yii\base\Object` changed to `yii\base\BaseObject` allowing compatibility with PHP 7.2 (klimov-paul) 15 | 16 | 17 | 1.0.0, August 26, 2016 18 | ---------------------- 19 | 20 | - Initial release. 21 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The Yii framework is free software. It is released under the terms of 2 | the following BSD License. 3 | 4 | Copyright © 2015 by Yii2tech (https://github.com/yii2tech) 5 | All rights reserved. 6 | 7 | Redistribution and use in source and binary forms, with or without 8 | modification, are permitted provided that the following conditions 9 | are met: 10 | 11 | * Redistributions of source code must retain the above copyright 12 | notice, this list of conditions and the following disclaimer. 13 | * Redistributions in binary form must reproduce the above copyright 14 | notice, this list of conditions and the following disclaimer in 15 | the documentation and/or other materials provided with the 16 | distribution. 17 | * Neither the name of Yii2tech nor the names of its 18 | contributors may be used to endorse or promote products derived 19 | from this software without specific prior written permission. 20 | 21 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 22 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 23 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 24 | FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 25 | COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 26 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 27 | BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 28 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 29 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 30 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 31 | ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 32 | POSSIBILITY OF SUCH DAMAGE. 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | 4 | 5 |

ActiveRecord Dynamic Attribute Extension for Yii2

6 |
7 |

8 | 9 | This extension provides dynamic ActiveRecord attributes stored into the single field in serialized state. 10 | 11 | For license information check the [LICENSE](LICENSE.md)-file. 12 | 13 | [![Latest Stable Version](https://poser.pugx.org/yii2tech/ar-dynattribute/v/stable.png)](https://packagist.org/packages/yii2tech/ar-dynattribute) 14 | [![Total Downloads](https://poser.pugx.org/yii2tech/ar-dynattribute/downloads.png)](https://packagist.org/packages/yii2tech/ar-dynattribute) 15 | [![Build Status](https://travis-ci.org/yii2tech/ar-dynattribute.svg?branch=master)](https://travis-ci.org/yii2tech/ar-dynattribute) 16 | 17 | 18 | Installation 19 | ------------ 20 | 21 | The preferred way to install this extension is through [composer](http://getcomposer.org/download/). 22 | 23 | Either run 24 | 25 | ``` 26 | php composer.phar require --prefer-dist yii2tech/ar-dynattribute 27 | ``` 28 | 29 | or add 30 | 31 | ```json 32 | "yii2tech/ar-dynattribute": "*" 33 | ``` 34 | 35 | to the require section of your composer.json. 36 | 37 | 38 | Usage 39 | ----- 40 | 41 | This extension provides dynamic ActiveRecord attributes stored into the single field in serialized state. 42 | For example: imagine we create a web site, where logged in user may customize its appearance, like changing 43 | color schema or enable/disable sidebar and so on. In order to make this customization persistent all user's 44 | choices should be stored into the database. In general each view setting should have its own column in the 45 | 'user' table. However, this is not very practical in case your application is under development and new 46 | settings appear rapidly. Thus it make sense to use single text field, which will store all chosen view 47 | parameters in the serialized string. If new option introduced there will no necessity to change 'user' table 48 | schema. 49 | Migration for the 'user' table creation may look like following: 50 | 51 | ```php 52 | class m??????_??????_create_user extends \yii\db\Migration 53 | { 54 | public function up() 55 | { 56 | $this->createTable('User', [ 57 | 'id' => $this->primaryKey(), 58 | 'username' => $this->string()->notNull(), 59 | 'email' => $this->string()->notNull(), 60 | 'passwordHash' => $this->string()->notNull(), 61 | // ... 62 | 'viewParams' => $this->text(), // field, which stores view parameters in serialized state 63 | ]); 64 | } 65 | 66 | public function down() 67 | { 68 | $this->dropTable('User'); 69 | } 70 | } 71 | ``` 72 | 73 | **Heads up!** In general such data storage approach is a **bad** practice and is not recommended to be used. 74 | Its main drawback is inability to use dynamic attributes in condition for the search query. 75 | It is acceptable only for the attributes, which are directly set and read for single record only, and never 76 | used for the filter queries. 77 | 78 | > Tip: you may store dynamic attributes into 'JSON' type column instead of plain text, in case you are using modern DBMS 79 | with built-in JSON support (e.g. MySQL >= 5.5 or PostgreSQL), however, you will have to deal with possible search 80 | condition composition on your own - this extension does not provide explicit support for it. 81 | 82 | This extension provides [[\yii2tech\ar\dynattribute\DynamicAttributeBehavior]] ActiveRecord behavior for 83 | the dynamic attributes support. 84 | For example: 85 | 86 | ```php 87 | use yii\db\ActiveRecord; 88 | use yii2tech\ar\dynattribute\DynamicAttributeBehavior; 89 | 90 | class User extends ActiveRecord 91 | { 92 | public function behaviors() 93 | { 94 | return [ 95 | 'dynamicAttribute' => [ 96 | 'class' => DynamicAttributeBehavior::className(), 97 | 'storageAttribute' => 'viewParams', // field to store serialized attributes 98 | 'dynamicAttributeDefaults' => [ // default values for the dynamic attributes 99 | 'bgColor' => 'green', 100 | 'showSidebar' => true, 101 | ], 102 | ], 103 | ]; 104 | } 105 | 106 | public static function tableName() 107 | { 108 | return 'User'; 109 | } 110 | 111 | // ... 112 | } 113 | ``` 114 | 115 | Once being attached [[\yii2tech\ar\dynattribute\DynamicAttributeBehavior]] allows its owner to operate 116 | dynamic attributes just as regular one. On model save they will be serialized and stored into the holding 117 | field. After record is fetched from database the first attempt to read the dynamic attributes will unserialize 118 | them and prepare for the usage. 119 | For example: 120 | 121 | ```php 122 | $model = new User(); 123 | // ... 124 | $model->bgColor = 'red'; 125 | $model->showSidebar = false; 126 | $model->save(); // 'bgColor' and 'showSidebar' are serialized and stored at 'viewParams' 127 | echo $model->viewParams; // outputs: '{"bgColor": "red", "showSidebar": false}' 128 | 129 | $refreshedModel = User::findOne($model->getPrimaryKey()); 130 | echo $refreshedModel->bgColor; // outputs 'red' 131 | echo $refreshedModel->showSidebar; // outputs 'false' 132 | ``` 133 | 134 | You may use dynamic attributes as the regular ActiveRecord attributes. For example: you may 135 | specify the validation rules for them and obtain their values via web form. 136 | 137 | > Note: keep in mind that dynamic attributes do not correspond to ActiveRecord entity fields, thus 138 | some particular ActiveRecord methods like `updateAttributes()` will not work for them. 139 | 140 | 141 | ## Default values setup 142 | 143 | As you may note from above example, you can provide a default values for the dynamic attributes 144 | via [[\yii2tech\ar\dynattribute\DynamicAttributeBehavior::$dynamicAttributeDefaults]]. 145 | Thus once you need extra dynamic attribute for your model you can just update the `dynamicAttributeDefaults` 146 | list with corresponding value, without necessity to perform any updates on your database. 147 | 148 | ```php 149 | class User extends ActiveRecord 150 | { 151 | public function behaviors() 152 | { 153 | return [ 154 | 'dynamicAttribute' => [ 155 | 'class' => DynamicAttributeBehavior::className(), 156 | 'storageAttribute' => 'viewParams', 157 | 'dynamicAttributeDefaults' => [ 158 | 'bgColor' => 'green', 159 | 'showSidebar' => true, 160 | 'fontColor' => 'black', // newly added attribute 161 | ], 162 | ], 163 | ]; 164 | } 165 | 166 | // ... 167 | } 168 | 169 | $newModel = new User(); 170 | echo $newModel->bgColor; // outputs 'green' 171 | echo $newModel->showSidebar; // outputs 'true' 172 | 173 | $oldModel = User::find()->orderBy(['id' => SORT_ASC])->limit(1)->one(); 174 | echo $oldModel->viewParams; // outputs: '{"bgColor": "red", "showSidebar": false}' 175 | echo $oldModel->fontColor; // outputs: 'black' 176 | ``` 177 | 178 | > Note: you may exclude dynamic attribute, which value equals the default one, from saving disabling 179 | [[\yii2tech\ar\dynattribute\DynamicAttributeBehavior::$saveDynamicAttributeDefaults]] option. 180 | 181 | 182 | ## Restrict dynamic attribute list 183 | 184 | Setup of the dynamic attribute default values not only useful, but in general is necessary. 185 | This list puts a restriction on the possible dynamic attribute names. Only attributes, which 186 | have default value specified can be set or read from the model. This prevents the possible mistakes 187 | caused by typos in the code. 188 | For example: 189 | 190 | ```php 191 | $newModel = new User(); 192 | $newModel->bgColor = 'blue'; // works fine 193 | $newModel->unExistingAttribute = 10; // throws an exception! 194 | ``` 195 | 196 | However sometimes there is necessity of storage list of attributes, which can not be predicted. 197 | For example, saving response fields from some external service. 198 | In this case you can disable check performed on attribute setter using 199 | [[\yii2tech\ar\dynattribute\DynamicAttributeBehavior::$allowRandomDynamicAttribute]]. 200 | If it is set to `true` you will be able to setup any dynamic attribute no matter declared or not 201 | at `dynamicAttributeDefaults`. 202 | 203 | > Note: you can also use [[\yii2tech\ar\dynattribute\DynamicAttributeBehavior::setDynamicAttributes()]] method 204 | to bypass naming restriction. This method will set all provided attributes without any checks. 205 | 206 | You can as well control the dynamic attributes list to be actually saved using [[\yii2tech\ar\dynattribute\DynamicAttributeBehavior::$dynamicAttributeSaveFilter]]. 207 | If set to `true` it will exclude any attribute, which is not listed at `dynamicAttributeDefaults` option. You may as 208 | well specify it as a PHP callback, which will perform some custom filtering. This option allows you to remove obsolete 209 | dynamic attributes, which existed in the past, but no longer actual. 210 | 211 | 212 | ## Serializer setup 213 | 214 | By default [[\yii2tech\ar\dynattribute\DynamicAttributeBehavior]] saves the dynamic attribute in JSON 215 | format. However, you may setup another serializer for them via [[\yii2tech\ar\dynattribute\DynamicAttributeBehavior::$serializer]]. 216 | The following serializers are available withing this extension: 217 | 218 | - [[\yii2tech\ar\dynattribute\JsonSerializer]] - stores data in JSON format 219 | - [[\yii2tech\ar\dynattribute\PhpSerializer]] - stores data using PHP `serialize()`/`unserialize()` functions 220 | - [[\yii2tech\ar\dynattribute\CallbackSerializer]] - stores data via custom serialize PHP callback. 221 | - [[\yii2tech\ar\dynattribute\JsonExpressionSerializer]] - handles [[yii\db\JsonExpression]] instances, supporting usage of 'JSON' DB column types. 222 | 223 | Please refer to the particular serializer class for more details. 224 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "yii2tech/ar-dynattribute", 3 | "description": "Provides dynamic ActiveRecord attributes stored into the single field in serialized state", 4 | "keywords": ["yii2", "active", "record", "dynamic", "attribute"], 5 | "type": "yii2-extension", 6 | "license": "BSD-3-Clause", 7 | "support": { 8 | "issues": "https://github.com/yii2tech/ar-dynattribute/issues", 9 | "forum": "http://www.yiiframework.com/forum/", 10 | "wiki": "https://github.com/yii2tech/ar-dynattribute/wiki", 11 | "source": "https://github.com/yii2tech/ar-dynattribute" 12 | }, 13 | "authors": [ 14 | { 15 | "name": "Paul Klimov", 16 | "email": "klimov.paul@gmail.com" 17 | } 18 | ], 19 | "require": { 20 | "yiisoft/yii2": "~2.0.14" 21 | }, 22 | "repositories": [ 23 | { 24 | "type": "composer", 25 | "url": "https://asset-packagist.org" 26 | } 27 | ], 28 | "autoload": { 29 | "psr-4": {"yii2tech\\ar\\dynattribute\\": "src"} 30 | }, 31 | "extra": { 32 | "branch-alias": { 33 | "dev-master": "1.0.x-dev" 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/CallbackSerializer.php: -------------------------------------------------------------------------------- 1 | 16 | * @since 1.0 17 | */ 18 | class CallbackSerializer extends BaseObject implements SerializerInterface 19 | { 20 | /** 21 | * @var callable PHP callback, which should be used to serialize value. 22 | */ 23 | public $serialize; 24 | /** 25 | * @var callable PHP callback, which should be used to unserialize value. 26 | */ 27 | public $unserialize; 28 | 29 | 30 | /** 31 | * {@inheritdoc} 32 | */ 33 | public function serialize($value) 34 | { 35 | return call_user_func($this->serialize, $value); 36 | } 37 | 38 | /** 39 | * {@inheritdoc} 40 | */ 41 | public function unserialize($value) 42 | { 43 | return call_user_func($this->unserialize, $value); 44 | } 45 | } -------------------------------------------------------------------------------- /src/DynamicAttributeBehavior.php: -------------------------------------------------------------------------------- 1 | [ 33 | * 'class' => DynamicAttributeBehavior::className(), 34 | * 'storageAttribute' => 'viewParams', // field to store serialized attributes 35 | * 'dynamicAttributeDefaults' => [ // default values for the dynamic attributes 36 | * 'bgColor' => 'green', 37 | * 'showSidebar' => true, 38 | * ], 39 | * ], 40 | * ]; 41 | * } 42 | * 43 | * // ... 44 | * } 45 | * 46 | * $model = new User(); 47 | * $model->bgColor = 'red'; 48 | * $model->showSidebar = false; 49 | * $model->save(); // 'bgColor' and 'showSidebar' are serialized and stored at 'viewParams' 50 | * echo $model->viewParams; // outputs: '{"bgColor": "red", "showSidebar": false}' 51 | * ``` 52 | * 53 | * @property BaseActiveRecord $owner 54 | * @property string|array|SerializerInterface $serializer serializer instance or its configuration. 55 | * @property array $dynamicAttributes dynamic attributes in format: name => value. 56 | * @property bool $isDynamicAttributeInitialized whether the dynamic attributes have been initialized or not. 57 | * 58 | * @author Paul Klimov 59 | * @since 1.0 60 | */ 61 | class DynamicAttributeBehavior extends Behavior 62 | { 63 | /** 64 | * @var string name of the owner attribute, which stores serialized dynamic attribute values. 65 | */ 66 | public $storageAttribute = 'data'; 67 | /** 68 | * @var array list of dynamic attribute default values. 69 | * 70 | * For example: 71 | * 72 | * ```php 73 | * [ 74 | * 'hasComment' => false, 75 | * 'commentCount' => 0, 76 | * 'gender' => null, 77 | * ] 78 | * ``` 79 | */ 80 | public $dynamicAttributeDefaults = []; 81 | /** 82 | * @var bool whether to save dynamic attribute values, which are equals to the ones, specified via [[dynamicAttributeDefaults]]. 83 | * By default `true`, which means default values will be saved st [[storageAttribute]]. 84 | * If set to `false`, which means dynamic attribute, which value exactly matches (`===`) the one specified at [[dynamicAttributeDefaults]], 85 | * thus its value will pick up new default value if it is changed. 86 | */ 87 | public $saveDynamicAttributeDefaults = true; 88 | /** 89 | * @var bool whether set of the attribute with the name, which is not exist neither at current [[dynamicAttributes]] 90 | * nor at [[$dynamicAttributeDefaults]], is allowed or not. 91 | * By default this option is disabled, providing the limitation of the dynamic attribute names, which can 92 | * be set via virtual property access or [[setDynamicAttribute()]] method. 93 | * If enabled dynamic attribute with any name will be allowed to be set. 94 | */ 95 | public $allowRandomDynamicAttribute = false; 96 | /** 97 | * @var bool|callable whether to filter [[dynamicAttributes]] value before save. 98 | * Being `null` or `false` means no filtering is performed. 99 | * If set to `true` any attribute, which is not present at [[dynamicAttributeDefaults]] will be removed 100 | * before saving. 101 | * You may setup this option with PHP callback, which accept raw attribute list and should return filtered list: 102 | * 103 | * ```php 104 | * function (array $rawAttributes) { 105 | * return array 106 | * } 107 | * ``` 108 | */ 109 | public $dynamicAttributeSaveFilter; 110 | 111 | /** 112 | * @var array dynamic attributes in format: name => value. 113 | */ 114 | private $_dynamicAttributes; 115 | /** 116 | * @var string|array|SerializerInterface serializer instance or its configuration. 117 | * Following shortcuts are supported: 118 | * 119 | * - 'php' - use [[PhpSerializer]] 120 | * - 'json' - use [[JsonSerializer]] 121 | * 122 | * Using array configuration, you may omit 'class' parameter, in this case [[CallbackSerializer]] will be used. 123 | * For example: 124 | * 125 | * ```php 126 | * [ 127 | * 'serialize' => function ($value) { return serialize($value); }, 128 | * 'unserialize' => function ($value) { return unserialize($value); }, 129 | * ] 130 | * ``` 131 | */ 132 | private $_serializer = 'json'; 133 | 134 | 135 | /** 136 | * Returns dynamic attribute values. 137 | * @return array dynamic attribute values in format: name => value. 138 | */ 139 | public function getDynamicAttributes() 140 | { 141 | if ($this->_dynamicAttributes === null) { 142 | $this->_dynamicAttributes = $this->unserializeAttributes($this->owner->{$this->storageAttribute}); 143 | if (!empty($this->dynamicAttributeDefaults)) { 144 | $this->_dynamicAttributes = array_merge($this->dynamicAttributeDefaults, $this->_dynamicAttributes); 145 | } 146 | } 147 | return $this->_dynamicAttributes; 148 | } 149 | 150 | /** 151 | * Sets dynamic attribute values. 152 | * Note that this method ignores [[allowRandomDynamicAttribute]] option. 153 | * @param array $dynamicAttributes dynamic attribute values in format: name => value. 154 | */ 155 | public function setDynamicAttributes($dynamicAttributes) 156 | { 157 | $this->_dynamicAttributes = $dynamicAttributes; 158 | } 159 | 160 | /** 161 | * Returns the value of specified dynamic attribute. 162 | * @param string $name attribute name. 163 | * @return mixed attribute value. 164 | */ 165 | public function getDynamicAttribute($name) 166 | { 167 | $attributes = $this->getDynamicAttributes(); 168 | if (!array_key_exists($name, $attributes)) { 169 | throw new InvalidArgumentException('Getting unknown dynamic attribute: ' . get_class($this->owner) . '::$' . $name); 170 | } 171 | return $attributes[$name]; 172 | } 173 | 174 | /** 175 | * Sets the value of the specified dynamic attribute. 176 | * @param string $name attribute name. 177 | * @param mixed $value attribute value. 178 | */ 179 | public function setDynamicAttribute($name, $value) 180 | { 181 | $attributes = $this->getDynamicAttributes(); 182 | if (!$this->allowRandomDynamicAttribute && !array_key_exists($name, $attributes)) { 183 | throw new InvalidArgumentException('Setting unknown dynamic attribute: ' . get_class($this->owner) . '::$' . $name); 184 | } 185 | $attributes[$name] = $value; 186 | $this->setDynamicAttributes($attributes); 187 | } 188 | 189 | /** 190 | * @return SerializerInterface serializer instance 191 | */ 192 | public function getSerializer() 193 | { 194 | if (!is_object($this->_serializer)) { 195 | $this->_serializer = $this->createSerializer($this->_serializer); 196 | } 197 | return $this->_serializer; 198 | } 199 | 200 | /** 201 | * @param SerializerInterface|array|string $serializer serializer to be used 202 | */ 203 | public function setSerializer($serializer) 204 | { 205 | $this->_serializer = $serializer; 206 | } 207 | 208 | /** 209 | * Creates serializer from given configuration. 210 | * @param string|array $config serializer configuration. 211 | * @return SerializerInterface serializer instance 212 | */ 213 | protected function createSerializer($config) 214 | { 215 | if (is_string($config)) { 216 | switch ($config) { 217 | case 'php': 218 | $config = [ 219 | 'class' => PhpSerializer::className() 220 | ]; 221 | break; 222 | case 'json': 223 | $config = [ 224 | 'class' => JsonSerializer::className() 225 | ]; 226 | break; 227 | } 228 | } elseif (is_array($config)) { 229 | if (!isset($config['class'])) { 230 | $config['class'] = CallbackSerializer::className(); 231 | } 232 | } 233 | return Instance::ensure($config, 'yii2tech\ar\dynattribute\SerializerInterface'); 234 | } 235 | 236 | /** 237 | * @return bool whether the dynamic attributes have been initialized or not. 238 | */ 239 | public function getIsDynamicAttributeInitialized() 240 | { 241 | return ($this->_dynamicAttributes !== null); 242 | } 243 | 244 | /** 245 | * Serializes given attributes into a string. 246 | * @param array $attributes attributes to be serialized in format: name => value 247 | * @return string serialized attributes. 248 | */ 249 | protected function serializeAttributes($attributes) 250 | { 251 | ksort($attributes); // sort the data to facilitate 'dirty-attributes' AR feature 252 | return $this->getSerializer()->serialize($attributes); 253 | } 254 | 255 | /** 256 | * Restores attribute values from string. 257 | * @param string $source serialized data string. 258 | * @return array restored attributes. 259 | */ 260 | protected function unserializeAttributes($source) 261 | { 262 | if (empty($source)) { 263 | return []; 264 | } 265 | return (array)$this->getSerializer()->unserialize($source); 266 | } 267 | 268 | // Property Access Extension: 269 | 270 | /** 271 | * PHP getter magic method. 272 | * This method is overridden so that dynamic attribute can be accessed like property. 273 | * 274 | * @param string $name property name 275 | * @throws UnknownPropertyException if the property is not defined 276 | * @return mixed property value 277 | */ 278 | public function __get($name) 279 | { 280 | try { 281 | return parent::__get($name); 282 | } catch (UnknownPropertyException $exception) { 283 | $attributes = $this->getDynamicAttributes(); 284 | if (array_key_exists($name, $attributes)) { 285 | return $attributes[$name]; 286 | } 287 | throw $exception; 288 | } 289 | } 290 | 291 | /** 292 | * PHP setter magic method. 293 | * This method is overridden so that dynamic attribute can be accessed like property. 294 | * @param string $name property name 295 | * @param mixed $value property value 296 | * @throws UnknownPropertyException if the property is not defined 297 | */ 298 | public function __set($name, $value) 299 | { 300 | try { 301 | parent::__set($name, $value); 302 | } catch (UnknownPropertyException $exception) { 303 | $attributes = $this->getDynamicAttributes(); 304 | if (!$this->allowRandomDynamicAttribute && !array_key_exists($name, $attributes)) { 305 | throw $exception; 306 | } 307 | $attributes[$name] = $value; 308 | $this->setDynamicAttributes($attributes); 309 | } 310 | } 311 | 312 | /** 313 | * Checks if a property is set, i.e. defined and not null. 314 | * 315 | * Do not call this method directly as it is a PHP magic method that 316 | * will be implicitly called when executing `isset($object->property)`. 317 | * 318 | * Note that if the property is not defined, false will be returned. 319 | * @param string $name the property name or the event name 320 | * @return bool whether the named property is set (not null). 321 | * @see http://php.net/manual/en/function.isset.php 322 | */ 323 | public function __isset($name) 324 | { 325 | if (parent::__isset($name)) { 326 | return true; 327 | } 328 | $attributes = $this->getDynamicAttributes(); 329 | return isset($attributes[$name]); 330 | } 331 | 332 | /** 333 | * Sets an object property to null. 334 | * 335 | * Do not call this method directly as it is a PHP magic method that 336 | * will be implicitly called when executing `unset($object->property)`. 337 | * 338 | * Note that if the property is not defined, this method will do nothing. 339 | * If the property is read-only, it will throw an exception. 340 | * @param string $name the property name 341 | * @see http://php.net/manual/en/function.unset.php 342 | */ 343 | public function __unset($name) 344 | { 345 | $attributes = $this->getDynamicAttributes(); 346 | if (array_key_exists($name, $attributes)) { 347 | unset($attributes[$name]); 348 | $this->setDynamicAttributes($attributes); 349 | } else { 350 | parent::__unset($name); 351 | } 352 | } 353 | 354 | /** 355 | * {@inheritdoc} 356 | */ 357 | public function canGetProperty($name, $checkVars = true) 358 | { 359 | if (parent::canGetProperty($name, $checkVars)) { 360 | return true; 361 | } 362 | $attributes = $this->getDynamicAttributes(); 363 | return array_key_exists($name, $attributes); 364 | } 365 | 366 | /** 367 | * {@inheritdoc} 368 | */ 369 | public function canSetProperty($name, $checkVars = true) 370 | { 371 | if (parent::canSetProperty($name, $checkVars)) { 372 | return true; 373 | } 374 | $attributes = $this->getDynamicAttributes(); 375 | return $this->allowRandomDynamicAttribute || array_key_exists($name, $attributes); 376 | } 377 | 378 | // Events : 379 | 380 | /** 381 | * {@inheritdoc} 382 | */ 383 | public function events() 384 | { 385 | return [ 386 | BaseActiveRecord::EVENT_BEFORE_INSERT => 'beforeSave', 387 | BaseActiveRecord::EVENT_BEFORE_UPDATE => 'beforeSave', 388 | ]; 389 | } 390 | 391 | /** 392 | * Handles owner 'beforeInsert' and 'beforeUpdate' events, ensuring dynamic attributes are saved. 393 | * @param \yii\base\Event $event event instance. 394 | */ 395 | public function beforeSave($event) 396 | { 397 | if (!$this->getIsDynamicAttributeInitialized()) { 398 | return; 399 | } 400 | 401 | $attributes = $this->getDynamicAttributes(); 402 | 403 | if (!$this->saveDynamicAttributeDefaults) { 404 | foreach ($this->dynamicAttributeDefaults as $name => $value) { 405 | if (array_key_exists($name, $attributes) && $attributes[$name] === $value) { 406 | unset($attributes[$name]); 407 | } 408 | } 409 | } 410 | 411 | if ($this->dynamicAttributeSaveFilter !== null) { 412 | if ($this->dynamicAttributeSaveFilter === true) { 413 | $attributes = array_intersect_key($attributes, $this->dynamicAttributeDefaults); 414 | } else { 415 | $attributes = call_user_func($this->dynamicAttributeSaveFilter, $attributes); 416 | } 417 | } 418 | 419 | $data = $this->serializeAttributes($attributes); 420 | 421 | $this->owner->{$this->storageAttribute} = $data; 422 | } 423 | } -------------------------------------------------------------------------------- /src/JsonExpressionSerializer.php: -------------------------------------------------------------------------------- 1 | 20 | * @since 1.0.2 21 | */ 22 | class JsonExpressionSerializer extends BaseObject implements SerializerInterface 23 | { 24 | /** 25 | * @var string|null Type of JSON, expression should be casted to. Defaults to `null`, meaning 26 | * no explicit casting will be performed. 27 | * @see JsonExpression::$type 28 | */ 29 | public $type; 30 | 31 | 32 | /** 33 | * {@inheritdoc} 34 | */ 35 | public function serialize($value) 36 | { 37 | return new JsonExpression($value, $this->type); 38 | } 39 | 40 | /** 41 | * {@inheritdoc} 42 | */ 43 | public function unserialize($value) 44 | { 45 | if ($value instanceof JsonExpression) { 46 | return $value->getValue(); 47 | } 48 | 49 | return $value; 50 | } 51 | } -------------------------------------------------------------------------------- /src/JsonSerializer.php: -------------------------------------------------------------------------------- 1 | 17 | * @since 1.0 18 | */ 19 | class JsonSerializer extends BaseObject implements SerializerInterface 20 | { 21 | /** 22 | * @var int the encoding options. For more details please refer to 23 | * . 24 | * Default is `JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE`. 25 | */ 26 | public $options = 320; 27 | 28 | 29 | /** 30 | * {@inheritdoc} 31 | */ 32 | public function serialize($value) 33 | { 34 | return Json::encode($value, $this->options); 35 | } 36 | 37 | /** 38 | * {@inheritdoc} 39 | */ 40 | public function unserialize($value) 41 | { 42 | return Json::decode($value); 43 | } 44 | } -------------------------------------------------------------------------------- /src/PhpSerializer.php: -------------------------------------------------------------------------------- 1 | 16 | * @since 1.0 17 | */ 18 | class PhpSerializer extends BaseObject implements SerializerInterface 19 | { 20 | /** 21 | * {@inheritdoc} 22 | */ 23 | public function serialize($value) 24 | { 25 | return serialize($value); 26 | } 27 | 28 | /** 29 | * {@inheritdoc} 30 | */ 31 | public function unserialize($value) 32 | { 33 | return unserialize($value); 34 | } 35 | } -------------------------------------------------------------------------------- /src/SerializerInterface.php: -------------------------------------------------------------------------------- 1 | 14 | * @since 1.0 15 | */ 16 | interface SerializerInterface 17 | { 18 | /** 19 | * Serializes given value. 20 | * @param mixed $value value to be serialized 21 | * @return string serialized value. 22 | */ 23 | public function serialize($value); 24 | 25 | /** 26 | * Restores value from its serialized representations 27 | * @param string $value serialized string. 28 | * @return mixed restored value 29 | */ 30 | public function unserialize($value); 31 | } --------------------------------------------------------------------------------