Model.php 31.4 KB
Newer Older
w  
Qiang Xue committed
1 2 3
<?php
/**
 * @link http://www.yiiframework.com/
Qiang Xue committed
4
 * @copyright Copyright (c) 2008 Yii Software LLC
w  
Qiang Xue committed
5 6 7
 * @license http://www.yiiframework.com/license/
 */

w  
Qiang Xue committed
8
namespace yii\base;
w  
Qiang Xue committed
9

10
use Yii;
11
use ArrayAccess;
12 13
use ArrayObject;
use ArrayIterator;
14 15
use ReflectionClass;
use IteratorAggregate;
Qiang Xue committed
16
use yii\helpers\ArrayHelper;
17
use yii\helpers\Inflector;
18
use yii\validators\RequiredValidator;
19
use yii\validators\Validator;
Qiang Xue committed
20 21
use yii\web\Link;
use yii\web\Linkable;
Qiang Xue committed
22

w  
Qiang Xue committed
23
/**
w  
Qiang Xue committed
24
 * Model is the base class for data models.
w  
Qiang Xue committed
25
 *
w  
Qiang Xue committed
26 27 28 29 30 31 32 33
 * Model implements the following commonly used features:
 *
 * - attribute declaration: by default, every public class member is considered as
 *   a model attribute
 * - attribute labels: each attribute may be associated with a label for display purpose
 * - massive attribute assignment
 * - scenario-based validation
 *
Qiang Xue committed
34
 * Model also raises the following events when performing data validation:
w  
Qiang Xue committed
35
 *
Qiang Xue committed
36 37
 * - [[EVENT_BEFORE_VALIDATE]]: an event raised at the beginning of [[validate()]]
 * - [[EVENT_AFTER_VALIDATE]]: an event raised at the end of [[validate()]]
w  
Qiang Xue committed
38 39 40
 *
 * You may directly use Model to store model data, or extend it with customization.
 * You may also customize Model by attaching [[ModelBehavior|model behaviors]].
w  
Qiang Xue committed
41
 *
42 43
 * @property \yii\validators\Validator[] $activeValidators The validators applicable to the current
 * [[scenario]]. This property is read-only.
resurtm committed
44
 * @property array $attributes Attribute values (name => value).
45 46 47 48 49 50
 * @property array $errors An array of errors for all attributes. Empty array is returned if no error. The
 * result is a two-dimensional array. See [[getErrors()]] for detailed description. This property is read-only.
 * @property array $firstErrors The first errors. An empty array will be returned if there is no error. This
 * property is read-only.
 * @property ArrayIterator $iterator An iterator for traversing the items in the list. This property is
 * read-only.
51
 * @property string $scenario The scenario that this model is in. Defaults to [[SCENARIO_DEFAULT]].
52 53
 * @property ArrayObject|\yii\validators\Validator[] $validators All the validators declared in the model.
 * This property is read-only.
Qiang Xue committed
54
 *
w  
Qiang Xue committed
55
 * @author Qiang Xue <qiang.xue@gmail.com>
w  
Qiang Xue committed
56
 * @since 2.0
w  
Qiang Xue committed
57
 */
