yii2 源碼分析 model類分析 (五)


模型類是數據模型的基類.此類繼承了組件類,實現了3個接口

先介紹一下模型類前面的大量注釋說了什么:

 * 模型類是數據模型的基類.此類繼承了組件類,實現了3個接口
 * 實現了IteratorAggregate(聚合式迭代器)接口,實現了ArrayAccess接口,可以像數組一樣訪問對象,這兩個接口是php自帶
 * Arrayable接口是yii2框架自帶
 * 模型實現了以下常用功能:
 *
 * - 屬性聲明: 默認情況下,每個公共類成員都被認為是模型屬性
 * - 屬性標簽: 每個屬性可以與用於顯示目的的標簽相關聯。
 * - 大量的屬性分配
 * - 基於場景的驗證
 *
 * 在執行數據驗證時,模型還引發下列事件:
 *
 * - [[EVENT_BEFORE_VALIDATE]]: 在開始時提出的事件 [[validate()]]
 * - [[EVENT_AFTER_VALIDATE]]: 結束時提出的事件[[validate()]]
 *
 * 您可以直接使用模型存儲模型數據, 或延長定制.
<?php
/**
 * @property \yii\validators\Validator[] $activeValidators 使用場景效驗[[scenario]],此屬性是只讀的
 * @property array $attributes 屬性值鍵值對方式 (name => value).
 * @property array $errors 所有屬性的錯誤數組. 數組為空表示沒有錯誤. 結果是一個二維數組
 * [[getErrors()]]獲取詳細錯誤信息,此屬性是只讀的
 * @property array $firstErrors 第一個錯誤,數組的鍵是屬性名, 數組的值是錯誤信息,空數組表示沒有錯誤,此屬性只讀
 * @property ArrayIterator $iterator 遍歷列表中的項的迭代器,此屬性只讀. 
 * @property string $scenario 模型所在的場景.默認是[[SCENARIO_DEFAULT]].
 * @property ArrayObject|\yii\validators\Validator[] $validators 在模型中定義的所有方法,此屬性只讀.
 *
 * @author Qiang Xue <qiang.xue@gmail.com>
 * @since 2.0
 */
class Model extends Component implements IteratorAggregate, ArrayAccess, Arrayable
{
     //自 PHP 5.4.0 起,PHP 實現了代碼復用的一個方法,稱為 traits,此處就使用了trait
    use ArrayableTrait;

    /**
     * 默認場景名
     */
    const SCENARIO_DEFAULT = 'default';
    /**
     * @event 模型事件先被調用驗證,調用[[validate()]]這個方法. You may set
     * 可以設置[[ModelEvent::isValid]] 的isValid屬性為false來阻止驗證.
     */
    const EVENT_BEFORE_VALIDATE = 'beforeValidate';
    /**
     * @event 驗證[[validate()]]后執行的事件
     */
    const EVENT_AFTER_VALIDATE = 'afterValidate';

    /**
     * @var array 存放驗證錯誤的數組,鍵是屬性名,值是有關錯誤的數組 (attribute name => array of errors)
     */
    private $_errors;
    /**
     * @var ArrayObject list 驗證器集合
     */
    private $_validators;
    /**
     * @var string 當前場景
     */
    private $_scenario = self::SCENARIO_DEFAULT;


