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教程網