58
class Model extends Component implements IteratorAggregate, ArrayAccess, Arrayable
w  
Qiang Xue committed
59
{
Qiang Xue committed
60 61
	use ArrayableTrait;

62 63 64
	/**
	 * The name of the default scenario.
	 */
65
	const SCENARIO_DEFAULT = 'default';
66 67 68 69 70 71 72 73 74 75
	/**
	 * @event ModelEvent an event raised at the beginning of [[validate()]]. You may set
	 * [[ModelEvent::isValid]] to be false to stop the validation.
	 */
	const EVENT_BEFORE_VALIDATE = 'beforeValidate';
	/**
	 * @event Event an event raised at the end of [[validate()]]
	 */
	const EVENT_AFTER_VALIDATE = 'afterValidate';

Qiang Xue committed
76 77 78 79 80
	/**
	 * @var array validation errors (attribute name => array of errors)
	 */
	private $_errors;
	/**
81
	 * @var ArrayObject list of validators
Qiang Xue committed
82 83 84 85 86
	 */
	private $_validators;
	/**
	 * @var string current scenario
	 */
87
	private $_scenario = self::SCENARIO_DEFAULT;
w  
Qiang Xue committed
88 89 90 91

	/**
	 * Returns the validation rules for attributes.
	 *
Qiang Xue committed
92
	 * Validation rules are used by [[validate()]] to check if attribute values are valid.
w  
Qiang Xue committed
93 94
	 * Child classes may override this method to declare different validation rules.
	 *
w  
Qiang Xue committed
95
	 * Each rule is an array with the following structure:
w  
Qiang Xue committed
96
	 *
w  
Qiang Xue committed
97
	 * ~~~
Alexander Makarov committed
98
	 * [
99
	 *     ['attribute1', 'attribute2'],
Qiang Xue committed
100
	 *     'validator type',
101
	 *     'on' => ['scenario1', 'scenario2'],
Qiang Xue committed
102
	 *     ...other parameters...
Alexander Makarov committed
103
	 * ]
w  
Qiang Xue committed
104 105
	 * ~~~
	 *
w  
Qiang Xue committed
106
	 * where
w  
Qiang Xue committed
107
	 *
slavcodev committed
108
	 *  - attribute list: required, specifies the attributes array to be validated, for single attribute you can pass string;
109 110
	 *  - validator type: required, specifies the validator to be used. It can be a built-in validator name,
	 *    a method name of the model class, an anonymous function, or a validator class name.
111
	 *  - on: optional, specifies the [[scenario|scenarios]] array when the validation
Qiang Xue committed
112
	 *    rule can be applied. If this option is not set, the rule will apply to all scenarios.
w  
Qiang Xue committed
113
	 *  - additional name-value pairs can be specified to initialize the corresponding validator properties.
Qiang Xue committed
114
	 *    Please refer to individual validator class API for possible properties.
w  
Qiang Xue committed
115
	 *
Qiang Xue committed
116 117
	 * A validator can be either an object of a class extending [[Validator]], or a model class method
	 * (called *inline validator*) that has the following signature:
w  
Qiang Xue committed
118
	 *
w  
Qiang Xue committed
119
	 * ~~~
w  
Qiang Xue committed
120
	 * // $params refers to validation parameters given in the rule
w  
Qiang Xue committed
121 122 123
	 * function validatorName($attribute, $params)
	 * ~~~
	 *
124
	 * In the above `$attribute` refers to currently validated attribute name while `$params` contains an array of
Alexander Makarov committed
125
	 * validator configuration options such as `max` in case of `string` validator. Currently validate attribute value
126 127
	 * can be accessed as `$this->[$attribute]`.
	 *
Qiang Xue committed
128
	 * Yii also provides a set of [[Validator::builtInValidators|built-in validators]].
Qiang Xue committed
129
	 * They each has an alias name which can be used when specifying a validation rule.
w  
Qiang Xue committed
130
	 *
Qiang Xue committed
131
	 * Below are some examples:
w  
Qiang Xue committed
132
	 *
w  
Qiang Xue committed
133
	 * ~~~
Alexander Makarov committed
134
	 * [
Qiang Xue committed
135
	 *     // built-in "required" validator
slavcodev committed
136
	 *     [['username', 'password'], 'required'],
Alexander Makarov committed
137
	 *     // built-in "string" validator customized with "min" and "max" properties
Alexander Makarov committed
138
	 *     ['username', 'string', 'min' => 3, 'max' => 12],
Qiang Xue committed
139
	 *     // built-in "compare" validator that is used in "register" scenario only
Alexander Makarov committed
140
	 *     ['password', 'compare', 'compareAttribute' => 'password2', 'on' => 'register'],
Qiang Xue committed
141
	 *     // an inline validator defined via the "authenticate()" method in the model class
Alexander Makarov committed
142
	 *     ['password', 'authenticate', 'on' => 'login'],
Qiang Xue committed
143
	 *     // a validator of class "DateRangeValidator"
Alexander Makarov committed
144 145
	 *     ['dateRange', 'DateRangeValidator'],
	 * ];
w  
Qiang Xue committed
146
	 * ~~~
w  
Qiang Xue committed
147 148
	 *
	 * Note, in order to inherit rules defined in the parent class, a child class needs to
w  
Qiang Xue committed
149
	 * merge the parent rules with child rules using functions such as `array_merge()`.
w  
Qiang Xue committed
150
	 *
w  
Qiang Xue committed
151
	 * @return array validation rules
152
	 * @see scenarios()
w  
Qiang Xue committed
153 154 155
	 */
	public function rules()
	{
Alexander Makarov committed
156
		return [];
w  
Qiang Xue committed
157 158
	}

159
	/**
160
	 * Returns a list of scenarios and the corresponding active attributes.
Qiang Xue committed
161
	 * An active attribute is one that is subject to validation in the current scenario.
162 163 164
	 * The returned array should be in the following format:
	 *
	 * ~~~
Alexander Makarov committed
165 166 167
	 * [
	 *     'scenario1' => ['attribute11', 'attribute12', ...],
	 *     'scenario2' => ['attribute21', 'attribute22', ...],
168
	 *     ...
Alexander Makarov committed
169
	 * ]
170 171
	 * ~~~
	 *
172
	 * By default, an active attribute is considered safe and can be massively assigned.
173
	 * If an attribute should NOT be massively assigned (thus considered unsafe),
Qiang Xue committed
174
	 * please prefix the attribute with an exclamation character (e.g. '!rank').
175
	 *
Qiang Xue committed
176
	 * The default implementation of this method will return all scenarios found in the [[rules()]]
177
	 * declaration. A special scenario named [[SCENARIO_DEFAULT]] will contain all attributes
178 179
	 * found in the [[rules()]]. Each scenario will be associated with the attributes that
	 * are being validated by the validation rules that apply to the scenario.
Qiang Xue committed
180 181
	 *
	 * @return array a list of scenarios and the corresponding active attributes.
182 183 184
	 */
	public function scenarios()
	{
185
		$scenarios = [self::SCENARIO_DEFAULT => []];
186
		foreach ($this->getValidators() as $validator) {
187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209
			foreach ($validator->on as $scenario) {
				$scenarios[$scenario] = [];
			}
			foreach ($validator->except as $scenario) {
				$scenarios[$scenario] = [];
			}
		}
		$names = array_keys($scenarios);

		foreach ($this->getValidators() as $validator) {
			if (empty($validator->on) && empty($validator->except)) {
				foreach ($names as $name) {
					foreach ($validator->attributes as $attribute) {
						$scenarios[$name][$attribute] = true;
					}
				}
			} elseif (empty($validator->on)) {
				foreach ($names as $name) {
					if (!in_array($name, $validator->except, true)) {
						foreach ($validator->attributes as $attribute) {
							$scenarios[$name][$attribute] = true;
						}
					}
210 211
				}
			} else {
212
				foreach ($validator->on as $name) {
213
					foreach ($validator->attributes as $attribute) {
214
						$scenarios[$name][$attribute] = true;
215 216 217 218
					}
				}
			}
		}
219

220
		foreach ($scenarios as $scenario => $attributes) {
221
			if (empty($attributes) && $scenario !== self::SCENARIO_DEFAULT) {
222 223 224
				unset($scenarios[$scenario]);
			} else {
				$scenarios[$scenario] = array_keys($attributes);
Qiang Xue committed
225 226
			}
		}
Qiang Xue committed
227

228
		return $scenarios;
229 230
	}

231 232 233 234 235 236 237 238 239 240 241 242 243 244 245
	/**
	 * Returns the form name that this model class should use.
	 *
	 * The form name is mainly used by [[\yii\web\ActiveForm]] to determine how to name
	 * the input fields for the attributes in a model. If the form name is "A" and an attribute
	 * name is "b", then the corresponding input name would be "A[b]". If the form name is
	 * an empty string, then the input name would be "b".
	 *
	 * By default, this method returns the model class name (without the namespace part)
	 * as the form name. You may override it when the model is used in different forms.
	 *
	 * @return string the form name of this model class.
	 */
	public function formName()
	{
246
		$reflector = new ReflectionClass($this);
247
		return $reflector->getShortName();
248 249
	}

250 251 252 253 254 255
	/**
	 * Returns the list of attribute names.
	 * By default, this method returns all public non-static properties of the class.
	 * You may override this method to change the default behavior.
	 * @return array list of attribute names.
	 */
256
	public function attributes()
257
	{
258
		$class = new ReflectionClass($this);
Alexander Makarov committed
259
		$names = [];
260 261
		foreach ($class->getProperties(\ReflectionProperty::IS_PUBLIC) as $property) {
			if (!$property->isStatic()) {
262
				$names[] = $property->getName();
263 264
			}
		}
Qiang Xue committed
265
		return $names;
266 267
	}

w  
Qiang Xue committed
268 269
	/**
	 * Returns the attribute labels.
w  
Qiang Xue committed
270 271 272 273 274
	 *
	 * Attribute labels are mainly used for display purpose. For example, given an attribute
	 * `firstName`, we can declare a label `First Name` which is more user-friendly and can
	 * be displayed to end users.
	 *
Qiang Xue committed
275
	 * By default an attribute label is generated using [[generateAttributeLabel()]].
w  
Qiang Xue committed
276 277 278
	 * This method allows you to explicitly specify attribute labels.
	 *
	 * Note, in order to inherit labels defined in the parent class, a child class needs to
w  
Qiang Xue committed
279
	 * merge the parent labels with child labels using functions such as `array_merge()`.
w  
Qiang Xue committed
280
	 *
resurtm committed
281
	 * @return array attribute labels (name => label)
282
	 * @see generateAttributeLabel()
w  
Qiang Xue committed
283 284 285
	 */
	public function attributeLabels()
	{
Alexander Makarov committed
286
		return [];
w  
Qiang Xue committed
287 288 289
	}

	/**
w  
Qiang Xue committed
290
	 * Performs the data validation.
w  
Qiang Xue committed
291
	 *
292 293 294 295 296
	 * This method executes the validation rules applicable to the current [[scenario]].
	 * The following criteria are used to determine whether a rule is currently applicable:
	 *
	 * - the rule must be associated with the attributes relevant to the current scenario;
	 * - the rules must be effective for the current scenario.
w  
Qiang Xue committed
297
	 *
Qiang Xue committed
298
	 * This method will call [[beforeValidate()]] and [[afterValidate()]] before and
299 300
	 * after the actual validation, respectively. If [[beforeValidate()]] returns false,
	 * the validation will be cancelled and [[afterValidate()]] will not be called.
w  
Qiang Xue committed
301
	 *
302 303
	 * Errors found during the validation can be retrieved via [[getErrors()]],
	 * [[getFirstErrors()]] and [[getFirstError()]].
w  
Qiang Xue committed
304
	 *
w  
Qiang Xue committed
305 306 307
	 * @param array $attributes list of attributes that should be validated.
	 * If this parameter is empty, it means any attribute listed in the applicable
	 * validation rules should be validated.
Qiang Xue committed
308
	 * @param boolean $clearErrors whether to call [[clearErrors()]] before performing validation
w  
Qiang Xue committed
309
	 * @return boolean whether the validation is successful without any error.
310
	 * @throws InvalidParamException if the current scenario is unknown.
w  
Qiang Xue committed
311
	 */
w  
Qiang Xue committed
312
	public function validate($attributes = null, $clearErrors = true)
w  
Qiang Xue committed
313
	{
314 315 316 317 318 319
		$scenarios = $this->scenarios();
		$scenario = $this->getScenario();
		if (!isset($scenarios[$scenario])) {
			throw new InvalidParamException("Unknown scenario: $scenario");
		}

w  
Qiang Xue committed
320
		if ($clearErrors) {
w  
Qiang Xue committed
321
			$this->clearErrors();
w  
Qiang Xue committed
322
		}
323 324 325
		if ($attributes === null) {
			$attributes = $this->activeAttributes();
		}
w  
Qiang Xue committed
326
		if ($this->beforeValidate()) {
w  
Qiang Xue committed
327
			foreach ($this->getActiveValidators() as $validator) {
Qiang Xue committed
328
				$validator->validateAttributes($this, $attributes);
w  
Qiang Xue committed
329
			}
w  
Qiang Xue committed
330 331 332
			$this->afterValidate();
			return !$this->hasErrors();
		}
w  
Qiang Xue committed
333
		return false;
w  
Qiang Xue committed
334 335 336 337
	}

	/**
	 * This method is invoked before validation starts.
Qiang Xue committed
338
	 * The default implementation raises a `beforeValidate` event.
w  
Qiang Xue committed
339 340
	 * You may override this method to do preliminary checks before validation.
	 * Make sure the parent implementation is invoked so that the event can be raised.
341
	 * @return boolean whether the validation should be executed. Defaults to true.
w  
Qiang Xue committed
342 343
	 * If false is returned, the validation will stop and the model is considered invalid.
	 */
w  
Qiang Xue committed
344
	public function beforeValidate()
w  
Qiang Xue committed
345
	{
Qiang Xue committed
346
		$event = new ModelEvent;
347
		$this->trigger(self::EVENT_BEFORE_VALIDATE, $event);
Qiang Xue committed
348
		return $event->isValid;
w  
Qiang Xue committed
349 350 351 352
	}

	/**
	 * This method is invoked after validation ends.
Qiang Xue committed
353
	 * The default implementation raises an `afterValidate` event.
w  
Qiang Xue committed
354 355 356
	 * You may override this method to do postprocessing after validation.
	 * Make sure the parent implementation is invoked so that the event can be raised.
	 */
w  
Qiang Xue committed
357
	public function afterValidate()
w  
Qiang Xue committed
358
	{
359
		$this->trigger(self::EVENT_AFTER_VALIDATE);
w  
Qiang Xue committed
360 361 362
	}

	/**
Qiang Xue committed
363
	 * Returns all the validators declared in [[rules()]].
w  
Qiang Xue committed
364
	 *
Qiang Xue committed
365
	 * This method differs from [[getActiveValidators()]] in that the latter
w  
Qiang Xue committed
366 367
	 * only returns the validators applicable to the current [[scenario]].
	 *
368
	 * Because this method returns an ArrayObject object, you may
w  
Qiang Xue committed
369 370 371
	 * manipulate it by inserting or removing validators (useful in model behaviors).
	 * For example,
	 *
w  
Qiang Xue committed
372
	 * ~~~
373
	 * $model->validators[] = $newValidator;
w  
Qiang Xue committed
374 375
	 * ~~~
	 *
slavcodev committed
376
	 * @return ArrayObject|\yii\validators\Validator[] all the validators declared in the model.
w  
Qiang Xue committed
377
	 */
w  
Qiang Xue committed
378
	public function getValidators()
w  
Qiang Xue committed
379
	{
w  
Qiang Xue committed
380
		if ($this->_validators === null) {
w  
Qiang Xue committed
381
			$this->_validators = $this->createValidators();
w  
Qiang Xue committed
382
		}
w  
Qiang Xue committed
383 384 385 386
		return $this->_validators;
	}

	/**
w  
Qiang Xue committed
387 388
	 * Returns the validators applicable to the current [[scenario]].
	 * @param string $attribute the name of the attribute whose applicable validators should be returned.
w  
Qiang Xue committed
389
	 * If this is null, the validators for ALL attributes in the model will be returned.
Qiang Xue committed
390
	 * @return \yii\validators\Validator[] the validators applicable to the current [[scenario]].
w  
Qiang Xue committed
391
	 */
w  
Qiang Xue committed
392
	public function getActiveValidators($attribute = null)
w  
Qiang Xue committed
393
	{
Alexander Makarov committed
394
		$validators = [];
w  
Qiang Xue committed
395
		$scenario = $this->getScenario();
w  
Qiang Xue committed
396
		foreach ($this->getValidators() as $validator) {
Qiang Xue committed
397
			if ($validator->isActive($scenario) && ($attribute === null || in_array($attribute, $validator->attributes, true))) {
Qiang Xue committed
398
				$validators[] = $validator;
w  
Qiang Xue committed
399 400 401 402 403 404
			}
		}
		return $validators;
	}

	/**
Qiang Xue committed
405 406
	 * Creates validator objects based on the validation rules specified in [[rules()]].
	 * Unlike [[getValidators()]], each time this method is called, a new list of validators will be returned.
407
	 * @return ArrayObject validators
Qiang Xue committed
408
	 * @throws InvalidConfigException if any validation rule configuration is invalid
w  
Qiang Xue committed
409 410 411
	 */
	public function createValidators()
	{
412
		$validators = new ArrayObject;
w  
Qiang Xue committed
413
		foreach ($this->rules() as $rule) {
414
			if ($rule instanceof Validator) {
415
				$validators->append($rule);
416
			} elseif (is_array($rule) && isset($rule[0], $rule[1])) { // attributes, validator type
417
				$validator = Validator::createValidator($rule[1], $this, (array) $rule[0], array_slice($rule, 2));
418
				$validators->append($validator);
Qiang Xue committed
419
			} else {
Qiang Xue committed
420
				throw new InvalidConfigException('Invalid validation rule: a rule must specify both attribute names and validator type.');
w  
Qiang Xue committed
421
			}
w  
Qiang Xue committed
422 423 424 425 426 427 428
		}
		return $validators;
	}

	/**
	 * Returns a value indicating whether the attribute is required.
	 * This is determined by checking if the attribute is associated with a
w  
Qiang Xue committed
429
	 * [[\yii\validators\RequiredValidator|required]] validation rule in the
w  
Qiang Xue committed
430
	 * current [[scenario]].
w  
Qiang Xue committed
431 432 433 434 435
	 * @param string $attribute attribute name
	 * @return boolean whether the attribute is required
	 */
	public function isAttributeRequired($attribute)
	{
w  
Qiang Xue committed
436
		foreach ($this->getActiveValidators($attribute) as $validator) {
437
			if ($validator instanceof RequiredValidator) {
w  
Qiang Xue committed
438
				return true;
w  
Qiang Xue committed
439
			}
w  
Qiang Xue committed
440 441 442 443 444 445 446 447
		}
		return false;
	}

	/**
	 * Returns a value indicating whether the attribute is safe for massive assignments.
	 * @param string $attribute attribute name
	 * @return boolean whether the attribute is safe for massive assignments
448
	 * @see safeAttributes()
w  
Qiang Xue committed
449 450 451
	 */
	public function isAttributeSafe($attribute)
	{
452
		return in_array($attribute, $this->safeAttributes(), true);
w  
Qiang Xue committed
453 454
	}

455 456 457 458 459 460 461 462 463 464 465
	/**
	 * Returns a value indicating whether the attribute is active in the current scenario.
	 * @param string $attribute attribute name
	 * @return boolean whether the attribute is active in the current scenario
	 * @see activeAttributes()
	 */
	public function isAttributeActive($attribute)
	{
		return in_array($attribute, $this->activeAttributes(), true);
	}

w  
Qiang Xue committed
466 467 468 469
	/**
	 * Returns the text label for the specified attribute.
	 * @param string $attribute the attribute name
	 * @return string the attribute label
470 471
	 * @see generateAttributeLabel()
	 * @see attributeLabels()
w  
Qiang Xue committed
472 473 474
	 */
	public function getAttributeLabel($attribute)
	{
w  
Qiang Xue committed
475
		$labels = $this->attributeLabels();
Alex committed
476
		return isset($labels[$attribute]) ? $labels[$attribute] : $this->generateAttributeLabel($attribute);
w  
Qiang Xue committed
477 478 479 480
	}

	/**
	 * Returns a value indicating whether there is any validation error.
481
	 * @param string|null $attribute attribute name. Use null to check all attributes.
w  
Qiang Xue committed
482 483
	 * @return boolean whether there is any error.
	 */
w  
Qiang Xue committed
484
	public function hasErrors($attribute = null)
w  
Qiang Xue committed
485
	{
w  
Qiang Xue committed
486
		return $attribute === null ? !empty($this->_errors) : isset($this->_errors[$attribute]);
w  
Qiang Xue committed
487 488 489 490 491
	}

	/**
	 * Returns the errors for all attribute or a single attribute.
	 * @param string $attribute attribute name. Use null to retrieve errors for all attributes.
492 493
	 * @property array An array of errors for all attributes. Empty array is returned if no error.
	 * The result is a two-dimensional array. See [[getErrors()]] for detailed description.
w  
Qiang Xue committed
494
	 * @return array errors for all attributes or the specified attribute. Empty array is returned if no error.
w  
Qiang Xue committed
495 496
	 * Note that when returning errors for all attributes, the result is a two-dimensional array, like the following:
	 *
w  
Qiang Xue committed
497
	 * ~~~
Alexander Makarov committed
498 499
	 * [
	 *     'username' => [
Qiang Xue committed
500 501
	 *         'Username is required.',
	 *         'Username must contain only word characters.',
Alexander Makarov committed
502 503
	 *     ],
	 *     'email' => [
Qiang Xue committed
504
	 *         'Email address is invalid.',
Alexander Makarov committed
505 506
	 *     ]
	 * ]
w  
Qiang Xue committed
507 508
	 * ~~~
	 *
509 510
	 * @see getFirstErrors()
	 * @see getFirstError()
w  
Qiang Xue committed
511
	 */
w  
Qiang Xue committed
512
	public function getErrors($attribute = null)
w  
Qiang Xue committed
513
	{
w  
Qiang Xue committed
514
		if ($attribute === null) {
Alexander Makarov committed
515
			return $this->_errors === null ? [] : $this->_errors;
Qiang Xue committed
516
		} else {
Alexander Makarov committed
517
			return isset($this->_errors[$attribute]) ? $this->_errors[$attribute] : [];
w  
Qiang Xue committed
518
		}
w  
Qiang Xue committed
519 520
	}

Qiang Xue committed
521 522
	/**
	 * Returns the first error of every attribute in the model.
Qiang Xue committed
523 524
	 * @return array the first errors. The array keys are the attribute names, and the array
	 * values are the corresponding error messages. An empty array will be returned if there is no error.
525 526
	 * @see getErrors()
	 * @see getFirstError()
Qiang Xue committed
527 528 529 530
	 */
	public function getFirstErrors()
	{
		if (empty($this->_errors)) {
Alexander Makarov committed
531
			return [];
Qiang Xue committed
532
		} else {
Alexander Makarov committed
533
			$errors = [];
Qiang Xue committed
534 535 536
			foreach ($this->_errors as $name => $es) {
				if (!empty($es)) {
					$errors[$name] = reset($es);
Qiang Xue committed
537 538
				}
			}
Qiang Xue committed
539
			return $errors;
Qiang Xue committed
540 541 542
		}
	}

w  
Qiang Xue committed
543 544 545 546
	/**
	 * Returns the first error of the specified attribute.
	 * @param string $attribute attribute name.
	 * @return string the error message. Null is returned if no error.
547 548
	 * @see getErrors()
	 * @see getFirstErrors()
w  
Qiang Xue committed
549
	 */
Qiang Xue committed
550
	public function getFirstError($attribute)
w  
Qiang Xue committed
551 552 553 554 555 556 557 558 559
	{
		return isset($this->_errors[$attribute]) ? reset($this->_errors[$attribute]) : null;
	}

	/**
	 * Adds a new error to the specified attribute.
	 * @param string $attribute attribute name
	 * @param string $error new error message
	 */
560
	public function addError($attribute, $error = '')
w  
Qiang Xue committed
561
	{
w  
Qiang Xue committed
562
		$this->_errors[$attribute][] = $error;
w  
Qiang Xue committed
563 564 565 566 567 568
	}

	/**
	 * Removes errors for all attributes or a single attribute.
	 * @param string $attribute attribute name. Use null to remove errors for all attribute.
	 */
w  
Qiang Xue committed
569
	public function clearErrors($attribute = null)
w  
Qiang Xue committed
570
	{
w  
Qiang Xue committed
571
		if ($attribute === null) {
Alexander Makarov committed
572
			$this->_errors = [];
Qiang Xue committed
573
		} else {
w  
Qiang Xue committed
574
			unset($this->_errors[$attribute]);
w  
Qiang Xue committed
575
		}
w  
Qiang Xue committed
576 577 578
	}

	/**
w  
Qiang Xue committed
579 580
	 * Generates a user friendly attribute label based on the give attribute name.
	 * This is done by replacing underscores, dashes and dots with blanks and
w  
Qiang Xue committed
581
	 * changing the first letter of each word to upper case.
w  
Qiang Xue committed
582
	 * For example, 'department_name' or 'DepartmentName' will generate 'Department Name'.
w  
Qiang Xue committed
583 584 585 586 587
	 * @param string $name the column name
	 * @return string the attribute label
	 */
	public function generateAttributeLabel($name)
	{
588
		return Inflector::camel2words($name, true);
w  
Qiang Xue committed
589 590 591
	}

	/**
w  
Qiang Xue committed
592
	 * Returns attribute values.
w  
Qiang Xue committed
593
	 * @param array $names list of attributes whose value needs to be returned.
594
	 * Defaults to null, meaning all attributes listed in [[attributes()]] will be returned.
w  
Qiang Xue committed
595
	 * If it is an array, only the attributes in the array will be returned.
596
	 * @param array $except list of attributes whose value should NOT be returned.
resurtm committed
597
	 * @return array attribute values (name => value).
w  
Qiang Xue committed
598
	 */
Alexander Makarov committed
599
	public function getAttributes($names = null, $except = [])
w  
Qiang Xue committed
600
	{
Alexander Makarov committed
601
		$values = [];
602 603 604 605 606 607 608 609
		if ($names === null) {
			$names = $this->attributes();
		}
		foreach ($names as $name) {
			$values[$name] = $this->$name;
		}
		foreach ($except as $name) {
			unset($values[$name]);
w  
Qiang Xue committed
610 611 612
		}

		return $values;
w  
Qiang Xue committed
613 614 615 616
	}

	/**
	 * Sets the attribute values in a massive way.
resurtm committed
617
	 * @param array $values attribute values (name => value) to be assigned to the model.
w  
Qiang Xue committed
618
	 * @param boolean $safeOnly whether the assignments should only be done to the safe attributes.
w  
Qiang Xue committed
619
	 * A safe attribute is one that is associated with a validation rule in the current [[scenario]].
620 621
	 * @see safeAttributes()
	 * @see attributes()
w  
Qiang Xue committed
622
	 */
w  
Qiang Xue committed
623
	public function setAttributes($values, $safeOnly = true)
w  
Qiang Xue committed
624
	{
w  
Qiang Xue committed
625
		if (is_array($values)) {
626
			$attributes = array_flip($safeOnly ? $this->safeAttributes() : $this->attributes());
w  
Qiang Xue committed
627 628 629
			foreach ($values as $name => $value) {
				if (isset($attributes[$name])) {
					$this->$name = $value;
Qiang Xue committed
630
				} elseif ($safeOnly) {
w  
Qiang Xue committed
631 632 633
					$this->onUnsafeAttribute($name, $value);
				}
			}
w  
Qiang Xue committed
634 635 636 637 638 639 640 641 642 643
		}
	}

	/**
	 * This method is invoked when an unsafe attribute is being massively assigned.
	 * The default implementation will log a warning message if YII_DEBUG is on.
	 * It does nothing otherwise.
	 * @param string $name the unsafe attribute name
	 * @param mixed $value the attribute value
	 */
w  
Qiang Xue committed
644
	public function onUnsafeAttribute($name, $value)
w  
Qiang Xue committed
645
	{
w  
Qiang Xue committed
646
		if (YII_DEBUG) {
Qiang Xue committed
647
			Yii::trace("Failed to set unsafe attribute '$name' in '" . get_class($this) . "'.", __METHOD__);
w  
Qiang Xue committed
648
		}
w  
Qiang Xue committed
649 650 651 652 653 654 655 656
	}

	/**
	 * Returns the scenario that this model is used in.
	 *
	 * Scenario affects how validation is performed and which attributes can
	 * be massively assigned.
	 *
657
	 * @return string the scenario that this model is in. Defaults to [[SCENARIO_DEFAULT]].
w  
Qiang Xue committed
658 659 660 661 662 663 664 665
	 */
	public function getScenario()
	{
		return $this->_scenario;
	}

	/**
	 * Sets the scenario for the model.
666 667
	 * Note that this method does not check if the scenario exists or not.
	 * The method [[validate()]] will perform this check.
w  
Qiang Xue committed
668 669 670 671
	 * @param string $value the scenario that this model is in.
	 */
	public function setScenario($value)
	{
w  
Qiang Xue committed
672
		$this->_scenario = $value;
w  
Qiang Xue committed
673 674 675
	}

	/**
676
	 * Returns the attribute names that are safe to be massively assigned in the current scenario.
677
	 * @return string[] safe attribute names
w  
Qiang Xue committed
678
	 */
679
	public function safeAttributes()
w  
Qiang Xue committed
680
	{
681
		$scenario = $this->getScenario();
682
		$scenarios = $this->scenarios();
683
		if (!isset($scenarios[$scenario])) {
Alexander Makarov committed
684
			return [];
685
		}
Alexander Makarov committed
686
		$attributes = [];
687 688 689
		foreach ($scenarios[$scenario] as $attribute) {
			if ($attribute[0] !== '!') {
				$attributes[] = $attribute;
w  
Qiang Xue committed
690 691
			}
		}
Qiang Xue committed
692
		return $attributes;
693
	}
w  
Qiang Xue committed
694

695 696
	/**
	 * Returns the attribute names that are subject to validation in the current scenario.
697
	 * @return string[] safe attribute names
698 699 700
	 */
	public function activeAttributes()
	{
701
		$scenario = $this->getScenario();
702
		$scenarios = $this->scenarios();
703
		if (!isset($scenarios[$scenario])) {
Alexander Makarov committed
704
			return [];
w  
Qiang Xue committed
705
		}
706
		$attributes = $scenarios[$scenario];
707 708 709 710 711 712
		foreach ($attributes as $i => $attribute) {
			if ($attribute[0] === '!') {
				$attributes[$i] = substr($attribute, 1);
			}
		}
		return $attributes;
w  
Qiang Xue committed
713 714
	}

715 716
	/**
	 * Populates the model with the data from end user.
717 718 719
	 * The data to be loaded is `$data[formName]`, where `formName` refers to the value of [[formName()]].
	 * If [[formName()]] is empty, the whole `$data` array will be used to populate the model.
	 * The data being populated is subject to the safety check by [[setAttributes()]].
720 721
	 * @param array $data the data array. This is usually `$_POST` or `$_GET`, but can also be any valid array
	 * supplied by end user.
722 723
	 * @param string $formName the form name to be used for loading the data into the model.
	 * If not set, [[formName()]] will be used.
724 725
	 * @return boolean whether the model is successfully populated with some data.
	 */
726
	public function load($data, $formName = null)
727
	{
728
		$scope = $formName === null ? $this->formName() : $formName;
729
		if ($scope == '' && !empty($data)) {
730 731 732 733 734 735 736 737 738 739
			$this->setAttributes($data);
			return true;
		} elseif (isset($data[$scope])) {
			$this->setAttributes($data[$scope]);
			return true;
		} else {
			return false;
		}
	}

740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767
	/**
	 * Populates a set of models with the data from end user.
	 * This method is mainly used to collect tabular data input.
	 * The data to be loaded for each model is `$data[formName][index]`, where `formName`
	 * refers to the value of [[formName()]], and `index` the index of the model in the `$models` array.
	 * If [[formName()]] is empty, `$data[index]` will be used to populate each model.
	 * The data being populated to each model is subject to the safety check by [[setAttributes()]].
	 * @param array $models the models to be populated. Note that all models should have the same class.
	 * @param array $data the data array. This is usually `$_POST` or `$_GET`, but can also be any valid array
	 * supplied by end user.
	 * @return boolean whether the model is successfully populated with some data.
	 */
	public static function loadMultiple($models, $data)
	{
		/** @var Model $model */
		$model = reset($models);
		if ($model === false) {
			return false;
		}
		$success = false;
		$scope = $model->formName();
		foreach ($models as $i => $model) {
			if ($scope == '') {
				if (isset($data[$i])) {
					$model->setAttributes($data[$i]);
					$success = true;
				}
			} elseif (isset($data[$scope][$i])) {
768
				$model->setAttributes($data[$scope][$i]);
769 770 771 772 773 774 775 776
				$success = true;
			}
		}
		return $success;
	}

	/**
	 * Validates multiple models.
777 778
	 * This method will validate every model. The models being validated may
	 * be of the same or different types.
779
	 * @param array $models the models to be validated
780 781 782
	 * @param array $attributes list of attributes that should be validated.
	 * If this parameter is empty, it means any attribute listed in the applicable
	 * validation rules should be validated.
783 784 785
	 * @return boolean whether all models are valid. False will be returned if one
	 * or multiple models have validation error.
	 */
786
	public static function validateMultiple($models, $attributes = null)
787 788 789 790
	{
		$valid = true;
		/** @var Model $model */
		foreach ($models as $model) {
791
			$valid = $model->validate($attributes) && $valid;
792 793 794 795
		}
		return $valid;
	}

Qiang Xue committed
796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832
	/**
	 * Returns the list of fields that should be returned by default by [[toArray()]] when no specific fields are specified.
	 *
	 * A field is a named element in the returned array by [[toArray()]].
	 *
	 * This method should return an array of field names or field definitions.
	 * If the former, the field name will be treated as an object property name whose value will be used
	 * as the field value. If the latter, the array key should be the field name while the array value should be
	 * the corresponding field definition which can be either an object property name or a PHP callable
	 * returning the corresponding field value. The signature of the callable should be:
	 *
	 * ```php
	 * function ($field, $model) {
	 *     // return field value
	 * }
	 * ```
	 *
	 * For example, the following code declares four fields:
	 *
	 * - `email`: the field name is the same as the property name `email`;
	 * - `firstName` and `lastName`: the field names are `firstName` and `lastName`, and their
	 *   values are obtained from the `first_name` and `last_name` properties;
	 * - `fullName`: the field name is `fullName`. Its value is obtained by concatenating `first_name`
	 *   and `last_name`.
	 *
	 * ```php
	 * return [
	 *     'email',
	 *     'firstName' => 'first_name',
	 *     'lastName' => 'last_name',
	 *     'fullName' => function () {
	 *         return $this->first_name . ' ' . $this->last_name;
	 *     },
	 * ];
	 * ```
	 *
	 * In this method, you may also want to return different lists of fields based on some context
Qiang Xue committed
833 834
	 * information. For example, depending on [[scenario]] or the privilege of the current application user,
	 * you may return different sets of visible fields or filter out some fields.
Qiang Xue committed
835 836 837 838 839 840 841 842 843 844 845 846 847 848
	 *
	 * The default implementation of this method returns [[attributes()]] indexed by the same attribute names.
	 *
	 * @return array the list of field names or field definitions.
	 * @see toArray()
	 */
	public function fields()
	{
		$fields = $this->attributes();
		return array_combine($fields, $fields);
	}

	/**
	 * Determines which fields can be returned by [[toArray()]].
Qiang Xue committed
849
	 * This method will check the requested fields against those declared in [[fields()]] and [[extraFields()]]
Qiang Xue committed
850 851 852 853 854 855 856
	 * to determine which fields can be returned.
	 * @param array $fields the fields being requested for exporting
	 * @param array $expand the additional fields being requested for exporting
	 * @return array the list of fields to be exported. The array keys are the field names, and the array values
	 * are the corresponding object property names or PHP callables returning the field values.
	 */
	protected function resolveFields(array $fields, array $expand)
Qiang Xue committed
857
	{
Qiang Xue committed
858 859 860 861 862 863 864 865 866 867 868 869 870 871 872
		$result = [];

		foreach ($this->fields() as $field => $definition) {
			if (is_integer($field)) {
				$field = $definition;
			}
			if (empty($fields) || in_array($field, $fields, true)) {
				$result[$field] = $definition;
			}
		}

		if (empty($expand)) {
			return $result;
		}

Qiang Xue committed
873
		foreach ($this->extraFields() as $field => $definition) {
Qiang Xue committed
874 875 876 877 878 879 880 881 882
			if (is_integer($field)) {
				$field = $definition;
			}
			if (in_array($field, $expand, true)) {
				$result[$field] = $definition;
			}
		}

		return $result;
Qiang Xue committed
883 884
	}

w  
Qiang Xue committed
885 886 887
	/**
	 * Returns an iterator for traversing the attributes in the model.
	 * This method is required by the interface IteratorAggregate.
888
	 * @return ArrayIterator an iterator for traversing the items in the list.
w  
Qiang Xue committed
889 890 891
	 */
	public function getIterator()
	{
w  
Qiang Xue committed
892
		$attributes = $this->getAttributes();
893
		return new ArrayIterator($attributes);
w  
Qiang Xue committed
894 895 896 897
	}

	/**
	 * Returns whether there is an element at the specified offset.
w  
Qiang Xue committed
898 899
	 * This method is required by the SPL interface `ArrayAccess`.
	 * It is implicitly called when you use something like `isset($model[$offset])`.
w  
Qiang Xue committed
900 901 902 903 904
	 * @param mixed $offset the offset to check on
	 * @return boolean
	 */
	public function offsetExists($offset)
	{
Qiang Xue committed
905
		return $this->$offset !== null;
w  
Qiang Xue committed
906 907 908 909
	}

	/**
	 * Returns the element at the specified offset.
w  
Qiang Xue committed
910 911
	 * This method is required by the SPL interface `ArrayAccess`.
	 * It is implicitly called when you use something like `$value = $model[$offset];`.
w  
Qiang Xue committed
912
	 * @param mixed $offset the offset to retrieve element.
w  
Qiang Xue committed
913 914 915 916 917 918 919 920 921
	 * @return mixed the element at the offset, null if no element is found at the offset
	 */
	public function offsetGet($offset)
	{
		return $this->$offset;
	}

	/**
	 * Sets the element at the specified offset.
w  
Qiang Xue committed
922 923
	 * This method is required by the SPL interface `ArrayAccess`.
	 * It is implicitly called when you use something like `$model[$offset] = $item;`.
w  
Qiang Xue committed
924 925 926
	 * @param integer $offset the offset to set element
	 * @param mixed $item the element value
	 */
w  
Qiang Xue committed
927
	public function offsetSet($offset, $item)
w  
Qiang Xue committed
928
	{
w  
Qiang Xue committed
929
		$this->$offset = $item;
w  
Qiang Xue committed
930 931 932
	}

	/**
933
	 * Sets the element value at the specified offset to null.
w  
Qiang Xue committed
934 935
	 * This method is required by the SPL interface `ArrayAccess`.
	 * It is implicitly called when you use something like `unset($model[$offset])`.
w  
Qiang Xue committed
936 937 938 939
	 * @param mixed $offset the offset to unset element
	 */
	public function offsetUnset($offset)
	{
940
		$this->$offset = null;
w  
Qiang Xue committed
941 942
	}
}