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) |
|
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) |
|
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
|
|||
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) |
|
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.