ActiveRecord预约义的事件,都在 yiidbBaseActiveRecord 中进行了明确:html
abstract class BaseActiveRecord extends Model implements ActiveRecordInterface { const EVENT_INIT = 'init'; // 初始化对象时触发 const EVENT_AFTER_FIND = 'afterFind'; // 执行查询结束时触发 const EVENT_BEFORE_INSERT = 'beforeInsert'; // 插入结束时触发 const EVENT_AFTER_INSERT = 'afterInsert'; // 插入以前触发 const EVENT_BEFORE_UPDATE = 'beforeUpdate'; // 更新记录前触发 const EVENT_AFTER_UPDATE = 'afterUpdate'; // 更新记录后触发 const EVENT_BEFORE_DELETE = 'beforeDelete'; // 删除记录前触发 const EVENT_AFTER_DELETE = 'afterDelete'; // 删除记录后触发 // ... ... } |
上述常量,定义了ActiveRecord对象经常使用的几个事件。这是预约义事件,咱们能够直接拿来 用。事件的定义具体看事件(Event) 部分的内容。linux
此外,做为ActiveRecord类的祖宗, yiibaseModel 类也定义了2个事件:ios
class Model extends Component implements IteratorAggregate, ArrayAccess, Arrayable { const EVENT_BEFORE_VALIDATE = 'beforeValidate'; // 在验证以前触发 const EVENT_AFTER_VALIDATE = 'afterValidate'; // 在验证以后触发 // ... ... } |
所以,上述总共10个事件,可供开发者在写入业务流程时使用。数据库
从上述事件来看,能够看出大部分事件是分别以before和after打头的成对事件。 有些是“读”操做时才会触发的事件,有些是“写”操做时发生的事件。编程
并且,“写”与“写”之间也是相互区别的。好比,增、改、删3个写操做, 都各自有一对事件前后在不一样场景下触发。但这3种“写”操做不会被同时触发。数组
首先,第一个事件,无可争议的,是 EVENT_INIT 。这是由 yii\base\Object 所决定的。该事件在 init() 方法中被触发。而咱们在 属性(Property) 中已经说过这个方法是最先调用的几个方法之一。具体代码:框架
public function init() { parent::init(); // 这里触发EVENT_INIT事件 $this->trigger(self::EVENT_INIT); } |
虽然这个事件触发得早,可是实际使用中,这个事件使用频率不高。 仅是由于有的代码不得不在初始化阶段执行,因此才提供了这个事件。 并且,这个事件因为所处阶段特殊,不像有的事件,能够有必定的替代性。yii
好比, EVENT_AFTER_VALIDATE 和 EVENT_BEFORE_UPDATE 尽管泾渭分明, 可是因为是相继触发,因此某些状况下能够在必定程度上互相替代。可是, 上述10个事件中,仅有 EVENT_INIT 是在初始化阶段触发。因此,其具备不可替代性。ide
EVENT_INIT 事件一般用于初始化一些东西,从模块化的角度, 能够简单当作是将当前类的 init() 方法的内容, 做为一个Event Handler单独划分为一个模块。模块化
EVENT_AFTER_FIND 事件在完成查询后触发,注意该事件少有地没有对应的Before事件。
另一个区别于其余事件的不一样在于,该事件并不是由 ActiveRecord 自身触发。 而是由 yii\db\ActiveQuery 触发。准确的触发时点,是在查询完成后, 向ActiveRecord填充字段所有内容后触发。
具体代码在 yii\db\ActiveQuery::populate()
// 该方法为ActiveQuery将查询到的内容 $rows 填充到ActiveReocrd中去的方法 public function populate($rows) { if (empty($rows)) { return []; } $models = $this->createModels($rows); if (!empty($this->join) && $this->indexBy === null) { $models = $this->removeDuplicatedModels($models); } if (!empty($this->with)) { $this->findWith($this->with, $models); } if (!$this->asArray) { // 重点在这个foreach里面的afterFind(), // afterFind()不干别的,就是专门调用 // $this->trigger(self::EVENT_AFTER_FIND) 来触发事件的。 foreach ($models as $model) { $model->afterFind(); } } return $models; } |
上面的代码咱们能够看出,在完成查询以后,查询到了多少个记录, 就会触发多少次实例级别的 EVENT_AFTER_FIND 事件。 事件的级别,请看 事件(Event) 部分的内容。
EVENT_AFTER_FIND 事件,用于查询后一些内容的填充。好比,有一个ActiveRecord, 专门用于表示博客文章,那么一般他有一个字段,用于表示发布的确切时间。
假设客户但愿在前台显示文章时,不直接显示确切时间,而是显示如“3分钟前” “2个月前”之类的相对时间。那么,咱们就须要有一个将绝对时间转化成相对时间的过程。
那么,就能够把这个转换过程的代码,写在 EVENT_AFTER_FIND 事件的Event Handler里。
验证事件是在验证时前后触发的2个事件,这2个事件均由 yii\base\Model::validate 触发:
public function validate($attributeNames = null, $clearErrors = true) { if ($clearErrors) { $this->clearErrors(); } // 这里的 beforeValidate() 会调用 // $this->trigger(self::EVENT_BEFORE_VALIDATE, $event) // 来触发 EVENT_BEFORE_VALIDATE 事件。 if (!$this->beforeValidate()) { return false; } // 下面是后续的验证代码,这里不用过多关注 $scenarios = $this->scenarios(); $scenario = $this->getScenario(); if (!isset($scenarios[$scenario])) { throw new InvalidParamException("Unknown scenario: $scenario"); } if ($attributeNames === null) { $attributeNames = $this->activeAttributes(); } foreach ($this->getActiveValidators() as $validator) { $validator->validateAttributes($this, $attributeNames); } // 这里的 afterValidate() 会调用 // $this->trigger(self::EVENT_AFTER_VALIDATE) // 来触发 EVENT_AFTER_VALIDATE 事件。 $this->afterValidate(); return !$this->hasErrors(); } |
这两个事件正如其名称所表示的,触发顺序为先 EVENT_BEFORE_VALIDATE 后 EVENT_AFTER_VALIDATE 。
这两个事件中, EVENT_BEFORE_VALIDATE 经常使用于验证前的一些规范化处理。 仍以博客文章的发布时间字段为例,在接收用户输入时, 咱们的应用接收一个字符相似“2015年3月8日”之类的字符串。
可是数据库中咱们通常并不以字符串形式保存时间,而是使用一个整型字段来保存。 这主要涉及存储空间,日期比较和排序,检索效率等数据库优化问题,具体不展开。 反正咱们就是想把时间以整型形式进行保存。
那么,在验证用户输入以前,咱们就须要将字符串类型的日期时间, 转换成整型类型的日期时间。不然的话,验证就通不过。
这个转换过程,就能够写在 EVENT_BEFORE_VALIDATE 的 Event Handler里面。
EVENT_BEFORE_VALIDATE 还有一个比较吸引人的特性, 它的Event Handler能够返回一个 boolean 值,当为 false 时, 表示后续的验证不必进行了:
public function beforeValidate() { // 建立一个 ModelEvent,并交给 trigger 在触发事件时使用 $event = new ModelEvent; $this->trigger(self::EVENT_BEFORE_VALIDATE, $event); return $event->isValid; } |
上面的代码中, trigger() 将传入的第二个 $event 传递给 Event Handler, 使得相关的这些个 Event Handler 能够在必要时修改 $event->isValid 的值。 以此来决定是否能够取消后续的验证,直接视为验证没有经过。
EVENT_AFTER_VALIDATE 一般用于用户输入验证后的一些处理。好比, 用于写入操做前的一些通用处理。由于后头接下来的事件, 会分红插入、更新等独立事件。若是有一些写入前的通用处理,放在 EVENT_AFTER_VALIDATE 阶段是比较合适的。
至于验证经过与否,与 EVENT_AFTER_VALIDATE 事件没有关系,只要执行完全部验证了, 这个事件就会被触发。并且,该事件的Event Handler没有返回值,没法干预验证结果。
“写”事件是指插入、更新、删除等写入操做时触发的事件。通常状况下, 验证事件先于“写”事件被触发。
但这不是绝对的。Yii容许在执行“写”操做时,不调用 validate() 进行验证, 也就不触发验证事件。
下面,咱们以更新操做update为例,来分析“写”事件。
首先,来看看 yii\db\BaseActiveRecord 里的有关代码:
public function save($runValidation = true, $attributeNames = null) { // insert() 和 update() 具体实现由ActiveRecord定义 if ($this->getIsNewRecord()) { return $this->insert($runValidation, $attributeNames); } else { return $this->update($runValidation, $attributeNames) !== false; } } // updateInternal() 由 update() 调用, // 相似的有deleteInternal() ,由ActiveRecord定义,这里略去。 protected function updateInternal($attributes = null) { // beforeSave() 会触发相应的before事件 // 并且若是beforeSave()返回false,就能够停止更新过程。 if (!$this->beforeSave(false)) { return false; } $values = $this->getDirtyAttributes($attributes); // 没有字段有修改,那么其实是不须要更新的。 // 所以,直接调用afterSave()来触发相应的after事件。 if (empty($values)) { $this->afterSave(false, $values); return 0; } // 如下为实际更新操做,没必要细究。 $condition = $this->getOldPrimaryKey(true); $lock = $this->optimisticLock(); if ($lock !== null) { $values[$lock] = $this->$lock + 1; $condition[$lock] = $this->$lock; } $rows = $this->updateAll($values, $condition); if ($lock !== null && !$rows) { throw new StaleObjectException('The object being updated is outdated.'); } $changedAttributes = []; foreach ($values as $name => $value) { $changedAttributes[$name] = isset($this->_oldAttributes[$name]) ? $this->_oldAttributes[$name] : null; $this->_oldAttributes[$name] = $value; } // 重点看这里,触发了事件 $this->afterSave(false, $changedAttributes); return $rows; } // 下面的beforeSave()和afterSave() 会根据判断是更新操做仍是插入操做, // 以此来决定是触发 INSERT 事件仍是 UPDATE 事件。 public function beforeSave($insert) { $event = new ModelEvent; // $insert 为 true 时,表示当前是插入操做,是个新记录,要触发INSERT事件 // $insert 为 false时,表示当前是插入操做,是个新记录,要触发INSERT事件 $this->trigger($insert ? self::EVENT_BEFORE_INSERT : self::EVENT_BEFORE_UPDATE, $event); return $event->isValid; } public function afterSave($insert, $changedAttributes) { // $insert 为 true 时,表示当前是插入操做,是个新记录,要触发INSERT事件 // $insert 为 false时,表示当前是插入操做,是个新记录,要触发INSERT事件 $this->trigger($insert ? self::EVENT_AFTER_INSERT : self::EVENT_AFTER_UPDATE, new AfterSaveEvent([ 'changedAttributes' => $changedAttributes ])); } |
就“写”操做而言,表面上调用的是 ActiveRecord 的 update() insert() delete() 等方法。
可是,更新最终调用的是 BaseActiveRecord::updateInteranl() , 插入最终调用的是 ActiveRecord::insertInternal() , 而删除最终调用的是 ActiveRecord::deleteInternal() 。
这些 internal 方法,会触发相应的“写”事件,但不会调用验证方法, 也不会触发验证事件。验证方法 validation() 由 update() insert() 调用。 所以,验证事件也由这两个方法触发。
并且,这些 update() insert() 能够选择不进行验证,在压根不触发验证事件的状况下,就能够完成“写”操做。
所以,虽然 EVENT_AFTER_VALIDATE 和 EVENT_BEFORE_UPDATE 相继发生, 在使用上有时能够有必定程度的替代。可是,其实二者是有严格界限的。 缘由就是验证事件可能在“写”操做过程当中不被触发。
此外,删除过程不触发验证事件。都要删掉的东西了,还须要验证么?
对于 internal 方法们,只是触发了相应的before和after“写”事件。
其中,before事件的Event Handler能够经过将 $event->isValid 设为 false 来停止“写”操做。
与在验证事件阶段停止时,视为验证没经过不一样,这里的停止视为“写”操做失败。
与验证事件阶段相似,after事件时因为生米已成熟饭,再也没法干预“写”操做的结果。
前面提到的诸多预约义事件,为咱们开发提供了方便。基本上使用这些预约义事件, 就能够知足各类开发需求了。
可是凡事总有例外,特别是对于业务逻辑复杂的状况。 好比,默认的删除事件,会在确确实实地要从数据表中删除记录时触发。 可是,有的时候,咱们并不是真的想从数据表中删除记录,咱们可能使用一个相似于“状态” 的字段,在想要删除时,只是将记录的“状态”标记为“删除”。
这种需求并很多见。这样便于咱们在后悔时,能够“恢复”删除。
从实质上是看,这实际上是一个更新操做。那么预约义的 EVENT_BEFORE_DELETE 和 EVENT_AFTER_DELETE 就不适用了。
对此,咱们能够本身定义事件来使用。具体的方法能够参见 事件(Event) 部分的内容。
大体的代码能够是这样的:
class Post extends \yii\db\ActiveRecord { // 第一步:定义本身的事件 const EVENT_BEFORE_MARK_DELETE = 'beforeMarkDelete'; const EVENT_AFTER_MARK_DELETE = 'afterMarkDelete'; // 第二步:定义Event Handler public function onBeforeMarkDelete () { // ... do sth ... } // 第三步:在初始化阶段绑定事件和Event Handler public function init() { parent::init(); $this->trigger(self::EVENT_INIT); // 完成绑定 $this->on(self::EVENT_BEFORE_MARK_DELETE, [$this, 'onBeforeMarkDelete']); } // 第四步:触发事件 public function beforeSave($insert) { // 注意,重载以后要调用父类同名函数 if (parent::beforeSave($insert)) { $status = $this->getDirtyAttributes(['status']); // 这个判断意会便可 if (!empty($status) && $status['status'] == self::STATUS_DELETE) { // 触发事件 $this->trigger(self::EVENT_BEFORE_MARK_DELETE); } return true; } else { return false; } } } |
上面的代码理解个大体流程就OK了,不用细究。
在事件的响应上,咱们有2个方法来写入咱们的代码。
最直观的方式,是使用 事件(Event) 中介绍的 Event Handler。也就是上面代码展示的, 为类定义一个成员函数,做为Event Handler。同时,在类的构造函数或初始化方法中, 把事件和Event Handler绑定起来。最后,在合适的时候,触发事件便可。
另外一种方式,是直接重载上面屡次提到的各类 beforeSave() afterSave() beforeValidate() afterValidate() 等方法。好比,上面的例子能够改为:
class Post extends \yii\db\ActiveRecord { // 不须要定义本身的事件 //const EVENT_BEFORE_MARK_DELETE = 'beforeMarkDelete'; //const EVENT_AFTER_MARK_DELETE = 'afterMarkDelete'; // 不须要定义Event Handler //public function onBeforeMarkDelete () { // ... do sth ... //} // 不须要绑定事件和Event Handler //public function init() //{ // parent::init(); // $this->trigger(self::EVENT_INIT); // $this->on(self::EVENT_BEFORE_MARK_DELETE, [$this, 'onBeforeMarkDelete']); //} // 只须要重载 public function beforeSave($insert) { // 注意,重载以后要调用父类同名函数 if (parent::beforeSave($insert)) { $status = $this->getDirtyAttributes(['status']); // 这个判断意会便可 if (!empty($status) && $status['status'] == self::STATUS_DELETE) { // 不须要触发事件 //$this->trigger(self::EVENT_BEFORE_MARK_DELETE); // 可是须要把原来 Event Handler的内容放到这里来 // ... do sth ... } return true; } else { return false; } } } |
对比来看,重载 beforeSave() 的方式要简洁不少。可是这种方式从严格意义上来说, 并非正规的事件处理机制。只不过是利用了Yii已经预先定义好的函数调用流程。 在使用中,须要格外注意的是,必定要在重载的函数中,调用父类的同名函数。不然的话, trigger() 再也不被自动调用,相关事件就不会再被触发。整个类的事件机制, 就全被破坏了。
在实际开发中,有一种典型的场景,即对数据库某个表的某个记录进行修改时,须要对关联 的表中的相关记录作相应的修改。
好比,一个典型的博客,表示文章的数据表中有一个字段用于记录当前文章有多少条评论。 那么,当用户发表新评论时,另外一个用于表示评论的表中,理所固然地要插入一条新记录。 不可避免的,文章表中,被评论文章所对应的记录,其评论计数字段应当加1。
那么这一过程怎么编程实现呢?
最直白的方法,是在操做评论记录的代码以前(后),写入相应的增长文章评论计数的代码 。 这样好理解,可是不一样功能代码的界限不清晰。
另外一种方法,是借助事件(Event),将增长文章评论计数的代码,写到 评论ActiveReocrd的相应Event Handler中。好比, EVENT_AFTER_INSERT 。
这样子代码功能界限清晰,便于查找、修改和扩展。 缺点是可能须要多看几个方法才能了解整个业务流程。实际中咱们多采用这种方法。
在实现数据库记录的关联操做时,第一步就是要利用上述的各类事件,来产生关联性。 其次,是要把这些关联性绑死在一块儿。也就是用数据库的事务。具体的原理, 参考 事务 部分的内容。
下面,咱们以上面提到的博客文章新增一个评论为例,讲解如何实现关联操做。
在ActiveRecord中有一个方法,用于告诉Yii咱们的哪些操做须要事务支持。对于插入、 更新、删除的1个或多个操做须要事务支持时,能够在这个方法中进行声明。 这个方法就是 ActiveRecord::transactions()
class ActiveRecord extends BaseActiveRecord { // 定义插入、更新、删除操做,及表示3合1的ALL const OP_INSERT = 0x01; const OP_UPDATE = 0x02; const OP_DELETE = 0x04; const OP_ALL = 0x07; // 须要事务支持时,重载这个方法。 public function transactions() { return []; } // ... ... } |
默认状况下,这个 transactions() 返回一个空数组,表示不须要任何的事务支持。
咱们的博客文章增长评论的案例中是要用到的,那么,咱们能够在评论模型 Comment 中,做以下声明:
public function transactions() { return [ 'addComment' => self::OP_INSERT, ]; } |
这个方法所返回的数组中,元素的键,如上面的 addComment 表示场景(scenario), 元素值,表示的是操做的类型,即 OP_INSERT 等。
ActiveRecord定义了3种可能会用到事务支持的操做 OP_INSERT OP_UPDATE OP_DELETE 分别表示插入、更新、删除。
能够把这3个操做两两组合做为 transactions() 所返回数组元素的值。 如, self::OP_INSERT|self::OP_UPDATE `` 表示插入和更新操做。 也能够直接使用 ``OP_ALL 表示3种操做都包含。
上一步中的 transactions() 被 ActiveRecord::isTransactional() 所调用:
// $operation就是预约义的OP_INSERT 等3种单一操做类型 public function isTransactional($operation) { // 获取当前的scenario $scenario = $this->getScenario(); $transactions = $this->transactions(); return isset($transactions[$scenario]) && ($transactions[$scenario] & $operation); } |
这个 isTransactional() 就是判断当前场景下,当前操做类型,是否已经在 transactions() 中声明为须要事务支持。
而这个 isTransactional() 又被各类“写”操做方法所调用。 在咱们的博客文章新增评论的案例中,就是被 insert() 所调用:
public function insert($runValidation = true, $attributes = null) { if ($runValidation && !$this->validate($attributes)) { Yii::info('Model not inserted due to validation error.', __METHOD__); return false; } // 这里调用了 isTransactional(),判断当前场景下, // 插入操做是否须要事务支持 if (!$this->isTransactional(self::OP_INSERT)) { // 无需事务支持,那就直接insert了事 return $this->insertInternal($attributes); } // 如下是须要事务支持的状况,那就启用事务 $transaction = static::getDb()->beginTransaction(); try { $result = $this->insertInternal($attributes); if ($result === false) { $transaction->rollBack(); } else { $transaction->commit(); } return $result; } catch (\Exception $e) { $transaction->rollBack(); throw $e; } } |
很明显的,咱们只须要在 transactions() 中声明须要事务支持的操做就足够了。 后续的怎么使声明生效的,Yii框架已经替咱们写好了。
在上面 insert() 的代码中,经过咱们的声明,Yii发现须要事务支持, 因而就调用了 static::getDb()->beginTransaction() 来启用事务。 事务的原理,请看 事务 部分的内容。
接下来,咱们在关联的事件,如 EVENT_AFTER_INSERT 中,写入关联操做。 这里,咱们就是要更新博客文章模型 Post 的评论计数字段。
所以,能够在评论模型 Comment 完成插入后的 EVENT_AFTER_INSERT 阶段, 写入更新 Post::comment_counter 的代码。若是使用简洁形式的事件响应方式, 那么代码能够是:
class Comment extends \yii\db\ActiveRecord { // 经过重载afterSave来“响应”事件 public function afterSave($insert) { if (parent::beforeSave($insert)) { // 新增一个评论 if ($insert) { // 关联Post的操做,评论计数字段+1 $post = Post::find($this->postId); $post->comment_counter += 1; $post->save(false); } } } } |
回顾下实现关联操做的过程,其实就2步:
先是在 transactions() 中声明要事务支持的操做类型,好比上面的例子, 声明的是插入操做。
在合适事件响应函数中,写下关联操做代码。
本文来自:Linux教程网