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;
16
use yii\helpers\Inflector;
17
use yii\validators\RequiredValidator;
18
use yii\validators\Validator;
Qiang Xue committed
19

w  
Qiang Xue committed
20
/**
w  
Qiang Xue committed
21
 * Model is the base class for data models.
w  
Qiang Xue committed
22
 *
w  
Qiang Xue committed
23 24 25 26 27 28 29 30
 * 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
31
 * Model also raises the following events when performing data validation:
w  
Qiang Xue committed
32
 *
Qiang Xue committed
33 34
 * - [[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
35 36 37
 *
 * 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
38
 *
39 40
 * @property \yii\validators\Validator[] $activeValidators The validators applicable to the current
 * [[scenario]]. This property is read-only.
resurtm committed
41
 * @property array $attributes Attribute values (name => value).
42 43
 * @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.
44 45 46
 * @property array $firstErrors 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. This property is
 * read-only.
47 48
 * @property ArrayIterator $iterator An iterator for traversing the items in the list. This property is
 * read-only.
49
 * @property string $scenario The scenario that this model is in. Defaults to [[SCENARIO_DEFAULT]].
50 51
 * @property ArrayObject|\yii\validators\Validator[] $validators All the validators declared in the model.
 * This property is read-only.
Qiang Xue committed
52
 *
w  
Qiang Xue committed
53
 * @author Qiang Xue <qiang.xue@gmail.com>
w  
Qiang Xue committed
54
 * @since 2.0
w  
Qiang Xue committed
55
 */
56
class Model extends Component implements IteratorAggregate, ArrayAccess, Arrayable
w  
Qiang Xue committed
57
{
Qiang Xue committed
58 59
	use ArrayableTrait;

60 61 62
	/**
	 * The name of the default scenario.
	 */
63
	const SCENARIO_DEFAULT = 'default';
64 65 66 67 68 69 70 71 72 73
	/**
	 * @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
74 75 76 77 78
	/**
	 * @var array validation errors (attribute name => array of errors)
	 */
	private $_errors;
	/**
79
	 * @var ArrayObject list of validators
Qiang Xue committed
80 81 82 83 84
	 */
	private $_validators;
	/**
	 * @var string current scenario
	 */
85
	private $_scenario = self::SCENARIO_DEFAULT;
w  
Qiang Xue committed
86 87 88 89

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

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

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

226
		return $scenarios;
227 228
	}

229 230 231 232 233 234 235 236 237 238 239 240 241 242 243
	/**
	 * 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()
	{
244
		$reflector = new ReflectionClass($this);
245
		return $reflector->getShortName();
246 247
	}

248 249 250 251 252 253
	/**
	 * 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.
	 */
254
	public function attributes()
255
	{
256
		$class = new ReflectionClass($this);
Alexander Makarov committed
257
		$names = [];
258 259
		foreach ($class->getProperties(\ReflectionProperty::IS_PUBLIC) as $property) {
			if (!$property->isStatic()) {
260
				$names[] = $property->getName();
261 262
			}
		}
Qiang Xue committed
263
		return $names;
264 265
	}

w  
Qiang Xue committed
266 267
	/**
	 * Returns the attribute labels.
w  
Qiang Xue committed
268 269 270 271 272
	 *
	 * 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
273
	 * By default an attribute label is generated using [[generateAttributeLabel()]].
w  
Qiang Xue committed
274 275 276
	 * 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
277
	 * merge the parent labels with child labels using functions such as `array_merge()`.
w  
Qiang Xue committed
278
	 *
resurtm committed
279
	 * @return array attribute labels (name => label)
280
	 * @see generateAttributeLabel()
w  
Qiang Xue committed
281 282 283
	 */
	public function attributeLabels()
	{
Alexander Makarov committed
284
		return [];
w  
Qiang Xue committed
285 286 287
	}

	/**
w  
Qiang Xue committed
288
	 * Performs the data validation.
w  
Qiang Xue committed
289
	 *
290 291 292 293 294
	 * 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
295
	 *
Qiang Xue committed
296
	 * This method will call [[beforeValidate()]] and [[afterValidate()]] before and
297 298
	 * after the actual validation, respectively. If [[beforeValidate()]] returns false,
	 * the validation will be cancelled and [[afterValidate()]] will not be called.
w  
Qiang Xue committed
299
	 *
300 301
	 * Errors found during the validation can be retrieved via [[getErrors()]],
	 * [[getFirstErrors()]] and [[getFirstError()]].
w  
Qiang Xue committed
302
	 *
w  
Qiang Xue committed
303 304 305
	 * @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
306
	 * @param boolean $clearErrors whether to call [[clearErrors()]] before performing validation
w  
Qiang Xue committed
307
	 * @return boolean whether the validation is successful without any error.
308
	 * @throws InvalidParamException if the current scenario is unknown.
w  
Qiang Xue committed
309
	 */
w  
Qiang Xue committed
310
	public function validate($attributes = null, $clearErrors = true)
