Completed
Push — master ( f259ea...93bbf5 )
by Alexander
82:39 queued 79:23
created

AttributeTypecastBehavior::typecastValue()   D

Complexity

Conditions 9
Paths 13

Size

Total Lines 26
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 9.5338

Importance

Changes 0
Metric Value
dl 0
loc 26
ccs 13
cts 16
cp 0.8125
rs 4.909
c 0
b 0
f 0
cc 9
eloc 18
nc 13
nop 2
crap 9.5338
1
<?php
2
/**
3
 * @link http://www.yiiframework.com/
4
 * @copyright Copyright (c) 2008 Yii Software LLC
5
 * @license http://www.yiiframework.com/license/
6
 */
7
8
namespace yii\behaviors;
9
10
use yii\base\Behavior;
11
use yii\base\InvalidParamException;
12
use yii\base\Model;
13
use yii\db\BaseActiveRecord;
14
use yii\helpers\StringHelper;
15
use yii\validators\BooleanValidator;
16
use yii\validators\NumberValidator;
17
use yii\validators\StringValidator;
18
19
/**
20
 * AttributeTypecastBehavior provides an ability of automatic model attribute typecasting.
21
 * This behavior is very useful in case of usage of ActiveRecord for the schema-less databases like MongoDB or Redis.
22
 * It may also come in handy for regular [[\yii\db\ActiveRecord]] or even [[\yii\base\Model]], allowing to maintain
23
 * strict attribute types after model validation.
24
 *
25
 * This behavior should be attached to [[\yii\base\Model]] or [[\yii\db\BaseActiveRecord]] descendant.
26
 *
27
 * You should specify exact attribute types via [[attributeTypes]].
28
 *
29
 * For example:
30
 *
31
 * ```php
32
 * use yii\behaviors\AttributeTypecastBehavior;
33
 *
34
 * class Item extends \yii\db\ActiveRecord
35
 * {
36
 *     public function behaviors()
37
 *     {
38
 *         return [
39
 *             'typecast' => [
40
 *                 'class' => AttributeTypecastBehavior::className(),
41
 *                 'attributeTypes' => [
42
 *                     'amount' => AttributeTypecastBehavior::TYPE_INTEGER,
43
 *                     'price' => AttributeTypecastBehavior::TYPE_FLOAT,
44
 *                     'is_active' => AttributeTypecastBehavior::TYPE_BOOLEAN,
45
 *                 ],
46
 *                 'typecastAfterValidate' => true,
47
 *                 'typecastBeforeSave' => false,
48
 *                 'typecastAfterFind' => false,
49
 *             ],
50
 *         ];
51
 *     }
52
 *
53
 *     // ...
54
 * }
55
 * ```
56
 *
57
 * Tip: you may left [[attributeTypes]] blank - in this case its value will be detected
58
 * automatically based on owner validation rules.
59
 * Following example will automatically create same [[attributeTypes]] value as it was configured at the above one:
60
 *
61
 * ```php
62
 * use yii\behaviors\AttributeTypecastBehavior;
63
 *
64
 * class Item extends \yii\db\ActiveRecord
65
 * {
66
 *
67
 *     public function rules()
68
 *     {
69
 *         return [
70
 *             ['amount', 'integer'],
71
 *             ['price', 'number'],
72
 *             ['is_active', 'boolean'],
73
 *         ];
74
 *     }
75
 *
76
 *     public function behaviors()
77
 *     {
78
 *         return [
79
 *             'typecast' => [
80
 *                 'class' => AttributeTypecastBehavior::className(),
81
 *                 // 'attributeTypes' will be composed automatically according to `rules()`
82
 *             ],
83
 *         ];
84
 *     }
85
 *
86
 *     // ...
87
 * }
88
 * ```
89
 *
90
 * This behavior allows automatic attribute typecasting at following cases:
91
 *
92
 * - after successful model validation
93
 * - before model save (insert or update)
94
 * - after model find (found by query or refreshed)
95
 *
96
 * You may control automatic typecasting for particular case using fields [[typecastAfterValidate]],
97
 * [[typecastBeforeSave]] and [[typecastAfterFind]].
98
 * By default typecasting will be performed only after model validation.
99
 *
100
 * Note: you can manually trigger attribute typecasting anytime invoking [[typecastAttributes()]] method:
101
 *
102
 * ```php
103
 * $model = new Item();
104
 * $model->price = '38.5';
105
 * $model->is_active = 1;
106
 * $model->typecastAttributes();
107
 * ```
108
 *
109
 * @author Paul Klimov <[email protected]>
110
 * @since 2.0.10
111
 */