    /**
     * 返回屬性的驗證規則.
     *
     * 驗證規則通過 [[validate()]] 方法檢驗屬性是否是有效的
     * 子類應該覆蓋這個方法來聲明不同的驗證規則
     *
     * 每個驗證規則都是下列結構的數組:
     *
     * ```php
     * [
     *     ['attribute1', 'attribute2'],
     *     'validator type',
     *     'on' => ['scenario1', 'scenario2'],
     *     //...other parameters...
     * ]
     * ```
     *
     * where
     *
     *  - 屬性集合: 必選, 指定要驗證的屬性數組, 對於單個屬性,你可以直接傳遞字符串;
     *  - 驗證類型: 必選, 指定要使用的驗證. 它可以是一個內置驗證器的名字,
     *    模型類的方法名稱, 匿名函數, 或驗證器類的名稱.
     *  - on: 可選參數, 是一個數組,表示在指定場景使用,沒設置,表示應用於所有的場景
     *  - 額外的名稱-值對可以指定初始化相應的驗證特性.
     *
     * 一個驗證器可以是一個類的對象延伸擴展[[Validator]], 或模型類方法*(*內置驗證器*),具有以下特征:
     *
     * ```php
     * // $params 引用給驗證規則的參數
     * function validatorName($attribute, $params)
     * ```
     *
     * 上面的 `$attribute` 指當前正在驗證的屬性 。。。 
     * `$params` 包含一個數組驗證配置選項,例如:當字符串驗證時的max屬性,驗證當前的屬性值
     * 可以訪問為 `$this->$attribute`. 注意 `$` before `attribute`; 這是取變量$attribute的值和使用它作為屬性的名稱訪問
     *
     * Yii提供了一套[[Validator::builtInValidators|built-in validators]].
     * 每一個都有別名,可以在指定驗證規則時使用
     *
     * 看下面的一些例子:
     *
     * ```php
     * [
     *     // 內置 "required" 驗證器
     *     [['username', 'password'], 'required'],
     *     // 內置 "string" 驗證器 用長度區間定制屬性
     *     ['username', 'string', 'min' => 3, 'max' => 12],
     *     // 內置 "compare" 驗證器,只能在 "register" 場景中使用
     *     ['password', 'compare', 'compareAttribute' => 'password2', 'on' => 'register'],
     *     // 一個內置驗證器 "authenticate()"方法被定義在模型類里
     *     ['password', 'authenticate', 'on' => 'login'],
     *     // 一個驗證器的類 "DateRangeValidator"
     *     ['dateRange', 'DateRangeValidator'],
     * ];
     * ```
     *
     * 注意,為了繼承定義在父類中的規則, 一個子類應該使用函數合並父類的規則,例如array_merge()這個函數
     *
     * @return array validation rules返回驗證規則數組
     * @see scenarios()
     */
    public function rules()
    {
        return [];
    }

    /**
     * 返回一個場景列表和每個場景對應的屬性,此屬性是活動屬性
     * 一個場景中的屬性只在當前場景中被驗證
     * 返回的數組應該是下列格式的:
     *
     * ```php
     * [
     *     'scenario1' => ['attribute11', 'attribute12', ...],
     *     'scenario2' => ['attribute21', 'attribute22', ...],
     *     ...
     * ]
     * ```
     *
     * 默認情況下,活動屬性被認為是安全的,並且可以被賦值
     * 如果一個屬性不應該被賦值 (因此認為不安全),
     * 請用感嘆號前綴屬性 (例如: `'!rank'`).
     *
     * 此方法默認返回聲明中的所有屬性 [[rules()]]
     * 一個特殊的場景被稱為默認場景[[SCENARIO_DEFAULT]] 將會包含在rules()規則里的所有的屬性
     * 每個場景將與正在應用於場景的驗證規則進行驗證的屬性關聯
     *
     * @return array 返回一個數組和相應的屬性列表
     */
    public function scenarios()
    {
        //在場景數組里先把默認場景放入
        $scenarios = [self::SCENARIO_DEFAULT => []];
        //獲取所有場景迭代,getValidators()獲取關於驗證類對象的數組
        foreach ($this->getValidators() as $validator) {
            //$validator->on的值是數組,獲得所有應用的場景名字
            foreach ($validator->on as $scenario) {
                $scenarios[$scenario] = [];
            }
            //$validator->on的值是數組,獲得當前不使用的場景的名字
            foreach ($validator->except as $scenario) {
                $scenarios[$scenario] = [];
            }
        }
        //$names獲得了所有的場景
        $names = array_keys($scenarios);

        foreach ($this->getValidators() as $validator) {
            if (empty($validator->on) && empty($validator->except)) {
                //on為空數組,except也是空數組,表示驗證規則中沒有設置場景,則把驗證規則運用到所有的場景
                foreach ($names as $name) {
                    foreach ($validator->attributes as $attribute) {
                        //把所有模型驗證屬性添加到每個場景
                        $scenarios[$name][$attribute] = true;
                    }
                }
            } elseif (empty($validator->on)) {
                //on為空,except不為空,表示除了except場景外,應用於所有的場景
                foreach ($names as $name) {
                    if (!in_array($name, $validator->except, true)) {
                        //找到不在except中的場景,放進場景屬性數組,表示在其它場景中驗證
                        foreach ($validator->attributes as $attribute) {
                            $scenarios[$name][$attribute] = true;
                        }
                    }
                }
            } else {
                //on不為空,在on場景中驗證這些屬性
                foreach ($validator->on as $name) {
                    foreach ($validator->attributes as $attribute) {
                        $scenarios[$name][$attribute] = true;
                    }
                }
            }
        }

        //使每個場景名對應一個屬性數組,$scenarios的鍵是場景名
        foreach ($scenarios as $scenario => $attributes) {
            if (!empty($attributes)) {
                $scenarios[$scenario] = array_keys($attributes);
            }
        }
        //場景名對應屬性名的數組
        return $scenarios;
    }