w  
Qiang Xue committed
311
	{
312 313 314 315 316 317
		$scenarios = $this->scenarios();
		$scenario = $this->getScenario();
		if (!isset($scenarios[$scenario])) {
			throw new InvalidParamException("Unknown scenario: $scenario");
		}

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

	/**
	 * This method is invoked before validation starts.
Qiang Xue committed
336
	 * The default implementation raises a `beforeValidate` event.
w  
Qiang Xue committed
337 338
	 * 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.
339
	 * @return boolean whether the validation should be executed. Defaults to true.
w  
Qiang Xue committed
340 341
	 * If false is returned, the validation will stop and the model is considered invalid.
	 */
w  
Qiang Xue committed
342
	public function beforeValidate()
w  
Qiang Xue committed
343
	{
Qiang Xue committed
344
		$event = new ModelEvent;
345
		$this->trigger(self::EVENT_BEFORE_VALIDATE, $event);
Qiang Xue committed
346
		return $event->isValid;
w  
Qiang Xue committed
347 348 349 350
	}

	/**
	 * This method is invoked after validation ends.
Qiang Xue committed
351
	 * The default implementation raises an `afterValidate` event.
w  
Qiang Xue committed
352 353 354
	 * 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
355
	public function afterValidate()
w  
Qiang Xue committed
356
	{
357
		$this->trigger(self::EVENT_AFTER_VALIDATE);
w  
Qiang Xue committed
358 359 360
	}

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

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

	/**
Qiang Xue committed
403 404
	 * 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.
405
	 * @return ArrayObject validators
Qiang Xue committed
406
	 * @throws InvalidConfigException if any validation rule configuration is invalid
w  
Qiang Xue committed
407 408 409
	 */
	public function createValidators()
	{
410
		$validators = new ArrayObject;
w  
Qiang Xue committed
411
		foreach ($this->rules() as $rule) {
412
			if ($rule instanceof Validator) {
413
				$validators->append($rule);
414
			} elseif (is_array($rule) && isset($rule[0], $rule[1])) { // attributes, validator type
415
				$validator = Validator::createValidator($rule[1], $this, (array) $rule[0], array_slice($rule, 2));
416
				$validators->append($validator);
Qiang Xue committed
417
			} else {
Qiang Xue committed
418
				throw new InvalidConfigException('Invalid validation rule: a rule must specify both attribute names and validator type.');
w  
Qiang Xue committed
419
			}
w  
Qiang Xue committed
420 421 422 423 424 425 426
		}
		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
427
	 * [[\yii\validators\RequiredValidator|required]] validation rule in the
w  
Qiang Xue committed
428
	 * current [[scenario]].
w  
Qiang Xue committed
429 430 431 432 433
	 * @param string $attribute attribute name
	 * @return boolean whether the attribute is required
	 */
	public function isAttributeRequired($attribute)
	{
w  
Qiang Xue committed
434
		foreach ($this->getActiveValidators($attribute) as $validator) {
435
			if ($validator instanceof RequiredValidator) {
w  
Qiang Xue committed
436
				return true;
w  
Qiang Xue committed
437
			}
w  
Qiang Xue committed
438 439 440 441 442 443 444 445
		}
		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
446
	 * @see safeAttributes()
w  
Qiang Xue committed
447 448 449
	 */
	public function isAttributeSafe($attribute)
	{
450
		return in_array($attribute, $this->safeAttributes(), true);
w  
Qiang Xue committed
451 452
	}

453 454 455 456 457 458 459 460 461 462 463
	/**
	 * 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
464 465 466 467
	/**
	 * Returns the text label for the specified attribute.
	 * @param string $attribute the attribute name
	 * @return string the attribute label
468 469
	 * @see generateAttributeLabel()
	 * @see attributeLabels()
w  
Qiang Xue committed
470 471 472
	 */
	public function getAttributeLabel($attribute)
	{
w  
Qiang Xue committed
473
		$labels = $this->attributeLabels();
Alex committed
474
		return isset($labels[$attribute]) ? $labels[$attribute] : $this->generateAttributeLabel($attribute);
w  
Qiang Xue committed
475 476 477 478
	}

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

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

Qiang Xue committed
519 520
	/**
	 * Returns the first error of every attribute in the model.
Qiang Xue committed
521 522
	 * @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.
523 524
	 * @see getErrors()
	 * @see getFirstError()
Qiang Xue committed
525 526 527 528
	 */
	public function getFirstErrors()
	{
		if (empty($this->_errors)) {
Alexander Makarov committed
529
			return [];
Qiang Xue committed
530
		} else {
Alexander Makarov committed
531
			$errors = [];
Qiang Xue committed
532 533 534
			foreach ($this->_errors as $name => $es) {
				if (!empty($es)) {
					$errors[$name] = reset($es);
Qiang Xue committed
535 536
				}
			}
Qiang Xue committed
537
			return $errors;
Qiang Xue committed
538 539 540
		}
	}

w  
Qiang Xue committed
541 542 543 544
	/**
	 * Returns the first error of the specified attribute.
	 * @param string $attribute attribute name.
	 * @return string the error message. Null is returned if no error.
545 546
	 * @see getErrors()
	 * @see getFirstErrors()
w  
Qiang Xue committed
547
	 */
Qiang Xue committed
548
	public function getFirstError($attribute)
w  
Qiang Xue committed
549 550 551 552 553 554 555 556 557
	{
		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
	 */
558
	public function addError($attribute, $error = '')
w  
Qiang Xue committed
559
	{
w  
Qiang Xue committed
560
		$this->_errors[$attribute][] = $error;
w  
Qiang Xue committed
561 562 563 564 565 566
	}

	/**
	 * 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
567
	public function clearErrors($attribute = null)
w  
Qiang Xue committed
568
	{
w  
Qiang Xue committed
569
		if ($attribute === null) {
Alexander Makarov committed
570
			$this->_errors = [];
Qiang Xue committed
571
		} else {
w  
Qiang Xue committed
572
			unset($this->_errors[$attribute]);
w  
Qiang Xue committed
573
		}
w  
Qiang Xue committed
574 575 576
	}

	/**
w  
Qiang Xue committed
577 578
	 * 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
579
	 * changing the first letter of each word to upper case.
w  
Qiang Xue committed
580
	 * For example, 'department_name' or 'DepartmentName' will generate 'Department Name'.
w  
Qiang Xue committed
581 582 583 584 585
	 * @param string $name the column name
	 * @return string the attribute label
	 */
	public function generateAttributeLabel($name)
	{
586
		return Inflector::camel2words($name, true);
w  
Qiang Xue committed
587 588 589
	}

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

		return $values;
w  
Qiang Xue committed
611 612 613 614
	}

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

	/**
	 * 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
642
	public function onUnsafeAttribute($name, $value)
w  
Qiang Xue committed
643
	{
w  
Qiang Xue committed
644
		if (YII_DEBUG) {
Qiang Xue committed
645
			Yii::trace("Failed to set unsafe attribute '$name' in '" . get_class($this) . "'.", __METHOD__);
w  
Qiang Xue committed
646
		}
w  
Qiang Xue committed
647 648 649 650 651 652 653 654
	}

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

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

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

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

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

738 739 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
	/**
	 * 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])) {
766
				$model->setAttributes($data[$scope][$i]);
767 768 769 770 771 772 773 774
				$success = true;
			}
		}
		return $success;
	}

	/**
	 * Validates multiple models.
775 776
	 * This method will validate every model. The models being validated may
	 * be of the same or different types.
777
	 * @param array $models the models to be validated
778 779 780
	 * @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.
781 782 783
	 * @return boolean whether all models are valid. False will be returned if one
	 * or multiple models have validation error.
	 */
784
	public static function validateMultiple($models, $attributes = null)
785 786 787 788
	{
		$valid = true;
		/** @var Model $model */
		foreach ($models as $model) {
789
			$valid = $model->validate($attributes) && $valid;
790 791 792 793
		}
		return $valid;
	}

Qiang Xue committed
794 795 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
	/**
	 * 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
831 832
	 * 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
833 834 835 836 837 838 839 840 841 842 843 844 845 846
	 *
	 * 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
847
	 * This method will check the requested fields against those declared in [[fields()]] and [[extraFields()]]
Qiang Xue committed
848 849 850 851 852 853 854
	 * 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
855
	{
Qiang Xue committed
856 857 858 859 860 861 862 863 864 865 866 867 868 869 870
		$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
871
		foreach ($this->extraFields() as $field => $definition) {
Qiang Xue committed
872 873 874 875 876 877 878 879 880
			if (is_integer($field)) {
				$field = $definition;
			}
			if (in_array($field, $expand, true)) {
				$result[$field] = $definition;
			}
		}

		return $result;
Qiang Xue committed
881 882
	}

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

	/**
	 * Returns whether there is an element at the specified offset.
w  
Qiang Xue committed
896 897
	 * 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
898 899 900 901 902
	 * @param mixed $offset the offset to check on
	 * @return boolean
	 */
	public function offsetExists($offset)
	{
Qiang Xue committed
903
		return $this->$offset !== null;
w  
Qiang Xue committed
904 905 906 907
	}

	/**
	 * Returns the element at the specified offset.
w  
Qiang Xue committed
908 909
	 * 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
910
	 * @param mixed $offset the offset to retrieve element.
w  
Qiang Xue committed
911 912 913 914 915 916 917 918 919
	 * @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
920 921
	 * 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
922 923 924
	 * @param integer $offset the offset to set element
	 * @param mixed $item the element value
	 */
w  
Qiang Xue committed
925
	public function offsetSet($offset, $item)
w  
Qiang Xue committed
926
	{
w  
Qiang Xue committed
927
		$this->$offset = $item;
w  
Qiang Xue committed
928 929 930
	}

	/**
931
	 * Sets the element value at the specified offset to null.
w  
Qiang Xue committed
932 933
	 * 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
934 935 936 937
	 * @param mixed $offset the offset to unset element
	 */
	public function offsetUnset($offset)
	{
938
		$this->$offset = null;
w  
Qiang Xue committed
939 940
	}
}