ActiveRecord預定義的事件,都在?yiidbBaseActiveRecord?中進行了明確:
~~~
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)_](http://www.digpage.com/event.html#event)?部分的內容。
此外,作為ActiveRecord類的祖宗,?yiibaseModel?類也定義了2個事件:
~~~
class Model extends Component implements IteratorAggregate, ArrayAccess, Arrayable
{
const EVENT_BEFORE_VALIDATE = 'beforeValidate'; // 在驗證之前觸發
const EVENT_AFTER_VALIDATE = 'afterValidate'; // 在驗證之后觸發
// ... ...
}
~~~
因此,上述總共10個事件,可供開發者在寫入業務流程時使用。
從上述事件來看,可以看出大部分事件是分別以before和after打頭的成對事件。 有些是“讀”操作時才會觸發的事件,有些是“寫”操作時發生的事件。
而且,“寫”與“寫”之間也是相互區別的。比如,增、改、刪3個寫操作, 都各自有一對事件先后在不同場景下觸發。但這3種“寫”操作不會被同時觸發。
## 初始化事件[](http://www.digpage.com/active_record.html#id1 "Permalink to this headline")
首先,第一個事件,無可爭議的,是?EVENT_INIT?。這是由?yii\base\Object?所決定的。該事件在init()?方法中被觸發。而我們在?[_屬性(Property)_](http://www.digpage.com/property.html#property)?中已經說過這個方法是最早調用的幾個方法之一。具體代碼:
~~~
public function init()
{
parent::init();
// 這里觸發EVENT_INIT事件
$this->trigger(self::EVENT_INIT);
}
~~~
雖然這個事件觸發得早,但是實際使用中,這個事件使用頻率不高。 僅是因為有的代碼不得不在初始化階段執行,所以才提供了這個事件。 而且,這個事件由于所處階段特殊,不像有的事件,可以有一定的替代性。
比如,?EVENT_AFTER_VALIDATE?和?EVENT_BEFORE_UPDATE?盡管涇渭分明, 但是由于是相繼觸發,所以某些情況下可以在一定程度上互相替代。但是, 上述10個事件中,僅有?EVENT_INIT?是在初始化階段觸發。所以,其具有不可替代性。
EVENT_INIT?事件通常用于初始化一些東西,從模塊化的角度, 可以簡單看成是將當前類的?init()方法的內容, 作為一個Event Handler單獨劃分為一個模塊。
## AfterFind事件[](http://www.digpage.com/active_record.html#afterfind "Permalink to this headline")
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)_](http://www.digpage.com/event.html#event)?部分的內容。
EVENT_AFTER_FIND?事件,用于查詢后一些內容的填充。比如,有一個ActiveRecord, 專門用于表示博客文章,那么通常他有一個字段,用于表示發布的確切時間。
假設客戶希望在前臺顯示文章時,不直接顯示確切時間,而是顯示如“3分鐘前” “2個月前”之類的相對時間。那么,我們就需要有一個將絕對時間轉化成相對時間的過程。
那么,就可以把這個轉換過程的代碼,寫在?EVENT_AFTER_FIND?事件的Event Handler里。
## 驗證事件[](http://www.digpage.com/active_record.html#id2 "Permalink to this headline")
驗證事件是在驗證時先后觸發的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沒有返回值,無法干預驗證結果。
## “寫”事件[](http://www.digpage.com/active_record.html#id3 "Permalink to this headline")
“寫”事件是指插入、更新、刪除等寫入操作時觸發的事件。一般情況下, 驗證事件先于“寫”事件被觸發。
但這不是絕對的。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事件時由于生米已成熟飯,再也無法干預“寫”操作的結果。
## 響應事件[](http://www.digpage.com/active_record.html#id4 "Permalink to this headline")
前面提到的諸多預定義事件,為我們開發提供了方便。基本上使用這些預定義事件, 就可以滿足各種開發需求了。
但是凡事總有例外,特別是對于業務邏輯復雜的情況。 比如,默認的刪除事件,會在確確實實地要從數據表中刪除記錄時觸發。 但是,有的時候,我們并非真的想從數據表中刪除記錄,我們可能使用一個類似于“狀態” 的字段,在想要刪除時,只是將記錄的“狀態”標記為“刪除”。
這種需求并不少見。這樣便于我們在后悔時,可以“恢復”刪除。
從實質上是看,這其實是一個更新操作。那么預定義的?EVENT_BEFORE_DELETE?和?EVENT_AFTER_DELETE就不適用了。
對此,我們可以自己定義事件來使用。具體的方法可以參見?[_事件(Event)_](http://www.digpage.com/event.html#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)_](http://www.digpage.com/event.html#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()?不再被自動調用,相關事件就不會再被觸發。整個類的事件機制, 就全被破壞了。
## 關聯操作[](http://www.digpage.com/active_record.html#id5 "Permalink to this headline")
在實際開發中,有一種典型的場景,即對數據庫某個表的某個記錄進行修改時,需要對關聯 的表中的相關記錄做相應的修改。
比如,一個典型的博客,表示文章的數據表中有一個字段用于記錄當前文章有多少條評論。 那么,當用戶發表新評論時,另一個用于表示評論的表中,理所當然地要插入一條新記錄。 不可避免的,文章表中,被評論文章所對應的記錄,其評論計數字段應當加1。
那么這一過程怎么編程實現呢?
最直白的方法,是在操作評論記錄的代碼之前(后),寫入相應的增加文章評論計數的代碼 。 這樣好理解,但是不同功能代碼的界限不清晰。
另一種方法,是借助事件(Event),將增加文章評論計數的代碼,寫到 評論ActiveReocrd的相應Event Handler中。比如,?EVENT_AFTER_INSERT?。
這樣子代碼功能界限清晰,便于查找、修改和擴展。 缺點是可能需要多看幾個方法才能了解整個業務流程。實際中我們多采用這種方法。
在實現數據庫記錄的關聯操作時,第一步就是要利用上述的各種事件,來產生關聯性。 其次,是要把這些關聯性綁死在一起。也就是用數據庫的事務。具體的原理, 參考?[_事務(Transaction)_](http://www.digpage.com/transaction.html#transaction)?部分的內容。
下面,我們以上面提到的博客文章新增一個評論為例,講解如何實現關聯操作。
### 聲明需要事務支持的操作[](http://www.digpage.com/active_record.html#id6 "Permalink to this headline")
在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種操作都包含。
### 啟用事務[](http://www.digpage.com/active_record.html#id7 "Permalink to this headline")
上一步中的?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()?來啟用事務。 事務的原理,請看?[_事務(Transaction)_](http://www.digpage.com/transaction.html#transaction)?部分的內容。
### 在事件響應中寫入關聯操作[](http://www.digpage.com/active_record.html#id8 "Permalink to this headline")
接下來,我們在關聯的事件,如?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()?中聲明要事務支持的操作類型,比如上面的例子, 聲明的是插入操作。
* 在合適事件響應函數中,寫下關聯操作代碼。
如果覺得《深入理解Yii2.0》對您有所幫助,也請[幫助《深入理解Yii2.0》](http://www.digpage.com/donate.html#donate)。 謝謝!
- 更新記錄
- 導讀
- Yii是什么
- Yii2.0的亮點
- 背景知識
- 如何閱讀本書
- Yii基礎
- 屬性(Property)
- 事件(Event)
- 行為(Behavior)
- Yii約定
- Yii應用的目錄結構和入口腳本
- 別名(Alias)
- Yii的類自動加載機制
- 環境和配置文件
- 配置項(Configuration)
- Yii模式
- MVC
- 依賴注入和依賴注入容器
- 服務定位器(Service Locator)
- 請求與響應(TBD)
- 路由(Route)
- Url管理
- 請求(Reqeust)
- Web應用Request
- Yii與數據庫(TBD)
- 數據類型
- 事務(Transaction)
- AcitveReocrd事件和關聯操作
- 樂觀鎖與悲觀鎖
- 《深入理解Yii2.0》視頻教程
- 第一講:基礎配置
- 第二講:用戶登錄
- 第三講:文章及評論的模型
- 附錄
- 附錄1:Yii2.0 對比 Yii1.1 的重大改進
- 附錄2:Yii的安裝
- 熱心讀者