    /**
     * 返回表單的名稱,就是這個 model 的類名.
     *
     * 在一個模型中表單的name值經常被使用在 [[\yii\widgets\ActiveForm]] 決定如何命名屬性的輸入字段
     * 如果表單的name值是A,表單元素屬性名是b,則表單元素的name值為"A[b]"
     * 如果表單的name值為空字符串,則表單元素的name為"b"
     *
     * 上述命名模式的目的是針對包含多個不同模型的表單,比較容易區分不同模型的不同屬性
     * 每個模型的屬性被分組在后的數據的子數組中,它是更容易區分它們。
     *
     * 默認情況下,此方法返回模型類名 (不包含命名空間)
     * 你可以覆蓋此方法,當一個表單中有多個模型時
     *
     * @return string the form name of this model class.模型類的名字
     * @see load()
     */
    public function formName()
    {
        //ReflectionClass是php中的擴展反射類
        $reflector = new ReflectionClass($this);
        //getShortName()返回不帶命名空間的類名
        return $reflector->getShortName();
    }

    /**
     * 返回一個屬性列表
     * 默認情況下,此方法返回類的所有公共非靜態屬性。
     * 可以覆蓋此方法返回你想要的屬性
     * @return array list of attribute names.
     */
    public function attributes()
    {
        //獲取這個類的相關信息
        $class = new ReflectionClass($this);
        $names = [];
        //遍歷這個類的每一個屬性,如果這個屬性是公共的,就把它放入name數組中
        foreach ($class->getProperties(\ReflectionProperty::IS_PUBLIC) as $property) {
            //不是public並且不是static
            if (!$property->isStatic()) {
                $names[] = $property->getName();
            }
        }

        return $names;
    }

    /**
     * 返回屬性的標簽
     *
     * 屬性標簽主要用於顯示. 例如, `firstName`屬性將會顯示成`First Name`標簽,可以有友好的展示給終端用戶
     *
     * 默認的標簽生成是使用 [[generateAttributeLabel()]]這個方法
     * 此方法允許您顯式指定屬性標簽.
     *
     * 注意,為了繼承父類中定義的標簽, 子類標簽可以使用`array_merge()`與父類標簽合並
     *
     * @return array attribute labels (name => label)
     * @see generateAttributeLabel()
     */
    public function attributeLabels()
    {
        return [];
    }

    /**
     * 返回屬性提示
     *
     * 屬性提示主要用於顯示. 例如,`isPublic`這個屬性可以用來描述“未登錄用戶的帖子是否應該可見”
     * 它提供了用戶友好的描述屬性的含義,並可以顯示給最終用戶.
     *
     * 如果省略了顯式聲明,則不會生成標記提示
     *
     * 注意,為了繼承父類中定義的標簽, 子類標簽可以使用`array_merge()`與父類標簽合並.
     *
     * @return array attribute hints (name => hint)
     * @since 2.0.4
     */
    public function attributeHints()
    {
        return [];
    }

    /**
     * 執行數據驗證.
     *
     * 此方法執行適用於當前場景的驗證規則 [[scenario]].
     * 下列標准用於判斷規則是否適用:
     *
     * - 規則必須與當前場景相關的屬性關聯;
     * - 規則在所處的情況下必須是有效的,
     *
     * validate()在執行前會先執行 [[beforeValidate()]] ,
     * validate()執行后會執行 [[afterValidate()]] 
     * 如果[[beforeValidate()]] 返回false,接下來的驗證將被取消
     *
     * 驗證期間發現的錯誤可以通過 [[getErrors()]],[[getFirstErrors()]] and [[getFirstError()]]獲得錯誤信息,
     *
     * @param array $attributeNames 一個應該被驗證的屬性列表
     * 若$attributeNames 為空,這意味着在適用的驗證規則中列出的任何屬性都應該經過驗證。
     * @param boolean $clearErrors 表示在執行驗證前是否先清除錯誤 [[clearErrors()]]
     * @return boolean 驗證是否成功無任何錯誤.
     * @throws InvalidParamException 不知道當前場景時會拋出異常.
     */
    public function validate($attributeNames = null, $clearErrors = true)
    {
        if ($clearErrors) {
            //清除所有的錯誤
            $this->clearErrors();
        }

        if (!$this->beforeValidate()) {
            //沒通過before驗證就返回false
            return false;
        }
        //返回當前活動的場景
        $scenarios = $this->scenarios();
        //返回此模型應用的場景
        $scenario = $this->getScenario();
        if (!isset($scenarios[$scenario])) {
            //若當前活動的場景不在此模型中,拋出異常
            throw new InvalidParamException("Unknown scenario: $scenario");
        }

        if ($attributeNames === null) {
            //屬性數組為空,自動查找當前場景下的安全屬性,並返回這些屬性
            $attributeNames = $this->activeAttributes();
        }

        //$this->getActiveValidators()返回Validator對象數組
        foreach ($this->getActiveValidators() as $validator) {
            //通過Validator對象驗證屬性
            $validator->validateAttributes($this, $attributeNames);
        }
        //驗證的后置方法
        $this->afterValidate();
        //沒有錯誤就返回真
        return !$this->hasErrors();
    }

