/ yii2

yii2-model

http://www.yiichina.com/doc/guide/2.0/db-active-record

关于数据:

  • 每一个 HTML Form 都是一个 Model
  • 每一个 Database Table 都是一个 ActiveRecord

最佳实践

组织 Model 代码的最佳实践:

  • 定义一组 base model 然后让不同的 applications 或 modules 共享。这些 base model 应该包含通用的 minimal sets of rules and logic
  • 在每个 application 或 module 中通过继承 base model 定义一个具体的 model,来定义 application 或 module 特有的 rules and logic。

例如,在高级开发模板中,应该首先应以 common\models\Post

  • 在 front end 定义 frontend\models\Post 继承 common\models\Post
  • 在 back end 定义 backend\models\Post 继承 common\models\Post

使用这种策略,可以让代码解耦,修改 frontend\models\Post 的 rule 和 logic 并不会 break the back end application。

文档大纲

  • 查询数据

  • 访问数据

    • 数据转换。
    • 以数组形式获取数据。
    • 批量获取数据。
  • 保存数据

    • 数据验证。安全基本准则之输入验证。
    • 块赋值。
    • 更新计数。
    • 脏属性。只有脏属性被保存,调用 $model->getDirtyAttributes() 获取当前的脏属性。
    • 默认属性值。$model->loadDefaultValues(); 填充数据库定义的默认值。
    • 属性类型转换。类型转换机制有几个限制:
      • 浮点值不被转换,并且将被表示为字符串,否则它们可能会使精度降低。
    • 更新多行数据。
  • 删除数据

  • 乐观锁。乐观锁是一种防止此冲突的方法:一行数据同时被多个用户更新。例如,同一时间内,用户 A 和用户 B 都在编辑 相同的 wiki 文章。用户 A 保存他的编辑后,用户 B 也点击“保存”按钮来 保存他的编辑。实际上,用户 B 正在处理的是过时版本的文章, 因此最好是,想办法阻止他保存文章并向他提示一些信息。

  • 使用关联数据

    • 声明关联关系。
    • 访问关联数据。
    • 动态关联查询。$orders = $customer->getOrders()->where(['>', 'subtotal', 200])->orderBy('id')->all();
    • 中间关联表。在数据建模中,当两个关联表之间的对应关系是多对多时,通常会引入一个连接表。例如 orderitem 可以通过 order_item 连接。
    • 延迟加载和即时加载(又称懒惰加载和贪婪加载)
    • 关联关系的 JOIN 查询。
    • Relation table aliases。
    • 反向关联。$this->hasMany(Order::className(), ['customer_id' => 'id'])->inverseOf('customer');注意,反向关联不能用在有连接表的关联声明中。
    • 保存关联数据。$customer = Customer::findOne(123);$order = new Order();$order->subtotal = 100;$order->link('customer', $customer);,你不能 link() 两个新的 AR 实例。(译者注:其中的一个必须是数据库中查询出来的)

Scenario

scenario 特性主要用于校验批量attribute赋值

一个 attribute 只有在 scenarios() 中被声明为 active attribute 并且在 rules() 有对应的一个或多个 active rule,这个 attribute 才会执行验证。

如果 rules() 中某个校验规则只想在特定 scenario 才会被应用,则需要使用 on 指定 scenario (场景)。

Massive Assignment

$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;

ActiveRecord

http://haobing.wang/yii2-ar

属性

字段 封装成 属性

例如:

  • first_namelast_name 封装成一个 name 属性。
  • created_at 封装成 crated,在赋值时时间字符串转换为整数型的 unix 时间戳,在读取时整数型型的 unix 时间戳转换为时间字符串

事务

$transaction = Yii::$app->db->beginTransaction();
try{
    //删除$model中的数据
    $res = $model->deleteAll($cond);
    if(!$res)
        throw new \Exception('操作失败!');
    
    //删除$model对应的$relation中的数据
    $rt = $relation->deleteAll(['polymeric_id'=>$cond['id']]);
    if(!$rt)
        throw new \Exception('操作失败!');
    
    //以上执行都成功,则对数据库进行实际执行
    $transaction->commit(); 
    return Helper::arrayReturn(['status'=>true]);
}catch (\Exception $e){
    //如果抛出错误则进入catch,先callback,然后捕获错误,返回错误
    $transaction->rollBack();
    return Helper::arrayReturn(['status'=>false,'msg'=>$e->getMessage()]);
}

场景

User 有 登录 和 注册 2 种场景。场景名称通常是使用 const 定义。然后在 rules 中某条校验规则的 on 中使用。

场景

