Zhucola 2019-07-15 17:31:45 345次扫瞄 0条评论 0 0 0

快乐飞艇文章已经同步到 GitHub 仓库 ,欢迎 star。

Model 类架构

Model类源码在verdor/yiisoft/yii2/base/Model.php,该类和传统框架的模型层不一样,比如CI3的模型层Model是专门和数据库交互的,而Yii2的Model是用来处理业务数据、逻辑、验证、猎取验证错误信息的
快乐飞艇 个人感觉yii2的Model层比较难理解,注释中作者写道

Model is the base class for data models
You may directly use Model to store model data, or extend it with customization

快乐飞艇单纯的Model类无法和数据库交互,需要配合活动记录ActiveRecord

class Model extends Component implements StaticInstanceInterface, IteratorAggregate, ArrayAccess, Arrayable
{
    use ArrayableTrait;   //多继续trait
    use StaticInstanceTrait;  //多继续trait
    const SCENARIO_DEFAULT = 'default';  //默认场景default
    const EVENT_BEFORE_VALIDATE = 'beforeValidate';  //结尾验证事件
    const EVENT_AFTER_VALIDATE = 'afterValidate';  //完毕验证事件
    private $_errors;   //验证错误的错误信息
    private $_validators;  //验证类
    private $_scenario = self::SCENARIO_DEFAULT;  //场景

创建 Model

可以在控制器里面直接new一个Model

use app\models\User;
class TestController extends Controller{
    public function actionModel(){
        $user = new User();
    }
}

也可以用静态方法实例化

use app\models\User;
class TestController extends Controller{
    public function actionModel(){
        $user1 = User:instance();
        $user2 = User:instance(true);
    }
}

因为 base\Model 使用了 StaticInstanceTraitStaticInstanceTrait 是一个 trait,里面只有一个静态方法 instance
参数 refresh 为 false 可以预防多次调用造成的多次实例化,同时true可以强制重新实例化

trait StaticInstanceTrait
{
    private static $_instances = [];
    public static function instance($refresh = false)
    {
        $className = get_called_class();
        if ($refresh || !isset(self::$_instances[$className])) {
            self::$_instances[$className] = Yii::createObject($className);
        }
        return self::$_instances[$className];
    }
}

由于是trait,所以可以在自己的Model里面重写instance方法

class User extends Model
{
    public static function instance(){
        echo "instance rewrite";
    }
}

可以使用 formName 方法来猎取去除命名空间的类名,formName 方法是 base\Model 的底层方法,在需要记录日志的情况下非常有用

public function formName()
{
    //反射本类
    $reflector = new ReflectionClass($this);
    //php版本在7.0.0以上并且不是匿名类
    if (PHP_VERSION_ID >= 70000 && $reflector->isAnonymous()) {
        throw new InvalidConfigException('The "formName()" method should be explicitly defined for anonymous models');
    }
    //猎取shortName
    return $reflector->getShortName();
}

属性

模型通过属性来代表业务数据,属性在Model里面需要定义成非静态共有方法
快乐飞艇 涉及属性的方法操作非常多,而且还有一个trait ArrayableTrait来多继续增加操作属性的方法

public function attributes()   //猎取全部全部非静态共有属性
public function attributeLabels()  //猎取全部属性标签
public function attributeHints()  //猎取全部属性hint
public function isAttributeRequired($attribute)  //推断$attribute是否是必填
public function safeAttributes()  //猎取该场景下的全部safe属性,safe属性就是rule中定义的非!规则属性
public function isAttributeActive($attribute)  //推断$attribute是否在该场景下有校验规则
public function getAttributeLabel($attribute) //猎取$attribute的属性标签
public function generateAttributeLabel($name)  //猎取$name的generate属性标签,就是mb_ucwords($name)
public function getAttributes($names = null, $except = [])  //猎取属性,并且排除except数组里面的属性
public function setAttributes($values, $safeOnly = true)  //设置属性值,假如safeOnly为true则只能设置在该场景下rule中的属性
public function fields()  //猎取属性数组
public function getIterator()  //属性迭代器,为IteratorAggregate接口需要继续的方法

attributes方法,猎取全部全部非静态共有属性
其实php的get_class_vars方法也是猎取全部共有属性,但是get_class_vars也猎取的静态共有属性

public function attributes()
{
    //反射该类
    $class = new ReflectionClass($this);
    $names = [];
    //猎取全部共有属性
    foreach ($class->getProperties(\ReflectionProperty::IS_PUBLIC) as $property) {
        if (!$property->isStatic()) {
            //排除静态属性
            $names[] = $property->getName();
        }
    }

    return $names;
}

getAttributes方法感觉比较鸡肋,源码比较复杂

public function getAttributes($names = null, $except = [])
{
    $values = [];
    if ($names === null) {
        $names = $this->attributes();  //猎取全部非静态共有属性
    }
    foreach ($names as $name) {
        $values[$name] = $this->$name;
    }
    foreach ($except as $name) {  //排除属性
        unset($values[$name]);
    }

    return $values;
}

快乐飞艇fields也是猎取全部非静态共有属性,只不过返回结构不同

public function fields()
{
    $fields = $this->attributes();

    return array_combine($fields, $fields);
}

快乐飞艇base\Model 有 trait ArrayableTrait,ArrayableTrait 里面也有一个 fields,区别就是 Model 的fields 不会猎取非静态共有属性

public function fields()
{
    $fields = array_keys(Yii::getObjectVars($this));
    return array_combine($fields, $fields);
}

attributeLabels 和 attributeHints 方法是猎取全部属性标签和属性 hint,需要自己去重写这两个方法
快乐飞艇 hint 其实没理解到底应该怎么用,其实我感觉直接使用标签就够了

public function attributeLabels()
{
    return [];
}
public function attributeHints()
{
    return [];
}
public function getAttributeLabel($attribute)   //猎取$attribute属性标签
{
    $labels = $this->attributeLabels();
    return isset($labels[$attribute]) ? $labels[$attribute] : $this->generateAttributeLabel($attribute);
}
public function getAttributeHint($attribute)  //猎取属性hints
{
    $hints = $this->attributeHints();
    return isset($hints[$attribute]) ? $hints[$attribute] : '';
}
public function generateAttributeLabel($name)
{
    return Inflector::camel2words($name, true);   //其实就是mb_ucwords
}

重写

class User extends Model
{
    public function attributeLabels()
    {
        return [
            "name"=>"a name",   //name被注册为一个属性标签
        ];
    }
    
