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\InvalidArgumentException; |
||||
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::class, |
||||
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::class, |
||||
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 saving owner model (insert or update). |
||||
166 | * This option may be disabled in order to achieve better performance. |
||||
167 | * For example, in case of [[\yii\db\ActiveRecord]] usage, typecasting after save |
||||
168 | * will grant no benefit an thus can be disabled. |
||||
169 | * Note that changing this option value will have no effect after this behavior has been attached to the model. |
||||
170 | * @since 2.0.14 |
||||
171 | */ |
||||
172 | public $typecastAfterSave = false; |
||||
173 | /** |
||||
174 | * @var bool whether to perform typecasting after retrieving owner model data from |
||||
175 | * the database (after find or refresh). |
||||
176 | * This option may be disabled in order to achieve better performance. |
||||
177 | * For example, in case of [[\yii\db\ActiveRecord]] usage, typecasting after find |
||||
178 | * will grant no benefit in most cases an thus can be disabled. |
||||
179 | * Note that changing this option value will have no effect after this behavior has been attached to the model. |
||||
180 | */ |
||||
181 | public $typecastAfterFind = false; |
||||
182 | |||||
183 | /** |
||||
184 | * @var array internal static cache for auto detected [[attributeTypes]] values |
||||
185 | * in format: ownerClassName => attributeTypes |
||||
186 | */ |
||||
187 | private static $autoDetectedAttributeTypes = []; |
||||
188 | |||||
189 | |||||
190 | /** |
||||
191 | * Clears internal static cache of auto detected [[attributeTypes]] values |
||||
192 | * over all affected owner classes. |
||||
193 | */ |
||||
194 | 8 | public static function clearAutoDetectedAttributeTypes() |
|||
195 | { |
||||
196 | 8 | self::$autoDetectedAttributeTypes = []; |
|||
197 | 8 | } |
|||
198 | |||||
199 | /** |
||||
200 | * {@inheritdoc} |
||||
201 | */ |
||||
202 | 208 | public function attach($owner) |
|||
203 | { |
||||
204 | 208 | parent::attach($owner); |
|||
205 | |||||
206 | 208 | if ($this->attributeTypes === null) { |
|||
207 | 1 | $ownerClass = get_class($this->owner); |
|||
208 | 1 | if (!isset(self::$autoDetectedAttributeTypes[$ownerClass])) { |
|||
209 | 1 | self::$autoDetectedAttributeTypes[$ownerClass] = $this->detectAttributeTypes(); |
|||
210 | } |
||||
211 | 1 | $this->attributeTypes = self::$autoDetectedAttributeTypes[$ownerClass]; |
|||
212 | } |
||||
213 | 208 | } |
|||
214 | |||||
215 | /** |
||||
216 | * Typecast owner attributes according to [[attributeTypes]]. |
||||
217 | * @param array $attributeNames list of attribute names that should be type-casted. |
||||
218 | * If this parameter is empty, it means any attribute listed in the [[attributeTypes]] |
||||
219 | * should be type-casted. |
||||
220 | */ |
||||
221 | 197 | public function typecastAttributes($attributeNames = null) |
|||
222 | { |
||||
223 | 197 | $attributeTypes = []; |
|||
224 | |||||
225 | 197 | if ($attributeNames === null) { |
|||
226 | 197 | $attributeTypes = $this->attributeTypes; |
|||
227 | } else { |
||||
228 | foreach ($attributeNames as $attribute) { |
||||
229 | if (!isset($this->attributeTypes[$attribute])) { |
||||
230 | throw new InvalidArgumentException("There is no type mapping for '{$attribute}'."); |
||||
231 | } |
||||
232 | $attributeTypes[$attribute] = $this->attributeTypes[$attribute]; |
||||
233 | } |
||||
234 | } |
||||
235 | |||||
236 | 197 | foreach ($attributeTypes as $attribute => $type) { |
|||
237 | 197 | $value = $this->owner->{$attribute}; |
|||
238 | 197 | if ($this->skipOnNull && $value === null) { |
|||
239 | 6 | continue; |
|||
240 | } |
||||
241 | 197 | $this->owner->{$attribute} = $this->typecastValue($value, $type); |
|||
242 | } |
||||
243 | 197 | } |
|||
244 | |||||
245 | /** |
||||
246 | * Casts the given value to the specified type. |
||||
247 | * @param mixed $value value to be type-casted. |
||||
248 | * @param string|callable $type type name or typecast callable. |
||||
249 | * @return mixed typecast result. |
||||
250 | */ |
||||
251 | 197 | protected function typecastValue($value, $type) |
|||
252 | { |
||||
253 | 197 | if (is_scalar($type)) { |
|||
254 | 193 | if (is_object($value) && method_exists($value, '__toString')) { |
|||
255 | $value = $value->__toString(); |
||||
256 | } |
||||
257 | |||||
258 | switch ($type) { |
||||
259 | 193 | case self::TYPE_INTEGER: |
|||
260 | 3 | return (int) $value; |
|||
261 | 193 | case self::TYPE_FLOAT: |
|||
262 | 3 | return (float) $value; |
|||
263 | 193 | case self::TYPE_BOOLEAN: |
|||
264 | 3 | return (bool) $value; |
|||
265 | 193 | case self::TYPE_STRING: |
|||
266 | 193 | if (is_float($value)) { |
|||
267 | return StringHelper::floatToString($value); |
||||
268 | } |
||||
269 | 193 | return (string) $value; |
|||
270 | default: |
||||
271 | throw new InvalidArgumentException("Unsupported type '{$type}'"); |
||||
272 | } |
||||
273 | } |
||||
274 | |||||
275 | 7 | return call_user_func($type, $value); |
|||
276 | } |
||||
277 | |||||
278 | /** |
||||
279 | * Composes default value for [[attributeTypes]] from the owner validation rules. |
||||
280 | * @return array attribute type map. |
||||
281 | */ |
||||
282 | 1 | protected function detectAttributeTypes() |
|||
283 | { |
||||
284 | 1 | $attributeTypes = []; |
|||
285 | 1 | foreach ($this->owner->getValidators() as $validator) { |
|||
286 | 1 | $type = null; |
|||
287 | 1 | if ($validator instanceof BooleanValidator) { |
|||
288 | 1 | $type = self::TYPE_BOOLEAN; |
|||
289 | 1 | } elseif ($validator instanceof NumberValidator) { |
|||
290 | 1 | $type = $validator->integerOnly ? self::TYPE_INTEGER : self::TYPE_FLOAT; |
|||
291 | 1 | } elseif ($validator instanceof StringValidator) { |
|||
292 | 1 | $type = self::TYPE_STRING; |
|||
293 | } |
||||
294 | |||||
295 | 1 | if ($type !== null) { |
|||
296 | 1 | foreach ((array) $validator->attributes as $attribute) { |
|||
297 | 1 | $attributeTypes[ltrim($attribute, '!')] = $type; |
|||
298 | } |
||||
299 | } |
||||
300 | } |
||||
301 | |||||
302 | 1 | return $attributeTypes; |
|||
303 | } |
||||
304 | |||||
305 | /** |
||||
306 | * {@inheritdoc} |
||||
307 | */ |
||||
308 | 208 | public function events() |
|||
309 | { |
||||
310 | 208 | $events = []; |
|||
311 | |||||
312 | 208 | if ($this->typecastAfterValidate) { |
|||
313 | 8 | $events[Model::EVENT_AFTER_VALIDATE] = 'afterValidate'; |
|||
314 | } |
||||
315 | 208 | if ($this->typecastBeforeSave) { |
|||
316 | 7 | $events[BaseActiveRecord::EVENT_BEFORE_INSERT] = 'beforeSave'; |
|||
317 | 7 | $events[BaseActiveRecord::EVENT_BEFORE_UPDATE] = 'beforeSave'; |
|||
318 | } |
||||
319 | 208 | if ($this->typecastAfterSave) { |
|||
320 | 1 | $events[BaseActiveRecord::EVENT_AFTER_INSERT] = 'afterSave'; |
|||
321 | 1 | $events[BaseActiveRecord::EVENT_AFTER_UPDATE] = 'afterSave'; |
|||
322 | } |
||||
323 | 208 | if ($this->typecastAfterFind) { |
|||
324 | 207 | $events[BaseActiveRecord::EVENT_AFTER_FIND] = 'afterFind'; |
|||
325 | } |
||||
326 | |||||
327 | 208 | return $events; |
|||
328 | } |
||||
329 | |||||
330 | /** |
||||
331 | * Handles owner 'afterValidate' event, ensuring attribute typecasting. |
||||
332 | * @param \yii\base\Event $event event instance. |
||||
333 | */ |
||||
334 | 2 | public function afterValidate($event) |
|||
0 ignored issues
–
show
|
|||||
335 | { |
||||
336 | 2 | if (!$this->owner->hasErrors()) { |
|||
337 | 2 | $this->typecastAttributes(); |
|||
338 | } |
||||
339 | 2 | } |
|||
340 | |||||
341 | /** |
||||
342 | * Handles owner 'beforeInsert' and 'beforeUpdate' events, ensuring attribute typecasting. |
||||
343 | * @param \yii\base\Event $event event instance. |
||||
344 | */ |
||||
345 | 4 | public function beforeSave($event) |
|||
0 ignored issues
–
show
The parameter
$event is not used and could be removed.
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
This check looks for parameters that have been defined for a function or method, but which are not used in the method body.
Loading history...
|
|||||
346 | { |
||||
347 | 4 | $this->typecastAttributes(); |
|||
348 | 4 | } |
|||
349 | |||||
350 | /** |
||||
351 | * Handles owner 'afterInsert' and 'afterUpdate' events, ensuring attribute typecasting. |
||||
352 | * @param \yii\base\Event $event event instance. |
||||
353 | * @since 2.0.14 |
||||
354 | */ |
||||
355 | 1 | public function afterSave($event) |
|||
0 ignored issues
–
show
The parameter
$event is not used and could be removed.
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
This check looks for parameters that have been defined for a function or method, but which are not used in the method body.
Loading history...
|
|||||
356 | { |
||||
357 | 1 | $this->typecastAttributes(); |
|||
358 | 1 | } |
|||
359 | |||||
360 | /** |
||||
361 | * Handles owner 'afterFind' event, ensuring attribute typecasting. |
||||
362 | * @param \yii\base\Event $event event instance. |
||||
363 | */ |
||||
364 | 192 | public function afterFind($event) |
|||
0 ignored issues
–
show
The parameter
$event is not used and could be removed.
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
This check looks for parameters that have been defined for a function or method, but which are not used in the method body.
Loading history...
|
|||||
365 | { |
||||
366 | 192 | $this->typecastAttributes(); |
|||
367 | 192 | } |
|||
368 | } |
||||
369 |
This check looks for parameters that have been defined for a function or method, but which are not used in the method body.