    /**
     * 在驗證前被驗證
     * 默認的實現提出了一個` beforevalidate `的事件
     * 驗證之前,您可以重寫此方法進行初步檢查。
     * 請確保調用父實現,然后就可以引發此事件。
     * @return boolean是否應執行接下來的驗證,默認是真
     * 如果返回false,則驗證將停止,該模型被認為是無效的
     */
    public function beforeValidate()
    {
        //這個不說了ModelEvent里一個方法都木有
        $event = new ModelEvent;
        $this->trigger(self::EVENT_BEFORE_VALIDATE, $event);

        return $event->isValid;
    }

    /**
     * 驗證執行后被調用
     * 廢話不多說了,可以覆蓋,記得調用父類方法
     */
    public function afterValidate()
    {
        $this->trigger(self::EVENT_AFTER_VALIDATE);
    }

    /**
     * 返回聲明在 [[rules()]]中的驗證.
     *
     * 此方法和 [[getActiveValidators()]] 不同,[[getActiveValidators()]]只返回當前場景的驗證
     *
     * 由於該方法返回一個數組對象的對象,你可以操縱它通過插入或刪除驗證器(模型行為的有用)。
     * For example,
     *
     * ```php
     * $model->validators[] = $newValidator;
     * ```
     *
     * @return ArrayObject|\yii\validators\Validator[] 返回在模型中定義的所有驗證器
     */
    public function getValidators()
    {
        if ($this->_validators === null) {
            $this->_validators = $this->createValidators();
        }
        return $this->_validators;
    }

    /**
     * Returns the validators applicable to the current [[scenario]].
     * @param string $attribute the name of the attribute whose applicable validators should be returned.
     * If this is null, the validators for ALL attributes in the model will be returned.
     * @return \yii\validators\Validator[] the validators applicable to the current [[scenario]].
     */
    public function getActiveValidators($attribute = null)
    {
        $validators = [];
        $scenario = $this->getScenario();
        foreach ($this->getValidators() as $validator) {
            if ($validator->isActive($scenario) && ($attribute === null || in_array($attribute, $validator->attributes, true))) {
                $validators[] = $validator;
            }
        }
        return $validators;
    }

    /**
     * 根據 [[rules()]]里的驗證規則創建一個驗證對象.
     * 和 [[getValidators()]]不一樣, 每次調用此方法,一個新的列表驗證器將返回。
     * @return ArrayObject validators
     * @throws InvalidConfigException 如果任何驗證規則配置無效,拋出異常
     */
    public function createValidators()
    {
        $validators = new ArrayObject;
        foreach ($this->rules() as $rule) {
            //遍歷規則中的每一項
            if ($rule instanceof Validator) {
                ///如果規則屬於Validator對象,添加入數組對象
                $validators->append($rule);
            } elseif (is_array($rule) && isset($rule[0], $rule[1])) { 
                //如果子規則是數組,創建一個驗證器類,把驗證的類型,模型,屬性名,驗證屬性的初始值傳入
                $validator = Validator::createValidator($rule[1], $this, (array) $rule[0], array_slice($rule, 2));
                //把創建的對象加入數組對象
                $validators->append($validator);
            } else {
                //拋出規則必須包含屬性名和驗證類型
                throw new InvalidConfigException('Invalid validation rule: a rule must specify both attribute names and validator type.');
            }
        }
        return $validators;
    }