    public function attributeHints()
    {
        return [
            "age"=>"x age",   //age被注册为一个属性hint
        ];
    }
}

isAttributeRequired 方法会推断参数 $attribute 是否需要被 RequiredValidator 类校验

public function isAttributeRequired($attribute)
{
    //猎取$attribute在该场景下的全部校验类
    foreach ($this->getActiveValidators($attribute) as $validator) {  
        if ($validator instanceof RequiredValidator && $validator->when === null) {
            return true;
        }
    }

    return false;
}

猎取safe属性

public function safeAttributes()
{
    //猎取本场景
    $scenario = $this->getScenario();
    //猎取场景与属性的对应关系
    $scenarios = $this->scenarios();
    if (!isset($scenarios[$scenario])) {
        return [];
    }
    $attributes = [];
    foreach ($scenarios[$scenario] as $attribute) {
        if ($attribute[0] !== '!' && !in_array('!' . $attribute, $scenarios[$scenario])) {
            $attributes[] = $attribute;
        }
    }
    return $attributes;
}

关于safe属性,假如有如下配置,场景为default,那么safe属性就是name和sex

class User extends Model{
    public function rules()
    {
        return [
            [['name'], 'required', 'on' => 'default'],
            [['sex'], 'boolean', 'on' => ['default','login']],
            [['!height'], 'boolean', 'except'=>"unregister"],
        ];
    }
}

还有一个属性是active属性,相对于safe属性,就是name、sex、height

public function activeAttributes()
{
    $scenario = $this->getScenario();
    $scenarios = $this->scenarios();
    if (!isset($scenarios[$scenario])) {
        return [];
    }
    $attributes = array_keys(array_flip($scenarios[$scenario]));
    foreach ($attributes as $i => $attribute) {
        if ($attribute[0] === '!') {
            $attributes[$i] = substr($attribute, 1);
        }
    }
    return $attributes;
}

快乐飞艇给属性赋值的操作是 setAttributes

public function setAttributes($values, $safeOnly = true)
{
    if (is_array($values)) {
        //推断是猎取safe属性还是直接猎取类的非静态共有方法
        $attributes = array_flip($safeOnly ? $this->safeAttributes() : $this->attributes());
        foreach ($values as $name => $value) {
            if (isset($attributes[$name])) {
                //给类属性赋值
                $this->$name = $value;
            } elseif ($safeOnly) {
                //对不安全的属性记录日志
                $this->onUnsafeAttribute($name, $value);
            }
        }
    }
}
//对不安全的属性记录日志,只在debug模式下会记录日志
public function onUnsafeAttribute($name, $value)
{
    if (YII_DEBUG) {
        Yii::debug("Failed to set unsafe attribute '$name' in '" . get_class($this) . "'.", __METHOD__);
    }
}

setAttributes 也叫块赋值,只用一行代码将用户全部输出填充到一个模型, 以下两段代码效果是不同的, 都是将终端用户输出的表单数据赋值到Model

$model = new \app\models\ContactForm;
$model->attributes = \Yii::$app->request->post('ContactForm');
$model = new \app\models\ContactForm;
$data = \Yii::$app->request->post('ContactForm', []);
$model->name = isset($data['name']) ? $data['name'] : null;
$model->email = isset($data['email']) ? $data['email'] : null;
$model->subject = isset($data['subject']) ? $data['subject'] : null;
$model->body = isset($data['body']) ? $data['body'] : null;

或者也可以使用load、loadMultiple给属性赋值

public function load($data, $formName = null)
{
    $scope = $formName === null ? $this->formName() : $formName;
    if ($scope === '' && !empty($data)) {
        $this->setAttributes($data);

        return true;
    } elseif (isset($data[$scope])) {
        $this->setAttributes($data[$scope]);

        return true;
    }

    return false;
}
public static function loadMultiple($models, $data, $formName = null)
{
    if ($formName === null) {
        /* @var $first Model|false */
        $first = reset($models);
        if ($first === false) {
            return false;
        }
        $formName = $first->formName();
    }

    $success = false;
    foreach ($models as $i => $model) {
        /* @var $model Model */
        if ($formName == '') {
            if (!empty($data[$i]) && $model->load($data[$i], '')) {
                $success = true;
            }
        } elseif (!empty($data[$formName][$i]) && $model->load($data[$formName][$i], '')) {
            $success = true;
        }
    }

    return $success;
}

场景

一个Model可以用在多个场景下,比如User模型用于登录和注册场景,不同的场景会有不同的校验规则和属性
快乐飞艇 默认Model场景为default

const SCENARIO_DEFAULT = 'default';
private $_scenario = self::SCENARIO_DEFAULT;

快乐飞艇因为Model继续于Component,所以可以使用属性注入,控制器代码如下

public function actionUser(){
    $user_model = User::instance();
    $user_model -> scenario = "login";
    return $user_model->scenario;
}

会调用Model底层的getScenario和setScenario

public function getScenario()
{
    return $this->_scenario;
}
public function setScenario($value)
{
    $this->_scenario = $value;
}

也可以在实例化的时候直接给场景赋值,底层走的是BaseYii::configure,然后走属性注入的魔术方法

public function actionUser(){
    $user_model = new User(["scenario"=>"login"]);
}

验证

快乐飞艇验证需要申明验证规则,base\Model的rules方法返回的是空数组

public function rules()
{
    return [];
}

所以需要重写这个方法

class User extends Model
{
    public function rules()
    {
        return [
            [['name'], 'required'],
            [['height','sex'], 'boolean', 'on' => ['default','login']],
            [['age'], 'boolean', 'except'=>"unregister"],
        ];
    }
}

rules方法的规则如下