模型可能在多个 场景 下使用,例如 User 模块可能会在收集用户登录输入, 也可能会在用户注册时使用。在不同的场景下, 模型可能会使用不同的业务规则和逻辑, 例如 email 属性在注册时强制要求有,但在登陆时不需要。

模型使用 yii\base\Model::scenario 属性保持使用场景的跟踪, 默认情况下,模型支持一个名为 default 的场景, 如下展示两种设置场景的方法:

// 场景作为属性来设置
$model = new User;
$model->scenario = 'login';

// 场景通过构造初始化配置来设置
$model = new User(['scenario' => 'login']);

默认情况下,模型支持的场景由模型中申明的 验证规则 来决定, 但你可以通过覆盖yii\base\Model::scenarios()方法来自定义行为, 如下所示:

namespace app\models;

use yii\db\ActiveRecord;

class User extends ActiveRecord
{
    const SCENARIO_LOGIN = 'login';
    const SCENARIO_REGISTER = 'register';
}

最佳实践:不要单独声明 scenarios(),只要在 rules() 中使用 on 就好。

场景的默认实现方法是列举 rules 中所有校验规则的 on。

As documentation about scenarios() says: The default implementation of this method will return all scenarios found in the rules() declaration. So generally you do not need to override this method, because it will look for on array keys to set active attributes for current scenario an validate them properly.

So in your case 'on' => 'some scenario' for different validations of the same attribute is exactly what you need.

yii2-scenarios-model-method

验证(Validate)

return [
    // 去掉输入信息的首尾空格,无法指定 message
    [['username', 'email'], 'trim'],
    
    // 默认值,使用 ActiveRecord::loadDefault() 填充为 null,无法指定 message
    [['username', 'email'], 'default'],

    // 默认值,使用 ActiveRecord::loadDefault() 填充为 1,无法指定 message
    ['level', 'default', 'value' => 1],
    
    // 校验是否存在,可以指定 message
    ['categoryid', 'exist', 'targetClass' => FaqCategory::class],
    
    // 检查 "age" 是否为整数,可以指定 message
    ['age', 'integer'],
];

FormModel 的默认值,可以直接在声明属性时定义。

如果没有 on ,则该验证规会在 all 场景下执行。

如果想让某条 rule 只在某个场景下执行,就需要在该条 rule 通过 on 来 specify 场景。

// 填充默认值 $model->loadDefaultValues();
['age', 'default', 'value' => 0],

客户端验证

许多核心验证器都支持开箱即用的客户端验证。你只需要用 yii\widgets\ActiveForm 的方式构建 HTML 表单即可。比如,下面的 LoginForm(登录表单)声明了两个规则:其一为 required 核心验证器,它同时支持客户端与服务器端的验证;另一个则采用 validatePassword 行内验证器,它只支持服务器端。

public function rules()
{
    return [
       // username 和 password 都是必填项
       [['username', 'password'], 'required'],

       // 用 validatePassword() 验证 password
       ['password', 'validatePassword'],
    ];
}

public function validatePassword()
{
   $user = User::findByUsername($this->username);

   if (!$user || !$user->validatePassword($this->password)) {
      $this->addError('password', 'Incorrect username or password.');
   }
}

若你需要完全关闭客户端验证,你只需配置 yii\widgets\ActiveForm::$enableClientValidation 属性为 false。你同样可以关闭各个输入框各自的客户端验证, 只要把它们的 yii\widgets\ActiveField::$enableClientValidation 属性设为 false

AJAX 验证

有些验证只能在服务器端完成,例如验证用户名的唯一性,这就需要检查数据库。我们可以基于 Ajax 来验证这种情况,通过在客户端校验中触发 Ajax 请求来验证输入框中的数据。

为了验证某个输入框的数据,需要给 form 添加 id 属性:

use yii\widgets\ActiveForm;

$form = ActiveForm::begin([
    'id' => 'registration-form',
]);

echo $form->field($model, 'username', ['enableAjaxValidation' => true]);

// ...

ActiveForm::end();

如果需要对整个表单进行 ajax 验证,需要给 form 设置enableAjaxValidation 属性为 true(因为默认是 false)

同时还需要对服务器端改造,以处理 ajax 请求:

use yii\web\Response;
use yii\widgets\ActiveForm;

if (Yii::$app->request->isAjax && $model->load(Yii::$app->request->post())) {
    Yii::$app->response->format = Response::FORMAT_JSON;
    return ActiveForm::validate($model);
}

这样当鼠标离开输入框时,就会发起 ajax 请求。但是请求的得到的错误信息如何显示?

Ajax 返回的信息如 {"loginform-username":["该手机号未注册"]},对应的字段是 username,为了显示信息,我们需要确保 username 的 input 的父元素id 为 loginform-username,这样信息才能正常显示。