112
class AttributeTypecastBehavior extends Behavior
113
{
114
    const TYPE_INTEGER = 'integer';
115
    const TYPE_FLOAT = 'float';
116
    const TYPE_BOOLEAN = 'boolean';
117
    const TYPE_STRING = 'string';
118
119
    /**
120
     * @var Model|BaseActiveRecord the owner of this behavior.
121
     */
122
    public $owner;
123
    /**
124
     * @var array attribute typecast map in format: attributeName => type.
125
     * Type can be set via PHP callable, which accept raw value as an argument and should return
126
     * typecast result.
127
     * For example:
128
     *
129
     * ```php
130
     * [
131
     *     'amount' => 'integer',
132
     *     'price' => 'float',
133
     *     'is_active' => 'boolean',
134
     *     'date' => function ($value) {
135
     *         return ($value instanceof \DateTime) ? $value->getTimestamp(): (int)$value;
136
     *     },
137
     * ]
138
     * ```
139
     *
140
     * If not set, attribute type map will be composed automatically from the owner validation rules.
141
     */
142
    public $attributeTypes;
143
    /**
144
     * @var bool whether to skip typecasting of `null` values.
145
     * If enabled attribute value which equals to `null` will not be type-casted (e.g. `null` remains `null`),
146
     * otherwise it will be converted according to the type configured at [[attributeTypes]].
147
     */
148
    public $skipOnNull = true;
149
    /**
150
     * @var bool whether to perform typecasting after owner model validation.
151
     * Note that typecasting will be performed only if validation was successful, e.g.
152
     * owner model has no errors.
153
     * Note that changing this option value will have no effect after this behavior has been attached to the model.
154
     */
155
    public $typecastAfterValidate = true;
156
    /**
157
     * @var bool whether to perform typecasting before saving owner model (insert or update).
158
     * This option may be disabled in order to achieve better performance.
159
     * For example, in case of [[\yii\db\ActiveRecord]] usage, typecasting before save
160
     * will grant no benefit an thus can be disabled.
161
     * Note that changing this option value will have no effect after this behavior has been attached to the model.
162
     */
163
    public $typecastBeforeSave = false;
164
    /**
165
     * @var bool whether to perform typecasting after retrieving owner model data from
166
     * the database (after find or refresh).
167
     * This option may be disabled in order to achieve better performance.
168
     * For example, in case of [[\yii\db\ActiveRecord]] usage, typecasting after find
169
     * will grant no benefit in most cases an thus can be disabled.
170
     * Note that changing this option value will have no effect after this behavior has been attached to the model.
171
     */
172
    public $typecastAfterFind = false;
173
174
    /**
175
     * @var array internal static cache for auto detected [[attributeTypes]] values
176
     * in format: ownerClassName => attributeTypes
177
     */
178
    private static $autoDetectedAttributeTypes = [];
179
180
181
    /**
182
     * Clears internal static cache of auto detected [[attributeTypes]] values
183
     * over all affected owner classes.
184
     */
185 5
    public static function clearAutoDetectedAttributeTypes()
186
    {
187 5
        self::$autoDetectedAttributeTypes = [];
188 5
    }
189
190
    /**
191
     * @inheritdoc
192
     */
193 5
    public function attach($owner)
194
    {
195 5
        parent::attach($owner);
196
197 5
        if ($this->attributeTypes === null) {
198 1
            $ownerClass = get_class($this->owner);
199 1
            if (!isset(self::$autoDetectedAttributeTypes[$ownerClass])) {
200 1
                self::$autoDetectedAttributeTypes[$ownerClass] = $this->detectAttributeTypes();
201
            }
202 1
            $this->attributeTypes = self::$autoDetectedAttributeTypes[$ownerClass];
203
        }
204 5
    }
205
206
    /**
207
     * Typecast owner attributes according to [[attributeTypes]].
208
     * @param array $attributeNames list of attribute names that should be type-casted.
209
     * If this parameter is empty, it means any attribute listed in the [[attributeTypes]]
210
     * should be type-casted.
211
     */
212 4
    public function typecastAttributes($attributeNames = null)
213
    {
214 4
        $attributeTypes = [];
215
216 4
        if ($attributeNames === null) {
217 4
            $attributeTypes = $this->attributeTypes;
218
        } else {
219
            foreach ($attributeNames as $attribute) {
220
                if (!isset($this->attributeTypes[$attribute])) {
221
                    throw new InvalidParamException("There is no type mapping for '{$attribute}'.");
222
                }
223
                $attributeTypes[$attribute] = $this->attributeTypes[$attribute];
224
            }
225
        }
226
227 4
        foreach ($attributeTypes as $attribute => $type) {
228 4
            $value = $this->owner->{$attribute};
229 4
            if ($this->skipOnNull && $value === null) {
230 3
                continue;
231
            }
232 4
            $this->owner->{$attribute} = $this->typecastValue($value, $type);
233
        }
234 4
    }
235
236
    /**
237
     * Casts the given value to the specified type.
238
     * @param mixed $value value to be type-casted.
239
     * @param string|callable $type type name or typecast callable.
240
     * @return mixed typecast result.
241
     */
242 4
    protected function typecastValue($value, $type)
243
    {
244 4
        if (is_scalar($type)) {
245 3
            if (is_object($value) && method_exists($value, '__toString')) {
246
                $value = $value->__toString();
247
            }
248
249
            switch ($type) {
250 3
                case self::TYPE_INTEGER:
251 3
                    return (int) $value;
252 3
                case self::TYPE_FLOAT:
253 3
                    return (float) $value;
254 3
                case self::TYPE_BOOLEAN:
255 3
                    return (bool) $value;
256 3
                case self::TYPE_STRING:
257 3
                    if (is_float($value)) {
258
                        return StringHelper::floatToString($value);
259
                    }
260 3
                    return (string) $value;
261
                default:
262
                    throw new InvalidParamException("Unsupported type '{$type}'");
263
            }
264
        }
265
266 4
        return call_user_func($type, $value);
267
    }
268
269
    /**
270
     * Composes default value for [[attributeTypes]] from the owner validation rules.
271
     * @return array attribute type map.
272
     */
273 1
    protected function detectAttributeTypes()
274
    {
275 1
        $attributeTypes = [];
276 1
        foreach ($this->owner->getValidators() as $validator) {
277 1
            $type = null;
278 1
            if ($validator instanceof BooleanValidator) {
279 1
                $type = self::TYPE_BOOLEAN;
280 1
            } elseif ($validator instanceof NumberValidator) {
281 1
                $type = $validator->integerOnly ? self::TYPE_INTEGER : self::TYPE_FLOAT;
282 1
            } elseif ($validator instanceof StringValidator) {
283 1
                $type = self::TYPE_STRING;
284
            }
285
286 1
            if ($type !== null) {
287 1
                foreach ((array) $validator->attributes as $attribute) {
288 1
                    $attributeTypes[$attribute] = $type;
289
                }
290
            }
291
        }
292
293 1
        return $attributeTypes;
294
    }
295
296
    /**
297
     * @inheritdoc
298
     */
299 5
    public function events()
300
    {
301 5
        $events = [];
302
303 5
        if ($this->typecastAfterValidate) {
304 5
            $events[Model::EVENT_AFTER_VALIDATE] = 'afterValidate';
305
        }
306 5
        if ($this->typecastBeforeSave) {
307 4
            $events[BaseActiveRecord::EVENT_BEFORE_INSERT] = 'beforeSave';
308 4
            $events[BaseActiveRecord::EVENT_BEFORE_UPDATE] = 'beforeSave';
309
        }
310 5
        if ($this->typecastAfterFind) {
311 4
            $events[BaseActiveRecord::EVENT_AFTER_FIND] = 'afterFind';
312
        }
313
314 5
        return $events;
315
    }
316
317
    /**
318
     * Handles owner 'afterValidate' event, ensuring attribute typecasting.
319
     * @param \yii\base\Event $event event instance.
320
     */
321 1
    public function afterValidate($event)
0 ignored issues
show
Unused Code introduced by
The parameter $event is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
322
    {
323 1
        if (!$this->owner->hasErrors()) {
324 1
            $this->typecastAttributes();
325
        }
326 1
    }
327
328
    /**
329
     * Handles owner 'afterInsert' and 'afterUpdate' events, ensuring attribute typecasting.
330
     * @param \yii\base\Event $event event instance.
331
     */
332 2
    public function beforeSave($event)
0 ignored issues
show
Unused Code introduced by
The parameter $event is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
333
    {
334 2
        $this->typecastAttributes();
335 2
    }
336
337
    /**
338
     * Handles owner 'afterFind' event, ensuring attribute typecasting.
339
     * @param \yii\base\Event $event event instance.
340
     */
341 2
    public function afterFind($event)
0 ignored issues
show
Unused Code introduced by
The parameter $event is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
342
    {
343 2
        $this->typecastAttributes();
344 2
    }
345
}
346