  • 假如没有on或者except标识,则说明本条规则在全部场景下适用
  • 假如有on标识,则说明本条规则只在on下适用
  • 假如有except标识,则说明本条规则除了except下适用
    所以以上User类的规则如下
  • defaults场景下验证name、height、sex、age
  • login场景下验证name、height、sex、age
  • unregister场景下验证name
    猎取验证规则可以使用scenarios方法

    public function scenarios()
    {
      //默认default场景
      $scenarios = [self::SCENARIO_DEFAULT => []];
      //猎取验证规则类,并且遍历
      foreach ($this->getValidators() as $validator) {
          //on下的验证规则
          foreach ($validator->on as $scenario) {
              $scenarios[$scenario] = [];
          }
          //except下的验证规则
          foreach ($validator->except as $scenario) {
              $scenarios[$scenario] = [];
          }
      }
      $names = array_keys($scenarios);
      //猎取验证规则类,并且遍历
      foreach ($this->getValidators() as $validator) {
          //假如本条验证规则没有on和except,就是全部场景下都适用
          if (empty($validator->on) && empty($validator->except)) {
              foreach ($names as $name) {
                  foreach ($validator->attributes as $attribute) {
                      $scenarios[$name][$attribute] = true;
                  }
              }
          //假如本条验证规则只在on场景下适用
          } elseif (empty($validator->on)) {
              foreach ($names as $name) {
                  //这个验证规则不在except场景下躲藏
                  if (!in_array($name, $validator->except, true)) {
                      foreach ($validator->attributes as $attribute) {
                          $scenarios[$name][$attribute] = true;
                      }
                  }
              }
          } else {
              foreach ($validator->on as $name) {
                  foreach ($validator->attributes as $attribute) {
                      $scenarios[$name][$attribute] = true;
                  }
              }
          }
      }
      foreach ($scenarios as $scenario => $attributes) {
          if (!empty($attributes)) {
              $scenarios[$scenario] = array_keys($attributes);
          }
      }
      return $scenarios;
    }
    