    /**
     * 檢查屬性是否在當前場景中被應用
     * This is determined by checking if the attribute is associated with a
     * [[\yii\validators\RequiredValidator|required]] validation rule in the
     * current [[scenario]].
     *
     * 注意,當確認有條件驗證的應用,使用
     * [[\yii\validators\RequiredValidator::$when|$when]] 這個方法將會返回
     * `false` 不管 `when` 條件, 因為它可能在模型加載數據前被調用
     *
     * @param string $attribute 屬性名
     * @return boolean whether the attribute is required
     */
    public function isAttributeRequired($attribute)
    {
        foreach ($this->getActiveValidators($attribute) as $validator) {
            if ($validator instanceof RequiredValidator && $validator->when === null) {
                return true;
            }
        }
        return false;
    }

    /**
     * 返回一個值,該值指示屬性是否是安全的
     * @param string $attribute 屬性名
     * @return boolean whether the attribute is safe for massive assignments
     * @see safeAttributes()
     */
    public function isAttributeSafe($attribute)
    {
        //判斷屬性是否在數組里,true表示全等(數值和類型都相等)
        return in_array($attribute, $this->safeAttributes(), true);
    }

    /**
     * 返回一個值,該值指示當前場景中的屬性是否處於活動狀態。
     * @param string $attribute 屬性名
     * @return boolean whether the attribute is active in the current scenario
     * @see activeAttributes()
     */
    public function isAttributeActive($attribute)
    {
        return in_array($attribute, $this->activeAttributes(), true);
    }

    /**
     * 返回指定屬性的文本標簽
     * @param string $attribute 屬性名
     * @return string 屬性標簽
     * @see generateAttributeLabel()
     * @see attributeLabels()
     */
    public function getAttributeLabel($attribute)
    {
        //獲得所有屬性標簽,並給$lable這個數組
        $labels = $this->attributeLabels();
        //如果這個方法被子類重寫了,直接返回自定義的標簽,如果沒有被重寫,返回默認的
        return isset($labels[$attribute]) ? $labels[$attribute] : $this->generateAttributeLabel($attribute);
    }

    /**
     * 返回指定屬性的文本提示
     * @param string $attribute 屬性名
     * @return string the attribute hint
     * @see attributeHints()
     * @since 2.0.4
     */
    public function getAttributeHint($attribute)
    {
        $hints = $this->attributeHints();
        //如果這個方法被子類重寫了,直接返回自定義的文本提示,如果沒有被重寫,返回''
        return isset($hints[$attribute]) ? $hints[$attribute] : '';
    }

    /**
     * 返回一個值,該值指示是否有任何驗證錯誤
     * @param string|null $attribute attribute name. Use null to check all attributes.
     * @return boolean whether there is any error.
     */
    public function hasErrors($attribute = null)
    {
        //如果有錯,_errors[$attribute]有值
        return $attribute === null ? !empty($this->_errors) : isset($this->_errors[$attribute]);
    }

    /**
     * 返回所有屬性或單個屬性的錯誤。
     * @param string $attribute attribute name. 使用NULL檢索所有屬性的錯誤
     * @property array An array of errors for all attributes. 其結果是二維數組,空數組意味着沒有錯誤
     * See [[getErrors()]] for detailed description.
     * @return array 返回一個或多個屬性所指定的錯誤。
     *
     * ```php
     * [
     *     'username' => [
     *         'Username is required.',
     *         'Username must contain only word characters.',
     *     ],
     *     'email' => [
     *         'Email address is invalid.',
     *     ]
     * ]
     * ```
     *
     * @see getFirstErrors()
     * @see getFirstError()
     */
    public function getErrors($attribute = null)
    {
        if ($attribute === null) {
            //如果屬性為空,返回所有屬性的驗證結果
            return $this->_errors === null ? [] : $this->_errors;
        } else {
            //屬性不為空,驗證單個屬性返回的錯誤結果
            return isset($this->_errors[$attribute]) ? $this->_errors[$attribute] : [];
        }
    }

    /**
     * 返回模型中每個屬性的第一個錯誤
     * @return array the first errors. 數組的鍵是屬性名, 數組的值是錯誤信息
     * 沒有錯誤就返回空數組
     * @see getErrors()
     * @see getFirstError()
     */
    public function getFirstErrors()
    {
        if (empty($this->_errors)) {
            //沒有錯誤,返回空數組
            return [];
        } else {
            $errors = [];
            //遍歷所有屬性的第一個錯誤,放進數組
            foreach ($this->_errors as $name => $es) {
                if (!empty($es)) {
                    $errors[$name] = reset($es);
                }
            }

            return $errors;
        }
    }