手动验证

如果需要关闭自动验证,可以在创建表单时设置:

$form =  ActiveForm::begin([
    'id' => 'form-signup',
    'validationUrl' => $validateUrl,
    'validateOnSubmit' => false,// 关闭自动验证
    'validateOnChange' => false,// 关闭自动验证
    'validateOnBlur' => false,// 关闭自动验证
]);

然后配置 js:

$('form').submit(function(event) {
  event.preventDefault();
  var form$ = $(this),data = form$.data("yiiActiveForm");
  $.each(data.attributes, function() {
      this.status = 3;
  });
  form$.yiiActiveForm("validate");
});

trigger active form validation manually before submit

校验事件函数:

$("#FORM-ID").on("afterValidate", function (event, messages) {
  // Now you can work with messages by accessing messages variable
  var attributes = $(this).data().attributes; // to get the list of attributes that has been passed in attributes property
  var settings = $(this).data().settings; // to get the settings
});

Behavior :自动维护任务

  • yii\behaviors\AttributeBehavior
  • yii\behaviors\AttributeTypecastBehavior
  • yii\behaviors\AttributesBehavior
  • yii\behaviors\BlameableBehavior
  • yii\behaviors\SluggableBehavior
  • yii\behaviors\TimestampBehavior

利用 Behavior 我们可以实现以下功能:

  • 自动维护时间
  • 自动提取汉字拼音首字母
  • 自动维护数据更新人员

自动维护的字段,均可以在 rule 中设置为 safe

自动维护时间
use yii\behaviors\TimestampBehavior;
public function behaviors()
{
     return [
         TimestampBehavior::className(),
     ];
}

public function rules()
{
    return [
        [['created_at','updated_at'], 'safe'],
    ];
}
use yii\behaviors\TimestampBehavior;

public function behaviors()
{
    return [
        [
            'class' => TimestampBehavior::className(),
            'createdAtAttribute' => 'create_at',
            'updatedAtAttribute' => 'update_at',
        ]
    ];
}

public function rules()
{
    return [
        [['create_at','update_at'], 'safe'],
    ];
}
use yii\behaviors\TimestampBehavior;
use yii\db\Expression;

public function behaviors()
{
    return [
        [
            'class' => TimestampBehavior::className(),
            'createdAtAttribute' => 'create_at',
            'updatedAtAttribute' => 'update_at',
            'value' => new Expression('NOW()'),
        ]
    ];
}

public function rules()
{
    return [
        [['create_at','update_at'], 'safe'],
    ];
}
自动维护拼音首字母

在将 create 或 update 之前,提取标题的首字母:

use yii\behaviors\AttributeBehavior;
public function behaviors()
{
    return [
        [
            'class' => AttributeBehavior::className(),
            'attributes' => [
                ActiveRecord::EVENT_BEFORE_INSERT => 'letter',
                ActiveRecord::EVENT_BEFORE_UPDATE => 'letter',
            ],
            'value' => function ($event) {
                return $this->getFirstCharter($this->title);
            }
        ]
    ];
}

public function getFirstCharter($str){
    // 具体实现
}
自动维护数据维护人员
use yii\behaviors\BlameableBehavior;

public function behaviors()
{
    return [
        // 自动维护修改人,默认字段 created_by updated_by
        BlameableBehavior::className(),
    ];
}

public function getCreateUser()
{
    return $this->hasOne(User::className(), ['id' => 'created_by']);
}

public function getCreateUsername()
{
    return $this->createUser ? $this->createUser->username : '- system -';
}

public function getupdateUser()
{
    return $this->hasOne(User::className(), ['id' => 'updated_by']);
}

public function getupdateUsername()
{
    return $this->updateUser ? $this->updateUser->username : '- system -';
}

使用数据库中定义的默认值

public function actionCreate()
{
    $model = new Product();

    // 创建数据时通常需要从数据库中拉取默认值填充 model
    $model->loadDefaultValues();

    if ($model->load(Yii::$app->request->post()) && $model->save()) {
        return $this->redirect(['view', 'id' => $model->id]);
    } else {
        return $this->render('create', [
            'model' => $model,
        ]);
    }
}

默认排序

在 Search Mode 中的 search() 方法中:

$query = License::find();

        // add conditions that should always apply here

        $dataProvider = new ActiveDataProvider([
            'query' => $query,
            // 设置默认排序
            'sort' => [
                'defaultOrder' => [
                    // 默认按到期时间排序
                    '_id' => SORT_DESC,
                ]
            ]
        ]);

创建数据表模型