    猎取验证类逻辑如下

    public function getValidators()
    {
      if ($this->_validators === null) {
          $this->_validators = $this->createValidators();
      }
      return $this->_validators;
    }
    public function createValidators()
    {
      $validators = new ArrayObject();
      foreach ($this->rules() as $rule) {
          if ($rule instanceof Validator) {
              $validators->append($rule);
          } elseif (is_array($rule) && isset($rule[0], $rule[1])) { // attributes, validator type
              $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;
    }
    

    涉及到了底层了验证Validator类,验证通过createValidator方法实例化

    public static function createValidator($type, $model, $attributes, $params = [])
    {
      $params['attributes'] = $attributes;
    
      if ($type instanceof \Closure || ($model->hasMethod($type) && !isset(static::$builtInValidators[$type]))) {
          // method-based validator
          $params['class'] = __NAMESPACE__ . '\InlineValidator';
          $params['method'] = $type;
      } else {
          if (isset(static::$builtInValidators[$type])) {
              $type = static::$builtInValidators[$type];
          }
          if (is_array($type)) {
              $params = array_merge($type, $params);
          } else {
              $params['class'] = $type;
          }
      }
      return Yii::createObject($params);
    }
    

    快乐飞艇在createValidator里面实例化后还会走init

    public function init()
    {
      parent::init();
      $this->attributes = (array) $this->attributes;
      $this->on = (array) $this->on;
      $this->except = (array) $this->except;
    }
    

    可用的验证规则如下

    public static $builtInValidators = [
      'boolean' => 'yii\validators\BooleanValidator',
      'captcha' => 'yii\captcha\CaptchaValidator',
      'compare' => 'yii\validators\CompareValidator',
      'date' => 'yii\validators\DateValidator',
      'datetime' => [
          'class' => 'yii\validators\DateValidator',
          'type' => DateValidator::TYPE_DATETIME,
      ],
      'time' => [
          'class' => 'yii\validators\DateValidator',
          'type' => DateValidator::TYPE_TIME,
      ],
      'default' => 'yii\validators\DefaultValueValidator',
      'double' => 'yii\validators\NumberValidator',
      'each' => 'yii\validators\EachValidator',
      'email' => 'yii\validators\EmailValidator',
      'exist' => 'yii\validators\ExistValidator',
      'file' => 'yii\validators\FileValidator',
      'filter' => 'yii\validators\FilterValidator',
      'image' => 'yii\validators\ImageValidator',
      'in' => 'yii\validators\RangeValidator',
      'integer' => [
          'class' => 'yii\validators\NumberValidator',
          'integerOnly' => true,
      ],
      'match' => 'yii\validators\RegularExpressionValidator',
      'number' => 'yii\validators\NumberValidator',
      'required' => 'yii\validators\RequiredValidator',
      'safe' => 'yii\validators\SafeValidator',
      'string' => 'yii\validators\StringValidator',
      'trim' => [
          'class' => 'yii\validators\FilterValidator',
          'filter' => 'trim',
          'skipOnArray' => true,
      ],
      'unique' => 'yii\validators\UniqueValidator',
      'url' => 'yii\validators\UrlValidator',
      'ip' => 'yii\validators\IpValidator',
    ];
    

    假如不想用以上的验证规则,可以在自己的类里面新建一个验证规则

    class User extends Model
    {
      public function rules()
      {
          return [
              [["name"],"myValidators"]
          ];
      }
        
      public function myValidators($value)
      {
          if($value != 123){
              return false;
          }
          return true;
      }
    }
    

    执行验证的方法是validate,就是根据rules和场景来进行Validator类的验证

    public function validate($attributeNames = null, $clearErrors = true)
    {
      if ($clearErrors) {
          //清除全部验证错误信息
          $this->clearErrors();
      }
        
      //验证结尾事件
      if (!$this->beforeValidate()) {
          return false;
      }
      //猎取全部场景下的验证规则
      $scenarios = $this->scenarios();
      //猎取场景
      $scenario = $this->getScenario();
      if (!isset($scenarios[$scenario])) {
          throw new InvalidArgumentException("Unknown scenario: $scenario");
      }
      //猎取本场景下的验证规则
      if ($attributeNames === null) {
          $attributeNames = $this->activeAttributes();
      }
      $attributeNames = (array)$attributeNames;
      //猎取验证规则对应的验证类,遍历
      foreach ($this->getActiveValidators() as $validator) {
          $validator->validateAttributes($this, $attributeNames);
      }
      //验证完毕事件
      $this->afterValidate();
      //是否有验证错误信息
      return !$this->hasErrors();
    }
    

    快乐飞艇大略的验证在Validator类里面的validateAttributes方法

    public function validateAttributes($model, $attributes = null)
    {
      //猎取被验证类需要验证的属性
      $attributes = $this->getValidationAttributes($attributes);
      foreach ($attributes as $attribute) {
          //在该属性已经有错误信息或者该属性是空的情况下跳过
          $skip = $this->skipOnError && $model->hasErrors($attribute)
              || $this->skipOnEmpty && $this->isEmpty($model->$attribute);
          if (!$skip) {
              if ($this->when === null || call_user_func($this->when, $model, $attribute)) {
                  //执行验证
                  $this->validateAttribute($model, $attribute);
              }
          }
      }
    }
    

    推断为空的逻辑如下

    public function isEmpty($value)
    {
      if ($this->isEmpty !== null) {
          return call_user_func($this->isEmpty, $value);
      }
    
      return $value === null || $value === [] || $value === '';
    }
    

    假如要在申明规则下不跳过错误并且不跳过空的,可以

    public function rules()
    {
      return [
          [['name'], 'required'],
          //申明height和sex属性不跳过错误并且不跳过空
          [['height','sex'], 'boolean', 'on' => ['default','login'],'skipOnEmpty'=>false,"skipOnError"=>false],
      ];
    }
    

    快乐飞艇验证方法为各个验证类的validateValue方法,假如验证胜利会执行addError添加验证错误信息

    public function addError($attribute, $error = '')
    {
      $this->_errors[$attribute][] = $error;
    }
    
亿速云
    没有找到数据。
您需要登录后才可以评论。登录 | 马上注册