    /**
     * 返回指定屬性的第一個錯誤。
     * @param string $attribute 屬性名
     * @return string 錯誤信息,空意味着沒有錯誤信息
     * @see getErrors()
     * @see getFirstErrors()
     */
    public function getFirstError($attribute)
    {
        //如果這個屬性的錯誤在驗證時存在,則返回這個錯誤,否則返回null
        return isset($this->_errors[$attribute]) ? reset($this->_errors[$attribute]) : null;
    }

    /**
     * 向指定屬性添加新錯誤
     * @param string $attribute attribute name
     * @param string $error new error message
     */
    public function addError($attribute, $error = '')
    {
        //把錯誤信息添加入指定屬性的數組
        $this->_errors[$attribute][] = $error;
    }

    /**
     * 添加錯誤列表
     * @param array $items 錯誤信息列表. 數組的鍵必須是屬性的名字
     * 數組的值是錯誤信息. 如果一個屬性有很多錯誤,則錯誤需要是數組的形式,
     * 你可以使用 [[getErrors()]] 作為此參數的值
     * @since 2.0.2
     */
    public function addErrors(array $items)
    {
        //遍歷屬性和對應的錯誤
        foreach ($items as $attribute => $errors) {
            if (is_array($errors)) {
                foreach ($errors as $error) {
                    $this->addError($attribute, $error);
                }
            } else {
                $this->addError($attribute, $errors);
            }
        }
    }

    /**
     * 刪除所有屬性或單個屬性的錯誤。
     * @param string $attribute attribute name. 使用NULL刪除所有屬性的錯誤。
     */
    public function clearErrors($attribute = null)
    {
        if ($attribute === null) {
            //如果沒傳屬性,把所有的錯誤清除掉
            $this->_errors = [];
        } else {
            //刪除對應屬性的錯誤
            unset($this->_errors[$attribute]);
        }
    }

    /**
     * 根據給定屬性名稱生成用戶友好的屬性標簽。
     * 這是通過用空格替換下划線、破折號和圓點,並將每個單詞的第一個字母替換為大寫字母。
     * @param string $name the column name
     * @return string the attribute label
     */
    public function generateAttributeLabel($name)
    {
        //camel2words定義了一個正則來替換字符串
        return Inflector::camel2words($name, true);
    }

    /**
     * 返回屬性值
     * @param array $names 返回需要的屬性列表
     * 默認為null, 意味着定義在 [[attributes()]] 里的所有屬性都會被返回
     * 如果是數組,則只返回數組中的屬性
     * @param array $except 不應返回值的屬性列表
     * @return array attribute values (name => value).
     */
    public function getAttributes($names = null, $except = [])
    {
        $values = [];
        if ($names === null) {
            //$names為null,$names設置成所有的屬性
            $names = $this->attributes();
        }
        foreach ($names as $name) {
            $values[$name] = $this->$name;
        }
        foreach ($except as $name) {
            //不返回哪個屬性,從數組中刪除哪個屬性
            unset($values[$name]);
        }

        return $values;
    }

    /**
     * 以大量的方式設置屬性值。
     * @param array $values 屬性以 (name => value) 被分配到模型
     * @param boolean $safeOnly 賦值是否只對安全的屬性進行
     * 安全屬性是與當前中的驗證規則關聯的屬性,定義在[[scenario]]中.
     * @see safeAttributes()
     * @see attributes()
     */
    public function setAttributes($values, $safeOnly = true)
    {
        if (is_array($values)) {
            //array_flip交換數組中的鍵和值,$safeOnly為true返回安全屬性,否則返回所有的屬性
            $attributes = array_flip($safeOnly ? $this->safeAttributes() : $this->attributes());
            foreach ($values as $name => $value) {
                if (isset($attributes[$name])) {
                    // 如果存在該屬性,就直接賦值
                    $this->$name = $value;
                } elseif ($safeOnly) {
                     // 如果不存在,而且是 safeOnly 的話,就觸發一下 onUnsafeAttribute 方法
                    $this->onUnsafeAttribute($name, $value);
                }
            }
        }
    }

    /**
     * 當一個不安全的屬性被賦值時調用此方法
     * 如果是在yii_debug,默認實現會記錄一個警告消息
     * @param string $name the unsafe attribute name
     * @param mixed $value the attribute value
     */
    public function onUnsafeAttribute($name, $value)
    {
        if (YII_DEBUG) {
            //debug模式,警告信息出現
            Yii::trace("Failed to set unsafe attribute '$name' in '" . get_class($this) . "'.", __METHOD__);
        }
    }

    /**
     * 返回在模型中應用的場景
     *
     * 場景影響如何進行驗證,哪些屬性可以大量分配。
     *
     * @return string the scenario that this model is in. 默認場景是 [[SCENARIO_DEFAULT]].
     */
    public function getScenario()
    {
        return $this->_scenario;
    }