创建一个继承自活动记录类的类 Country, 把它放在 models/Country.php 文件,去代表和读取 country 表的数据。

<?php

namespace app\models;

use yii\db\ActiveRecord;

class Country extends ActiveRecord
{
}

这个 Country 类继承自 yii\db\ActiveRecord。你不用在里面写任何代码。 只需要像现在这样,Yii 就能根据类名去猜测对应的数据表名。

注意: 如果类名和数据表名不能直接对应,可以覆写 tableName() 方法去显式指定相关表名。

$model = new User;
//...

$model->getError();

关联 1:1

订单属于某个顾客

class Order extends \yii\db\ActiveRecord
{
    // 订单和客户通过 Customer.id -> customer_id 关联建立一对一关系
    public function getCustomer()
    {
        return $this->hasOne(Customer::className(), ['id' => 'customer_id']);
    }
}

关联 1:n

一个顾客,可以有多个订单

class Customer extends \yii\db\ActiveRecord
{
    public function getOrders()
    {
        // 客户和订单通过 Order.customer_id -> id 关联建立一对多关系
        return $this->hasMany(Order::className(), ['customer_id' => 'id']);
    }
}

建立关联关系后,获取关联数据和获取组件属性一样简单, 执行以下相应getter方法即可:

// 取得客户的订单
$customer = Customer::findOne(1);
$orders = $customer->orders; // $orders 是 Order 对象数组

以上代码实际执行了以下两条 SQL 语句:

SELECT * FROM customer WHERE id=1;
SELECT * FROM order WHERE customer_id=1;

Tip: 再次用表达式 $customer->orders将不会执行第二次 SQL 查询, SQL 查询只在该表达式第一次使用时执行。 数据库访问只返回缓存在内部前一次取回的结果集,如果你想查询新的 关联数据,先要注销现有结果集:unset($customer->orders);。

有时候需要在关联查询中传递参数,如不需要返回客户全部订单, 只需要返回购买金额超过设定值的大订单, 通过以下getter方法声明一个关联数据 bigOrders :

class Customer extends \yii\db\ActiveRecord
{
    public function getBigOrders($threshold = 100)
    {
        return $this->hasMany(Order::className(), ['customer_id' => 'id'])
            ->where('subtotal > :threshold', [':threshold' => $threshold])
            ->orderBy('id');
    }
}

如上声明后,执行 $customer->bigOrders 就返回 总额大于100的订单。使用以下代码更改设定值:

$orders = $customer->getBigOrders(200)->all();

关联 n:n

订单和商品之间就是典型的 多对多 问题,一个订单可能有多个商品,一个商品也可能出现在不同的订单中。

有时,两个表通过中间表关联,定义这样的关联关系, 可以通过调用 yii\db\ActiveQuery::via() 方法或 viaTable() 方法来定制 yii\db\ActiveQuery 对象 。

举例而言,如果 order 表和 item 表通过中间表 order_item 关联起来, 可以在 Order 类声明 items 关联关系取代中间表:

class Order extends \yii\db\ActiveRecord
{
    public function getItems()
    {
        return $this->hasMany(Item::className(), ['id' => 'item_id'])
            ->viaTable('order_item', ['order_id' => 'id']);
    }
}

仔细为Model方法命名

由于Model的代码量比较大,又集中了大量的逻辑,因此,会在一个Model中有大量的方法。仍然以Post为例, 会涉及到创建、审核、发布、回收等流程,相关的方法比较多,在命名上要用心。 可能会涉及到的、名字又比较接近的方法就有:

  • getPrevPost(),前一篇文章,用于导航
  • getNextPost(),下一篇文章,用于导航
  • getRelatedPosts($n = 10),获取相关的N篇文章,用于相关文章推荐列表
  • getPostsOfAuthor($n = 10),获取同一作者的N篇相关文章,用于作者文章列表
  • getLatestPosts($n = 10),最新的N篇文章,静态方法,用于文章列表或RSS输出
  • getHotestPosts($n = 10),最热门的N篇文章,静态方法,用于热门文章列表
  • getPublishPosts($n = -1),获取已经发布的N篇文章,静态方法,用于文章列表
  • getDraftPosts($n = -1),获取未发布的N篇文章,静态方法,用于作者页面

这里只是一些获取其他Post的方法,命名比较合理,一看就知道意思。 而且全部写成getter的形式,可以使用读取属性的方式进行访问。

不单单是在Model方法的命名上要用心, 在变量名、类名、方法名等的命名上,也要养成习惯,形成规律。 不要图一时之快,胡乱起名。否则,出来混,迟早要还的。

问题

$activeRecord->load()$activeRecord->attributes() 区别?