什麼是行爲
所謂行爲,其實說白了,就是把A類綁定到B類上,可以在不修改B類的情況下,對B類進行功能和屬性的擴充,這樣B類就擁有了A類的屬性和方法,只是在這裏,我們習慣成A類爲行爲。
舉一個例子:
class MyClass
{
public $hello = "我是類的自有屬性hello".PHP_EOL;
public function hello()
{
echo "我是類的自有方法hello".PHP_EOL;
}
}
class MyBehavior
{
public $hi = "我是行爲的自有屬性hi".PHP_EOL;
public function hi()
{
echo "我是行爲的自有方法hi".PHP_EOL;
}
}
$class = new MyClass();
$behavior = new MyBehavior();
$class->attachBehavior('behavior1',$behavior);//將類和行爲進行綁定,這裏爲$behavior綁定了一個別名:behavior1
echo $class->hello;//輸出:我是類的自有屬性hello
$class->hello();//輸出:我是類的自有方法hello
echo $class->hi;//期望輸出:我是行爲的自有屬性hi
$class->hi();//期望輸出:我是行爲的自有方法hi
實現自己的行爲
實現行爲的關鍵知識點:__get(),__set(),__call(),call_user_func_array,前三個是PHP的魔術方法,這四個是非常重要的知識點
以上僞代碼就是行爲的實現,現在我們來動手實現自己的行爲:
//定義一個基類,後面的類都來繼承它
class Base
{
//存放行爲類的容器數組
private $_behaviors = [];
//綁定行爲類:其實就是放入數組$_beheviors
public function attachBehavior($name,$behavior)
{
//如果$_beheviors裏有這個別名的話,先unset掉,給新綁定的行爲讓路
if(isset($this->_behaviors[$name])) {
unset($this->_behaviors[$name]);
}
$this->_behaviors[$name] = $behavior;
}
public function __get($name)
{
foreach($this->_behaviors as $behavior) {
if($behavior->hasProperty($name)) {
return $behavior->$name;
}
}
}
public function __set($name,$value)
{
foreach($this->_behaviors as $behavior) {
if($behavior->hasProperty($name)) {
$behavior->$name = $value;
}
}
}
public function __call($method,$params)
{
foreach($this->_behaviors as $behavior) {
if($behavior->hasMethod($method)) {
call_user_func_array([$behavior,$method],$params);
}
}
}
}
//定義行爲基類,所有的行爲類都繼承它
class Behavior
{
//判斷該行爲類是否有$name屬性
public function hasProperty($name)
{
return property_exists($this,$name);
}
//判斷該行爲類是否有$method方法
public function hasMethod($method)
{
return method_exists($this,$method);
}
}
定義好這兩個基類之後,我們還要修改一下MyClass和MyBehavior:
class MyClass extends Base
{
// 讓Myclass繼承自Base,才能綁定行爲
···
}
class MyBehavior extends Behavior
{
// 讓MyBehavior繼承自Behavior
···
}
然後我們繼續運行我們的代碼:
$class = new MyClass();
$behavior = new MyBehavior();
$class->attachBehavior('behavior1',$behavior);
echo $class->hello;// 輸出:我是類的自有屬性hello
$class->hello();// 輸出:我是類的自有方法hello
echo $class->hi;// 輸出:我是行爲的自有屬性hi
$class->hi();// 輸出:我是行爲的自有方法hi
// 這裏動態去設置行爲的屬性
$class->hi = "我是重新設置好的行爲的自有屬性hi".PHP_EOL;
echo $class->hi;// 輸出:我是重新設置好的行爲的自有屬性hi
看到了嗎,我們想要的期望輸出,這回變成現實了,整體還是很好理解的。
這裏沒有考慮行爲的方法和屬性的權限問題,例子裏都寫成了Public,只是方便舉例和測試,實際開發中肯定要進行權限控制判斷的。
Yii中的行爲
如何使用Yii中的行爲
- 由於在Yii中,實現行爲的功能是yii\base\Component中的,所以想實現行爲,就必須從yii\base\Component繼承;
- 從yii\base\Behavior派生自己的行爲類;
- 將Component和Behavior綁定起來;
- 像使用Component自己的屬性和方法一樣,使用綁定好的行爲中的屬性和方法;
行爲的綁定
在Yii中,一個行爲的綁定實現步驟是這樣的:
* yii\base\Component::behaviors(); //第一步
* yii\base\Component::ensureBehaviors(); //第二步
* yii\base\Behavior::attach(); //第三步
在yii項目實際的開發中,我們很少手動去綁定行爲,一般都是定義一個數組,如下所示:
class MyClass
{
// 定義好的要綁定的行爲數組
public function behaviors()
{
return [
// 匿名的行爲,僅給出行爲的類名
MyBehavior::className(),
// 名爲myBehavior2的行爲,也是僅給出行爲的類名
'myBehavior2' => MyBehavior::className(),
// 匿名行爲,給出了MyBehavior類的配置數組
[
'class' => MyBehavior::className(),
],
];
}
}
那麼,yii是如何去綁定上述定義好的數組的呢?答案在第二步,yii\base\Component::ensureBehaviors():
public function ensureBehaviors()
{
if ($this->_behaviors === null) {
$this->_behaviors = [];
// 遍歷 $this->behaviors()數組,並綁定
foreach ($this->behaviors() as $name => $behavior) {
$this->attachBehaviorInternal($name, $behavior);
}
}
}
ensureBehaviors()方法其實是調用了attachBehaviorInternal():
private function attachBehaviorInternal($name, $behavior)
{
// 如果不是Behavior的實例,比如說只是一個類名的字符串,或者一個配置數組
if (!($behavior instanceof Behavior)) {
// 就把這個對象創建出來
// createObject()人如其名,就是用來創建對象的,這個知識點在Yii的服務定位器中,其實就是去容器裏取出來對象
$behavior = Yii::createObject($behavior);
}
// 如果是int,說明在配置$behaviors數組的時候,沒有對其人爲的設定一個key,那肯定就是默認的數字索引
if (is_int($name)) {
$behavior->attach($this); // 這裏是第四步
$this->_behaviors[] = $behavior; //將行爲放入$this->_behaviors容器
} else {
// 命名行爲:
// 已經有一個同名的行爲,要先解除,再將新的行爲綁定上去。
if (isset($this->_behaviors[$name])) {
$this->_behaviors[$name]->detach();
}
$behavior->attach($this); // 這裏是第四步
$this->_behaviors[$name] = $behavior; //將行爲放入$this->_behaviors容器
}
return $behavior;
}
其實經過以上兩步,Yii已經將行爲綁定到Component的子類中,這個時候,已經可以使用行爲的屬性和方法了,那麼第三步是幹什麼的呢,我們先看一下源碼,代碼在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);
}
}
這個attach方法主要是爲了把行爲中定義的事件都綁定到行爲的所有者身上,這樣所有者就有了觸發事件的能力
行爲的解綁
行爲的解綁的原理和綁定剛好相反,從行爲數組$_behaviors中,unset掉要解綁的行爲,代碼在yii\base\Component::detachBehavior():
public function detachBehavior($name)
{
$this->ensureBehaviors();
if (isset($this->_behaviors[$name])) {
$behavior = $this->_behaviors[$name];
unset($this->_behaviors[$name]);
$behavior->detach(); // 這個方法主要是爲了解綁通過行爲綁定給Component的事件
return $behavior;
}
return null;
}
$behavior->detach()方法在yii\base\Behavior::detach():
public function detach()
{
if ($this->owner) {
foreach ($this->events() as $event => $handler) {
// 解除Component的事件
$this->owner->off($event, is_string($handler) ? [$this, $handler] : $handler);
}
$this->owner = null;
}
}
行爲的屬性和方法注入的原理分析
yii的行爲實現,其實和我們文章一開始自己寫的行爲類實現原理是一樣的,主要用到了__get(),__set(),__call()魔術方法:
屬性的獲取:
public function __get($name)
{
// 由於在yii的基類Object裏爲屬性定義了getter()方法
// 所以獲取屬性的時候,先判斷該屬性有沒有對應的getter()方法
$getter = 'get' . $name;
if (method_exists($this, $getter)) {
// read property, e.g. getName()
return $this->$getter();
}
// 如果沒有getter(),那麼該屬性就可能是行爲的屬性
// 所以遍歷行爲數組,如果哪個行爲有這個屬性,就返回
$this->ensureBehaviors();
foreach ($this->_behaviors as $behavior) {
if ($behavior->canGetProperty($name)) {
return $behavior->$name;
}
}
···
}
屬性的設置:
public function __set($name, $value)
{
// 設置一個屬性的時候,如果有setter()方法,那麼優先調用setter
$setter = 'set' . $name;
if (method_exists($this, $setter)) {
$this->$setter($value);
return;
}
$this->ensureBehaviors();
// 遍歷行爲數組,如果行爲有$name屬性,那麼就設置爲$value
foreach ($this->_behaviors as $behavior) {
if ($behavior->canSetProperty($name)) {
$behavior->$name = $value;
return;
}
}
···
}
方法的獲取:
public function __call($name, $params)
{
$this->ensureBehaviors();
// 遍歷行爲數組
foreach ($this->_behaviors as $object) {
// 如果某個行爲有$name這個方法體,那麼就調用,參數爲$params
if ($object->hasMethod($name)) {
return call_user_func_array([$object, $name], $params);
}
}
···
}
引出:行爲和Traits的區別
php的traits是在5.4版本之後才引入的一個新特性,從實現效果上來看,行爲和traits都能把自身的屬性和方法注入到當前類中去,但是還是有一些區別:
- 行爲從本質上來講,也是PHP的類,一個行爲可以繼承自另一個行爲,從而實現代碼的複用。而traits只是php的一種語法,效果上類似把tratis的代碼導入到一個類中,從而實現代碼的注入,特性是不支持繼承的。
- 行爲可以支持動態的綁定,解綁,而不必對類進行修改。但是traits必須在類內使用use,才能導入,要解除traits時,則要刪除這個use語句,也就是說要對類進行修改。
- traits在效率上要比行爲高一點,因爲traits是php內部實現的,行爲是我們後期自己實現的。