    /**
     * 為模型設置場景
     * 注意,此方法不檢查場景是否存在
     * [[validate()]]會檢查場景是否存在.
     * @param string $value 再模型中的場景名.
     */
    public function setScenario($value)
    {
        $this->_scenario = $value;
    }

    /**
     * 返回當前場景中大量分配的安全的屬性名稱
     * @return string[] safe attribute names
     */
    public function safeAttributes()
    {
        // 獲取當前的場景
        $scenario = $this->getScenario();
         // 獲取所有場景及其屬性
        $scenarios = $this->scenarios();
        if (!isset($scenarios[$scenario])) {
            // 場景不存在,就返回空
            return [];
        }
        $attributes = [];
        foreach ($scenarios[$scenario] as $attribute) {
            // 將開頭不是!的屬性才會放入到 $attributes 中
            if ($attribute[0] !== '!' && !in_array('!' . $attribute, $scenarios[$scenario])) {
                $attributes[] = $attribute;
            }
        }

        return $attributes;
    }

    /**
     * 返回當前場景中要受驗證的屬性名稱
     * @return string[] 返回安全的屬性名稱
     */
    public function activeAttributes()
    {
        $scenario = $this->getScenario();
        $scenarios = $this->scenarios();
        if (!isset($scenarios[$scenario])) {
            return [];
        }
        // 獲取當前場景中的所有屬性
        $attributes = $scenarios[$scenario];
        foreach ($attributes as $i => $attribute) {
            if ($attribute[0] === '!') {
                // 如果屬性名以!開頭,就把!截取掉,並放入數組
                $attributes[$i] = substr($attribute, 1);
            }
        }

        return $attributes;
    }

    /**
     * 填充模型的輸入數據(把數據加載到模型中)
     *
     * 這種方法提供了一個方便快捷的方式:
     *
     * ```php
     * if (isset($_POST['FormName'])) {
     *     $model->attributes = $_POST['FormName'];
     *     if ($model->save()) {
     *         // handle success
     *     }
     * }
     * ```
     *
     * 如果使用load方法
     *
     * ```php
     * if ($model->load($_POST) && $model->save()) {
     *     // handle success
     * }
     * ```
     *
     * `load()` gets the `'FormName'` from the model's [[formName()]] method (which you may override), unless the
     * `$formName` parameter is given. If the form name is empty, `load()` populates the model with the whole of `$data`,
     * instead of `$data['FormName']`.
     *
     * 注意,被填充的數據將會接受[[setAttributes()]]的安全檢查.
     *
     * @param array $data 模型加載的數組, 典型的是 `$_POST` or `$_GET`.
     * @param string $formName 用於將數據加載到模型中的表單名稱
     * If not set, [[formName()]] is used.
     * @return boolean whether `load()` found the expected form in `$data`.
     */
    public function load($data, $formName = null)
    {
        //如果沒有傳表單名稱,就取所在類的名稱
        $scope = $formName === null ? $this->formName() : $formName;
        if ($scope === '' && !empty($data)) {
             //如果 $scope 為空字符串,且 $data不為空,就設置屬性
            $this->setAttributes($data);

            return true;
        } elseif (isset($data[$scope])) {
            // 存在 $data[$scope],使用 $data[$scope] 去設置屬性
            $this->setAttributes($data[$scope]);

            return true;
        } else {
            return false;
        }
    }

    /**
     * 從終端用戶獲取數據,形成模型
     * 該方法主要用於收集表格數據輸入
     * 為每個模型加載的數據`$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]` 將用於填充每個模型.
     * 每個模型的數據都要經過安全檢查 [[setAttributes()]].
     * @param array $models 要填充的模型 注意所有的模型都應該有相同的類
     * @param array $data the data array. 通常是 `$_POST` or `$_GET`, 也可以是最終用戶提供的任何有效數組。
     * @param string $formName 要把數據加載到模型的表單名
     * If not set, it will use the [[formName()]] value of the first model in `$models`.
     * This parameter is available since version 2.0.1.
     * @return boolean whether at least one of the models is successfully populated.
     */
    public static function loadMultiple($models, $data, $formName = null)
    {
        if ($formName === null) {
            /* @var $first Model */
            //reset — 將數組的內部指針指向第一個單元
            $first = reset($models);
            if ($first === false) {
                 // 不存在就返回 false
                return false;
            }
            // 拿到所在類的名稱
            $formName = $first->formName();
        }

        $success = false;
        // 遍歷 $models,一個個 load 數據
        foreach ($models as $i => $model) {
            /* @var $model Model */
            if ($formName == '') {
                if (!empty($data[$i])) {
                    // 數據不為空,就 load 到相應的 model 中
                    $model->load($data[$i], '');
                    $success = true;
                }
            } elseif (!empty($data[$formName][$i])) {
                // 存在 $formName,且數據不為空,就 load 到相應的 model 中
                $model->load($data[$formName][$i], '');
                $success = true;
            }
        }

        return $success;
    }

