本文實例講述了YII2.0框架行為(Behavior)。分享給大家供大家參考,具體如下:
行為(Behavior)
使用行為(behavior)可以在不修改現有類的情況下,對類的功能進行擴充。 通過將行為綁定到一個類,可以使類具有行為本身所定義的屬性和方法,就好像類本來就有這些屬性和方法一樣。 而且不需要寫一個新的類去繼承或包含現有類。
Yii中的行為,其實是 yii/base/Behavior 類的實例, 只要將一個Behavior實例綁定到任意的 yii/base/Component 實例上, 這個Component就可以擁有該Behavior所定義的屬性和方法了。而如果將行為與事件關聯起來,可以玩的花樣就更多了。
但有一點需要注意,Behavior只能與Component類綁定。 他們是天生的一對,愛情不是你想買,想買就能買的,必要的物質是少不了的,奮斗吧少年。 所以,如果你寫了一個類,需要使用到行為,那么就果斷地繼承自yii/base/Component 。
同時,行為單獨靠Behavior一方是實現不了的,就好像愛情不是一廂情愿。 為了支持Behavior,Yii對于yii/base/Component 也進行了精心設計,這兩者共同配合,才有了神奇的行為。
使用行為
一個綁定了行為的類,表現起來是這樣的:
// Step 1: 定義一個將綁定行為的類class MyClass extends yii/base/Component{ // 空的}// Step 2: 定義一個行為類,他將綁定到MyClass上class MyBehavior extends yii/base/Behavior{ // 行為的一個屬性 public $property1 = 'This is property in MyBehavior.'; // 行為的一個方法 public function method1() { return 'Method in MyBehavior is called.'; }}$myClass = new MyClass();$myBehavior = new MyBehavior();// Step 3: 將行為綁定到類上$myClass->attachBehavior('myBehavior', $myBehavior);// Step 4: 訪問行為中的屬性和方法,就和訪問類自身的屬性和方法一樣echo $myClass->property1;echo $myClass->method1(); 上面的代碼你不用全都看懂,雖然你可能已經用腳趾頭猜到了這些代碼的意思, 但這里你只需要記住行為中的屬性和方法可以被所綁定的類像訪問自身的屬性和方法一樣直接訪問就OK了。 代碼中, $myClass 是沒有property1 method() 成員的。這倆是 $myBehavior 的成員。 但是,通過 attachBehavior() 將行為綁定到對象之后, $myCalss 就好像練成了吸星大法、化功大法,表現的財大氣粗,將別人的屬性和方法都變成了自己的。
另外,從上面的代碼中,你還要掌握使用行為的大致流程:
行為的要素
我們提到了行為只是 yii/base/Behavior 類的實例。 那么這個類究竟有什么秘密呢?其實說破了也沒有什么的他只是一個簡單的封裝而已,非常的簡單:
class Behavior extends Object{ // 指向行為本身所綁定的Component對象 public $owner; // Behavior 基類本身沒用,主要是子類使用,重載這個函數返回一個數組表 // 示行為所關聯的事件 public function events() { return []; } // 綁定行為到 $owner public function attach($owner) { ... ... } // 解除綁定 public function detach() { ... ... }}這就是Behavior的全部代碼了,是不是很簡單?Behavior類的要素的確很簡單:
events() 用于表示行為所有要響應的事件;attach() 用于將行為與Component綁定起來;deatch() 用于將行為從Component上解除。下面分別進行講解。
行為的依附對象
yii/base/Behavior::$owner 指向的是Behavior實例本身所依附的對象。這是行為中引用所依附對象的唯一手段了。 通過這個 $owner ,行為才能訪問所依附的Component,才能將本身的方法作為事件handler綁定到Component上。
$owner 由 yii/base/Behavior::attach() 進行賦值。 也就是在將行為綁定到某個Component時, $owner 就已經名花有主了。 一般情況下,不需要你自己手動去指定 $owner 的值, 在調用 yii/base/Componet::attachBehavior() 將行為與對象綁定時, Component會自動地將 $this 作為參數,調用 yii/base/Behavior::attach() 。
有一點需要格外注意,由于行為從本質來講是一個PHP類,其方法就是類方法,就是成員函數。 所以,在行為的方法中, $this 引用的是行為本身, 試圖通過 $this 來訪問行為所依附的Component是行不通的。 正確的方法是通過 yii/base/Behavior::$owner 來訪問Component。
行為所要響應的事件
行為與事件結合后,可以在不對類作修改的情況下,補充類在事件觸發后的各種不同反應。 為此,只需要重載yii/base/Behavior::events() 方法,表示這個行為將對類的何種事件進行何種反饋即可:
namespace app/Components;use yii/db/ActiveRecord;use yii/base/Behavior;class MyBehavior extends Behavior{ // 重載events() 使得在事件觸發時,調用行為中的一些方法 public function events() { // 在EVENT_BEFORE_VALIDATE事件觸發時,調用成員函數 beforeValidate return [ ActiveRecord::EVENT_BEFORE_VALIDATE => 'beforeValidate', ]; } // 注意beforeValidate 是行為的成員函數,而不是綁定的類的成員函數。 // 還要注意,這個函數的簽名,要滿足事件handler的要求。 public function beforeValidate($event) { // ... }} 上面的代碼中, events() 返回一個數組,表示所要做出響應的事件, 上例中的事件是ActiveRecord::EVENT_BEFORE_VALIDATE ,以數組的鍵來表示, 而數組的值則表示做好反應的事件handler,上例中是beforeValidate() ,事件handler可以是以下形式:
對于事件響應函數的簽名,要求與事件handler一樣:
function ($event) { }具體內容,請參考 事件(Event) 的內容。
行為的綁定與解除
說到綁定與解除,這意味著這個事情有2方,行為和Component。單獨一方是沒有綁定或解除的說法的。 因此,這里我們先賣一關子,等后面講綁定和解除的原理時,再來講有關的內容。
這里你只需要知道,對于綁定和解除,Behavior 分別使用 attach() 和 detach() 來實現就OK了。
定義一個行為
定義一個行為,就是準備好要注入到現有類中去的屬性和方法, 這些屬性和方法要寫到一個 yii/base/Behavior 類中。 所以,定義一個行為,就是寫一個 Behavior的子類,子類中包含了所要注入的屬性和方法:
namespace app/Components;use yii/base/Behavior;class MyBehavior extends Behavior{ public $prop1; private $_prop2; private $_prop3; private $_prop4; public function getProp2() { return $this->_prop2; } public function setProp3($value) { $this->_prop3 = $value; } public function foo() { // ... } protected function bar() { // ... }}上面的代碼通過定義一個 app/Components/MyBehavior 類而定義一個行為。 由于 MyBehavior 繼承自yii/base/Behavior 從而間接地繼承自 yii/base/Object 。 沒錯,這是我們的老朋友了。因此,這個類有一個public的成員變量 prop1 , 一個只讀屬性 prop2 ,一個只寫屬性 prop3 ,一個public的方法 foo() 。 另外,還有一個private 的成員變量 $_prop4 ,一個protected 的方法 bar() 。 如果你不清楚只讀屬性和只寫屬性,最好回頭看看 屬性(Property) 部分的內容。
當這MyBehavior與一個Component綁定后, 綁定的Component也就擁有了 prop1 prop2 這兩個屬性和方法foo(),因為他們都是 public 的。 而 private 的 $_prop4 和 protected 的 bar 就得不到了。 至于原因么,后面講行為注入的原理時,我們再解釋。
行為的綁定
行為的綁定通常是由Component來發起。有兩種方式可以將一個Behavior綁定到一個 yii/base/Component 。 一種是靜態的方法,另一種是動態的。靜態的方法在實踐中用得比較多一些。 因為一般情況下,在你的代碼沒跑起來之前,一個類應當具有何種行為,是確定的。 動態綁定的方法主要是提供了更靈活的方式,但實際使用中并不多見。
靜態方法綁定行為
靜態綁定行為,只需要重載 yii/base/Component::behaviors() 就可以了。 這個方法用于描述類所具有的行為。如何描述呢? 使用配置來描述,可以是Behavior類名,也可以是Behavior類的配置數組:
namespace app/models;use yii/db/ActiveRecord;use app/Components/MyBehavior;class User extends ActiveRecord{ public function behaviors() { return [ // 匿名的行為,僅直接給出行為的類名稱 MyBehavior::className(), // 名為myBehavior2的行為,也是僅給出行為的類名稱 'myBehavior2' => MyBehavior::className(), // 匿名行為,給出了MyBehavior類的配置數組 [ 'class' => MyBehavior::className(), 'prop1' => 'value1', 'prop3' => 'value3', ], // 名為myBehavior4的行為,也是給出了MyBehavior類的配置數組 'myBehavior4' => [ 'class' => MyBehavior::className(), 'prop1' => 'value1', 'prop3' => 'value3', ] ]; }}還有一個靜態的綁定辦法,就是通過配置文件來綁定:
[ 'as myBehavior2' => MyBehavior::className(), 'as myBehavior3' => [ 'class' => MyBehavior::className(), 'prop1' => 'value1', 'prop3' => 'value3', ],]
具體參見配置項(Configuration) 部分的內容。
動態方法綁定行為
動態綁定行為,需要調用 yii/base/Compoent::attachBehaviors():
$Component->attachBehaviors([ 'myBehavior1' => new MyBehavior, // 這是一個命名行為 MyBehavior::className(), // 這是一個匿名行為]);
這個方法接受一個數組參數,參數的含義與上面靜態綁定行為是一樣一樣的。
在上面的這些例子中,以數組的鍵作為行為的命名,而對于沒有提供鍵名的行為,就是匿名行為。
對于命名的行為,可以調用 yii/base/Component::getBehavior() 來取得這個綁定好的行為:
$behavior = $Component->getBehavior('myBehavior2');對于匿名的行為,則沒有辦法直接引用了。但是,可以獲取所有的綁定好的行為:
$behaviors = $Component->getBehaviors();
綁定的內部原理
只是重載一個 yii/base/Component::behaviors() 就可以這么神奇地使用行為了? 這只是冰山的一角,實際上關系到綁定的過程,有關的方面有:
yii/base/Component::behaviors()yii/base/Component::ensureBehaviors()yii/base/Component::attachBehaviorInternal()yii/base/Behavior::attach()4個方法中,Behavior只占其一,更多的代碼,是在Component中完成的。
yii/base/Component::behaviors() 上面講靜態方法綁定行為時已經提到了,就是返回一個數組用于描述行為。 那么yii/base/Component::ensuerBehaviors() 呢?
這個方法會在Component的諸多地方調用 __get() __set() __isset() __unset() __call() canGetProperty()hasMethod() hasEventHandlers() on() off() 等用到,看到這么多是不是頭疼?一點都不復雜,一句話,只要涉及到類的屬性、方法、事件這個函數都會被調用到。
這么眾星拱月,被諸多凡人所需要的 ensureBehaviors() 究竟是何許人也? 就像名字所表明的,他的作用在于“ensure” 。其實只是確保 behaviors() 中所描述的行為已經進行了綁定而已:
public function ensureBehaviors(){ // 為null表示尚未綁定 // 多說一句,為空數組表示沒有綁定任何行為 if ($this->_behaviors === null) { $this->_behaviors = []; // 遍歷 $this->behaviors() 返回的數組,并綁定 foreach ($this->behaviors() as $name => $behavior) { $this->attachBehaviorInternal($name, $behavior); } }} 這個方法主要是對子類用的, yii/base/Compoent 沒有任何預先注入的行為,所以,這個調用沒有用。 但是對于子類,你可能重載了 yii/base/Compoent::behaviros() 來預先注入一些行為。 那么,這個函數會將這些行為先注入進來。
從上面的代碼中,自然就看到了接下來要說的第三個東東, yii/base/Component/attachBehaviorInternal():
private function attachBehaviorInternal($name, $behavior){ // 不是 Behavior 實例,說是只是類名、配置數組,那么就創建出來吧 if (!($behavior instanceof Behavior)) { $behavior = Yii::createObject($behavior); } // 匿名行為 if (is_int($name)) { $behavior->attach($this); $this->_behaviors[] = $behavior; // 命名行為 } else { // 已經有一個同名的行為,要先解除,再將新的行為綁定上去。 if (isset($this->_behaviors[$name])) { $this->_behaviors[$name]->detach(); } $behavior->attach($this); $this->_behaviors[$name] = $behavior; } return $behavior;}首先要注意到,這是一個private成員。其實在Yii中,所有后綴為 *Internal 的方法,都是私有的。 這個方法干了這么幾件事:
Yii::createObject() 創建出來。 在 yii/base/Component::attachBehaviorInternal() 中, 以 $this 為參數調用了 yii/base/Behavior::attach()。 從而,引出了跟綁定相關的最后一個家伙 yii/base/Behavior::attach() , 這也是前面我們講行為的要素時沒講完的。先看看代碼:
public function attach($owner){ $this->owner = $owner; foreach ($this->events() as $event => $handler) { $owner->on($event, is_string($handler) ? [$this, $handler] : $handler); }}上面的代碼干了兩件事:
events() 返回的數組,將準備響應的事件,通過所依附類的 on() 綁定到類上說了這么多,關于綁定,做個小結:
解除行為
解除行為只需調用 yii/base/Component::detachBehavior() 就OK了:
$Component->detachBehavior('myBehavior2');這樣就可以解除已經綁定好的名為 myBehavior2 的行為了。 但是,對于匿名行為,這個方法就無從下手了。不過我們可以一不做二不休,解除所有綁定好的行為:
$Component->detachBehaviors();
這上面兩種方法,都會調用到 yii/base/Behavior::detach() ,其代碼如下:
public function detach(){ // 這得是個名花有主的行為才有解除一說 if ($this->owner) { // 遍歷行為定義的事件,一一解除 foreach ($this->events() as $event => $handler) { $this->owner->off($event, is_string($handler) ? [$this, $handler] : $handler); } $this->owner = null; }} 與 yii/base/Behavior::attach() 相反,解除的過程就是干兩件事: 一是將 $owner 設置為 null ,表示這個行為沒有依附到任何類上。 二是通過Component的 off() 將綁定到類上的事件hanlder解除下來。一句話,善始善終。
行為響應的事件實例
上面的綁定和解除過程,我們看到Yii費了那么大勁,主要就是為了將行為中的事件handler綁定到類中去。 在實際編程時,行為用得最多的,也是對于Compoent各種事件的響應。 通過行為注入,可以在不修改現有類的代碼的情況下,更改、擴展類對于事件的響應和支持。 使用這個技巧,可以玩出很炫的花樣。 而要將行為與Component的事件關聯起來,就要通過 yii/base/Behavior::events() 方法。
上面Behavior基類的代碼中,這個方法只是返回了一個空數組,說明不對所依附的Compoent的任何事件產生關聯。 但是在實際使用時,往往通過重載這個方法來告訴Yii,這個行為將對Compoent的何種事件,使用哪個方法進行處理。
比如,Yii自帶的 yii/behaviors/AttributeBehavior 類,定義了在一個 ActiveRecord 對象的某些事件發生時, 自動對某些字段進行修改的行為。 他有一個很常用的子類 yii/behaviors/TimeStampBehavior 用于將指定的字段設置為一個當前的時間戳。 常用于表示最后修改日期、上次登陸時間等場景。我們以這個行為為例,來分析行為響應事件的原理。
在 yii/behaviors/AttributeBehavior::event() 中,代碼如下:
public function events(){ return array_fill_keys(array_keys($this->attributes), 'evaluateAttributes');} 這段代碼的意思這里不作過多深入,學有余力的讀者朋友可以自行研究,難度并不高。 這里,你只需要大致知道,這段代碼將返回一個數組,其鍵值為 $this->attributes 數組的鍵值, 數組元素的值為成員函數evaluateAttributes 。
而在 yii/behaviors/TimeStampBehavior::init() 中,有以下的代碼:
public function init(){ parent::init(); if (empty($this->attributes)) { // 重點看這里 $this->attributes = [ BaseActiveRecord::EVENT_BEFORE_INSERT => [$this->createdAtAttribute, $this->updatedAtAttribute], BaseActiveRecord::EVENT_BEFORE_UPDATE => $this->updatedAtAttribute, ]; }} 上面的代碼重點看的是對于 $this->attributes 的初始化部分。 結合上面2個方法的代碼,對于yii/base/Behavior::events() 的返回數組,其格式應該是這樣的:
return [ BaseActiveRecord::EVENT_BEFORE_INSERT => 'evaluateAttributes', BaseActiveRecord::EVENT_BEFORE_UPDATE => 'evaluateAttributes',];
數組的鍵值用于指定要響應的事件, 這里是 BaseActiveRecord::EVENT_BEFORE_INSERT 和BaseActiveRecord::EVENT_BEFORE_UPDATE 。 數組的值是一個事件handler,如上面的 evaluateAttributes 。
那么一旦TimeStampBehavior與某個ActiveRecord綁定,就會調用 yii/behaviors/TimeStampBehavior::attach(), 那么就會有:
// 這里 $owner 是某個 ActiveRecordpublic function attach($owner){ $this->owner = $owner; // 遍歷上面提到的 events() 所定義的數組 foreach ($this->events() as $event => $handler) { // 調用 ActiveRecord::on 來綁定事件 // 這里 $handler 為字符串 `evaluateAttributes` // 因此,相當于調用 on(BaseActiveRecord::EVENT_BEFORE_INSERT, // [$this, 'evaluateAttributes']) $owner->on($event, is_string($handler) ? [$this, $handler] : $handler); }} 因此,事件 BaseActiveRecord::EVENT_BEFORE_INSERT 和 BaseActiveRecord::EVENT_BEFORE_UPDATE 就綁定到了ActiveRecord上了。當新建記錄或更新記錄時, TimeStampBehavior::evaluateAttributes 就會被觸發。 從而實現時間戳的功能。具體可以看看 yii/behaviors/AttributeBehavior::evaluateAttributes() 和yii/behaviors/TimeStampBehavior::getValues() 的代碼。這里因為只是具體功能實現,對于行為的理解關系不大。 就不把代碼粘出來占用篇幅了。
行為的屬性和方法注入原理
上面我們了解到了行為的用意在于將自身的屬性和方法注入給所依附的類。 那么Yii中是如何將一個行為yii/base/Behavior 的屬性和方法, 注入到一個 yii/base/Component 中的呢? 對于屬性而言,是通過 __get() 和__set()魔術方法來實現的。 對于方法,是通過 __call() 方法。
屬性的注入
以讀取為例,如果訪問 $Component->property1 ,Yii在幕后干了些什么呢? 這個看看 yii/base/Component::__get()
public function __get($name){ $getter = 'get' . $name; if (method_exists($this, $getter)) { return $this->$getter(); } else { // 注意這個 else 分支的內容,正是與 yii/base/Object::__get() 的 // 不同之處 $this->ensureBehaviors(); foreach ($this->_behaviors as $behavior) { if ($behavior->canGetProperty($name)) { // 屬性在行為中須為 public。否則不可能通過下面的形式訪問呀。 return $behavior->$name; } } } if (method_exists($this, 'set' . $name)) { throw new InvalidCallException('Getting write-only property: ' . get_class($this) . '::' . $name); } else { throw new UnknownPropertyException('Getting unknown property: ' . get_class($this) . '::' . $name); }} 重點來看 yii/base/Compoent::__get() 與 yii/base/Object::__get() 的不同之處。 就是在于對于未定義getter函數之后的處理, yii/base/Object 是直接拋出異常, 告訴你想要訪問的屬性不存在之類。 但是 yii/base/Component則是在不存在getter之后,還要看看是不是注入的行為的屬性:
$this->ensureBehaviors() 。這個方法已經在前面講過了,主要是確保行為已經綁定。$this->_behaviors 。 Yii將類所有綁定的行為都保存在yii/base/Compoent::$_behaviors[] 數組中。canGetProperty() 判斷這個屬性, 是否是所綁定行為的可讀屬性,如果是,就返回這個行為的這個屬性 $behavior->name 。 完成屬性的讀取。 至于 canGetProperty() 已經在 :ref::property 部分已經簡單講過了, 后面還會有針對性地一個介紹。對于setter,代碼類似,這里就不占用篇幅了。
方法的注入
與屬性的注入通過 __get() __set() 魔術方法類似, Yii通過 __call() 魔術方法實現對行為中方法的注入:
public function __call($name, $params){ $this->ensureBehaviors(); foreach ($this->_behaviors as $object) { if ($object->hasMethod($name)) { return call_user_func_array([$object, $name], $params); } } throw new UnknownMethodException('Calling unknown method: ' . get_class($this) . "::$name()");} 從上面的代碼中可以看出,Yii還是先是調用了 $this->ensureBehaviors() 確保行為已經綁定。
然后,也是遍歷 yii/base/Component::$_behaviros[] 數組。 通過 hasMethod() 方法判斷方法是否存在。 如果所綁定的行為中要調用的方法存在,則使用PHP的 call_user_func_array() 調用之。 至于 hasMethod() 方法,我們后面再講。
注入屬性與方法的訪問控制
在前面我們針對行為中public和private、protected的成員在所綁定的類中是否可訪問舉出了具體例子。 這里我們從代碼層面解析原因。
在上面的內容,我們知道,一個屬性可不可訪問,主要看行為的 canGetProperty() 和 canSetProperty() 。 而一個方法可不可調用,主要看行為的 hasMethod() 。 由于 yii/base/Behavior 繼承自我們的老朋友 yii/base/Object ,所以上面提到的三個判斷方法, 事實上代碼都在 Object 中。我們一個一個來看:
public function canGetProperty($name, $checkVars = true){ return method_exists($this, 'get' . $name) || $checkVars && property_exists($this, $name);}public function canSetProperty($name, $checkVars = true){ return method_exists($this, 'set' . $name) || $checkVars && property_exists($this, $name);}public function hasMethod($name){ return method_exists($this, $name);}這三個方法真的談不上復雜。對此,我們可以得出以下結論:
行為與繼承和特性(Traits) 的區別
從實現的效果看,你是不是會認為Yii真是多此一舉?PHP中要達到這樣的效果,可以使用繼承呀,可以使用PHP新引入的特性(Traits)呀。但是,行為具有繼承和特性所沒有的優點,從實際使用的角度講,繼承和特性更靠底層點??康讓?,就意味著開發效率低,運行效率高。行為的引入,是以可以接受的運行效率犧牲為成本,謀取開發效率大提升的一筆買賣。
行為與繼承
首先來講,拿行為與繼承比較,從邏輯上是不對的,這兩者是在完全不同的層面上的事物,是不對等的。之所以進行比較,是因為在實現的效果上,兩者有的類似的地方。看起來,行為和繼承都可以使一個類具有另一個類的屬性和方法,從而達到擴充類的功能的目的。
相比較于使用繼承的方式來擴充類功能,使用行為的方式,一是不必對現有類進行修改,二是PHP不支持多繼承,但是Yii可以綁定多個行為,從而達到類似多繼承的效果。
反過來,行為是絕對無法替代繼承的。亞洲人,美洲人都是地球人,你可以將亞洲人和美洲人當成地球人來對待。但是,你絕對不能把一只在某些方面表現得像人的猴子,真的當成人來對待。
這里就不展開講了。從本質上來講,行為只是一種設計模式,是解決問題的方法學。繼承則是PHP作為編程語言所提供的特性,根本不在一個層次上。
行為與特性
特性是PHP5.4之后引入的一個新feature。從實現效果看,行為與特性都達到把自身的public 變量、屬性、方法注入到當前類中去的目的。在使用上,他們也各有所長,但總的原則可以按下面的提示進行把握。
傾向于使用行為的情況:
傾向于使用特性的情況:
希望本文所述對大家基于Yii框架的PHP程序設計有所幫助。
新聞熱點
疑難解答
圖片精選