    /**
     * 驗證多模型
     * 這種方法將驗證每一個模型。被驗證的模型可以是相同的或不同類型的。
     * @param array $models 要驗證的模型
     * @param array $attributeNames 應該驗證的屬性名稱列表。
     * 如果這個參數是空的,它意味着在適用的驗證規則中列出的任何屬性都應該被驗證。
     * @return boolean 所有模型的驗證規則是否有效. 一個或多個驗證不通過都會返回false
     */
    public static function validateMultiple($models, $attributeNames = null)
    {
        $valid = true;
        /* @var $model Model */
        foreach ($models as $model) {
            //遍歷$models 調用validate()方法
            $valid = $model->validate($attributeNames) && $valid;
        }

        return $valid;
    }

    /**
     * 當沒有指定特定字段時,通過[[toArray()]] 方法返回默認情況下應返回的字段列表
     *
     * 此方法應返回字段名稱或字段定義的數組
     * 當沒有指定特定字段時,字段名稱將被視為對象屬性名稱,其值將用作字段值 
     * 當指定字段時,數組鍵應該是字段名,而數組值應該是相應的字段定義,它可以是對象屬性名,也可以是PHP可調用返回相應字段值
     * 
     *回調函數格式為:
     * ```php
     * function ($model, $field) {
     *     // return field value
     * }
     * ```
     *
     * 例如,下面的代碼聲明四個字段:
     *
     * - `email`: 字段名稱與屬性名稱相同`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 ($model) {
     *         return $model->first_name . ' ' . $model->last_name;
     *     },
     * ];
     * ```
     *
     * 在此方法中,還可以根據一些上下文信息返回字段的不同列表
     * 例如,取決於場景[scenario]或當前應用程序用戶的特權
     * 您可以返回不同的可見字段集或篩選一些字段。
     *
     * 此方法返回[[attributes()]] 的默認實現,索引是屬性名
     *
     * @return array 返回字段名稱或字段定義的列表.
     * @see toArray()
     */
    public function fields()
    {
        $fields = $this->attributes();
        //合並兩個數組來創建一個新數組,其中的一個數組元素為鍵名,另一個數組元素為鍵值
        return array_combine($fields, $fields);
    }

    /**
     * 返回用於遍歷模型中的屬性的迭代器
     * 此方法所需的接口[[\IteratorAggregate]].
     * @return ArrayIterator 遍歷列表中的項的迭代器
     */
    public function getIterator()
    {
        $attributes = $this->getAttributes();
        return new ArrayIterator($attributes);
    }

    /**
     * 返回指定偏移量是否有元素
     * 此方法需要SPL接口 [[\ArrayAccess]].
     * 它會隱式調用 `isset($model[$offset])`.
     * @param mixed $offset 檢查的偏移量
     * @return boolean 是否存在偏移
     */
    public function offsetExists($offset)
    {
        return isset($this->$offset);
    }

    /**
     * 返回指定偏移量的元素
     * 此方法需要SPL接口 [[\ArrayAccess]].
     * 會隱式調用 `$value = $model[$offset];`.
     * @param mixed $offset the offset to retrieve element.
     * @return mixed the element at the offset,如果在偏移處沒有找到元素,返回null
     */
    public function offsetGet($offset)
    {
        return $this->$offset;
    }

    /**
     * 設置指定偏移量的元素
     * 此方法需要SPL接口[[\ArrayAccess]].
     * 會被隱式調用 `$model[$offset] = $item;`.
     * @param integer $offset the offset to set element
     * @param mixed $item 節點的值
     */
    public function offsetSet($offset, $item)
    {
        $this->$offset = $item;
    }

    /**
     * 將指定偏移量的元素值設置為空
     * 此方法需要SPL接口 [[\ArrayAccess]].
     * 會隱式調用 `unset($model[$offset])`.
     * @param mixed $offset the offset to unset element
     */
    public function offsetUnset($offset)
    {
        $this->$offset = null;
    }
}

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM