yii 2.0 屬性

屬性用於表征類的狀態,從訪問的形式上看,屬性與成員變量沒有區別。 你能一眼看出$object->foo中的foo是成員變量還是屬性麼?顯然不行。 但是,成員變量是就類的結構構成而言的概念,而屬性是就類的功能邏輯而言的概念,兩者緊密聯系又 相互區別。比如,我們說People類有一個成員變量int$age,表示年齡。那麼這裡年齡就是屬性 ,$age就是成員變量。

再舉個更學術化點的例子,與非門:

class NotAndGate extends Object{
    private $_key1;
    private $_key2;

    public function setKey1($value){
        $this->_key1 = $value;
    }

    public function setKey2($value){
        $this->_key2 = $value;
    }

    public function getOutput(){
        if (!$this->_key1 || !$this->_key2)
            return true;
        else if ($this->_key1 && $this->_key2)
            return false;
    }
}

與非門有兩個輸入,當兩個輸入都為真時,與非門的輸出為假,否則,輸出為真。上面的代碼中,與非 門類有兩個成員變量,$_key1$_key2。但是有3個屬性,表示2個輸入的key1key2,以及表示輸出的output

成員變量和屬性的區別與聯系在於:

  • 成員變量是一個“內”概念,反映的是類的結構構成。屬性是一個“外”概念,反映的是類的邏輯意義。
  • 成員變量沒有讀寫權限控制,而屬性可以指定為隻讀或隻寫,或可讀可寫。
  • 成員變量不對讀出作任何後處理,不對寫入作任何預處理,而屬性則可以。
  • public成員變量可以視為一個可讀可寫、沒有任何預處理或後處理的屬性。 而private成員變量由於外部不可見,與屬性“外”的特性不相符,所以不能視為屬性。
  • 雖然大多數情況下,屬性會由某個或某些成員變量來表示,但屬性與成員變量沒有必然的對應關系, 比如與非門的output屬性,就沒有一個所謂的$output成員變量與之對應。

在Yii中,由yii\base\Object提供瞭對屬性的支持,因此,如果要使你的類支持屬性, 必須繼承自yii\base\Object。Yii中屬性是通過PHP的魔法函數__get()__set()來產生作用的。 下面的代碼是yii\base\Object類對於__get()__set()的定義:

public function __get($name)              // 這裡$name是屬性名
{
    $getter = 'get' . $name;              // getter函數的函數名
    if (method_exists($this, $getter)) {
        return $this->$getter();          // 調用瞭getter函數
    } elseif (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);
    }
}

// $name是屬性名,$value是擬寫入的屬性值
public function __set($name, $value)
{
    $setter = 'set' . $name;             // setter函數的函數名
    if (method_exists($this, $setter)) {
        $this->$setter($value);          // 調用setter函數
    } elseif (method_exists($this, 'get' . $name)) {
        throw new InvalidCallException('Setting read-only property: ' .
            get_class($this) . '::' . $name);
    } else {
        throw new UnknownPropertyException('Setting unknown property: '
            . get_class($this) . '::' . $name);
    }
}

實現屬性的步驟

我們知道,在讀取和寫入對象的一個不存在的成員變量時,__get()__set()會被自動調用。 Yii正是利用這點,提供對屬性的支持的。從上面的代碼中,可以看出,如果訪問一個對象的某個屬性, Yii會調用名為get屬性名()的函數。如,SomeObject->Foo, 會自動調用SomeObject->getFoo()。如果修改某一屬性,會調用相應的setter函數。 如,SomeObject->Foo=$someValue,會自動調用SomeObject->setFoo($someValue)

因此,要實現屬性,通常有三個步驟:

  • 繼承自yii\base\Object
  • 聲明一個用於保存該屬性的私有成員變量。
  • 提供getter或setter函數,或兩者都提供,用於訪問、修改上面提到的私有成員變量。 如果隻提供瞭getter,那麼該屬性為隻讀屬性,隻提供瞭setter,則為隻寫。

如下的Post類,實現瞭可讀可寫的屬性title:

class Post extends yii\base\Object    // 第一步:繼承自 yii\base\Object
{
    private $_title;                 // 第二步:聲明一個私有成員變量

    public function getTitle()       // 第三步:提供getter和setter
    {
        return $this->_title;
    }

    public function setTitle($value)
    {
        $this->_title = trim($value);
    }
}

從理論上來講,將private$_title寫成public$title,也是可以實現對$post->title的讀寫的。但這不是好的習慣,理由如下:

  • 失去瞭類的封裝性。 一般而言,成員變量對外不可見是比較好的編程習慣。 從這裡你也許沒看出來,但是假如有一天,你不想讓用戶修改標題瞭,你怎麼改? 怎麼確保代碼中沒有直接修改標題? 如果提供瞭setter,隻要把setter刪掉,那麼一旦有沒清理幹凈的對標題的寫入,就會拋出異常。 而使用public$title的方法的話,你改成private$title可以排查寫入的異常,但是讀取的也被禁止瞭。
  • 對於標題的寫入,你想去掉空格。 使用setter的方法,隻需要像上面的代碼段一樣在這個地方調用trim()就可以瞭。 但如果使用public$title的方法,那麼毫無疑問,每個寫入語句都要調用trim()。 你能保證沒有一處遺漏?

因此,使用public$title隻是一時之快,看起來簡單,但今後的修改是個麻煩事。 簡直可以說是惡夢。這就是軟件工程的意義所在,通過一定的方法,使代碼易於維護、便於修改。 一時看著好像沒必要,但實際上吃過虧的朋友或者被客戶老板逼著修改上一個程式員寫的代碼,問候過他親人的, 都會覺得這是十分必要的。

但是,世事無絕對。由於__get()__set()是在遍歷所有成員變量,找不到匹配的成員變量時才被調用。 因此,其效率天生地低於使用成員變量的形式。在一些表示數據結構、數據集合等簡單情況下,且不需讀寫控制等, 可以考慮使用成員變量作為屬性,這樣可以提高一點效率。

另外一個提高效率的小技巧就是:使用$pro=$object->getPro()來代替$pro=$object->pro, 用$objcect->setPro($value)來代替$object->pro=$value。 這在功能上是完全一樣的效果,但是避免瞭使用__get()__set(),相當於繞過瞭遍歷的過程。

這裡估計有人該罵我瞭,Yii好不容易實現瞭屬性的機制,就是為瞭方便開發者, 結果我卻在這裡教大傢怎麼使用原始的方式,去提高所謂的效率。 嗯,確實,開發的便利性與執行高效率存在一定的矛盾。我個人的觀點更傾向於以便利為先, 用好、用足Yii為我們創造的便利條件。至於效率的事情,更多的是框架自身需要註意的, 我們隻要別寫出格外2的代碼就OK瞭。

不過你完全可以放心,在Yii的框架中,極少出現$app->request之類的代碼,而是使用$app->getRequest()。 換句話說,框架自身還是格外地註重效率的,至於便利性,則留給瞭開發者。 總之,這裡隻是點出來有這麼一個知識點,至於用不用,怎麼用,完全取決於你瞭。

值得註意的是:

  • 由於自動調用__get()__set()的時機僅僅發生在訪問不存在的成員變量時。 因此,如果定義瞭成員變量public$title那麼,就算定義瞭getTitle()setTitle(), 他們也不會被調用。因為$post->title時,會直接指向該pulic$title__get()__set()是不會被調用的。從根上就被切斷瞭。
  • 由於PHP對於類方法不區分大小寫,即大小寫不敏感,$post->getTitle()$post->gettitle()是調用相同的函數。 因此,$post->title$post->Title是同一個屬性。即屬性名也是不區分大小寫的。
  • 由於__get()__set()都是public的, 無論將getTitle()setTitle()聲明為 public, private, protected, 都沒有意義,外部同樣都是可以訪問。所以,所有的屬性都是public的。
  • 由於__get()__set()都不是static的,因此,沒有辦法使用static 的屬性。

Object的其他與屬性相關的方法

除瞭__get()__set()之外,yii\base\Object還提供瞭以下方法便於使用屬性:

  • __isset()用於測試屬性值是否不為null,在isset($object->property)時被自動調用。 註意該屬性要有相應的getter。
  • __unset()用於將屬性值設為null,在unset($object->property)時被自動調用。 註意該屬性要有相應的setter。
  • hasProperty()用於測試是否有某個屬性。即,定義瞭getter或setter。 如果hasProperty()的參數$checkVars=true(默認為true), 那麼隻要具有同名的成員變量也認為具有該屬性,如前面提到的public$title
  • canGetProperty()測試一個屬性是否可讀,參數$checkVars的意義同上。隻要定義瞭getter,屬性即可讀。 同時,如果$checkVarstrue。那麼隻要類定義瞭成員變量,不管是public, private 還是 protected, 都認為是可讀。
  • canSetProperty()測試一個屬性是否可寫,參數$checkVars的意義同上。隻要定義瞭setter,屬性即可寫。 同時,在$checkVarsture。那麼隻要類定義瞭成員變量,不管是public, private 還是 protected, 都認為是可寫。

Object和Component

yii\base\Component繼承自yii\base\Object,因此,他也具有屬性等基本功能。

但是,由於Componet還引入瞭事件、行為,因此,它並非簡單繼承瞭Object的屬性實現方式,而是基於同樣的機制, 重載瞭__get()__set()等函數。但從實現機制上來講,是一樣的。這個不影響理解。

前面說過,官方將Yii定位於一個基於組件的框架。可見組件這一概念是Yii的基礎。 如果你有興趣閱讀Yii的源代碼或是API文檔,你將會發現, Yii幾乎所有的核心類都派生於(繼承自)yii\base\Component

在Yii1.1時,就已經有瞭component瞭,那時是 CComponent。Yii2將Yii1.1中的CComponent拆分成兩個類:yii\base\Objectyii\base\Component

其中,Object比較輕量級些,通過getter和setter定義瞭類的屬性(property)。 Component派生自Object,並支持事件(event)和行為(behavior)。因此,Component類具有三個重要的特性:

  • 屬性(property)
  • 事件(event)
  • 行為(behavior)

相信你或多或少瞭解過,這三個特性是豐富和拓展類功能、改變類行為的重要切入點。 因此,Component在Yii中的地位極高。

在提供更多功能、更多便利的同時,Component由於增加瞭event和behavior這兩個特性, 在方便開發的同時,也犧牲瞭一定的效率。 如果開發中不需要使用event和behavior這兩個特性,比如表示一些數據的類。 那麼,可以不從Component繼承,而從Object繼承。 典型的應用場景就是如果表示用戶輸入的一組數據,那麼,使用Object。 而如果需要對對象的行為和能響應處理的事件進行處理,毫無疑問應當采用Component。 從效率來講,Object更接近原生的PHP類,因此,在可能的情況下,應當優先使用Object。

Object的配置方法

Yii提供瞭一個統一的配置對象的方式。這一方式貫穿整個Yii。Application對象的配置就是這種配置方式的體現:

$config = yii\helpers\ArrayHelper::merge(
    require(__DIR__ . '/../../common/config/main.php'),
    require(__DIR__ . '/../../common/config/main-local.php'),
    require(__DIR__ . '/../config/main.php'),
    require(__DIR__ . '/../config/main-local.php')
);

$application = new yii\web\Application($config);

$config看著復雜,但本質上就是一個各種配置項的數組。Yii中就是統一使用數組的方式對對象進行配置,而實現這一切的關鍵就在yii\base\Object定義的構造函數中:

public function __construct($config = [])
{
    if (!empty($config)) {
        Yii::configure($this, $config);
    }
    $this->init();
}

所有yii\base\Object的構建流程是:

  • 構建函數以$config數組為參數被自動調用。
  • 構建函數調用Yii::configure()對對象進行配置。
  • 在最後,構造函數調用對象的init()方法進行初始化。

數組配置對象的秘密在Yii::configure()中,但說破瞭其實也沒有什麼神奇的:

public static function configure($object, $properties)
{
    foreach ($properties as $name => $value) {
        $object->$name = $value;
    }

    return $object;
}

配置的過程就是遍歷$config配置數組,將數組的鍵作為屬性名,以對應的數組元素的值對對象的屬性賦值。因此,實現Yii這一統一的配置方式的要點有:

  • 繼承自yii\base\Object
  • 為對象屬性提供setter方法,以正確處理配置過程。
  • 如果需要重載構造函數,請將$config作為該構造函數的最後一個參數,並將該參數傳遞給父構造函數。
  • 重載的構造函數的最後,一定記得調用父構造函數。
  • 如果重載瞭yii\base\Object::init()函數,註意一定要在重載函數的開頭調用父類的init()

隻要實現瞭以上要點,就可以使得你編寫的類可以按照Yii約定俗成的方式進行配置。這在編寫代碼的過程中,帶來許多便利。

像你這麼聰明的,肯定會提出來,如果配置數組的某個配置項,也是一個數組,這怎麼辦? 如果某個對象的屬性,也是一個對象,而非一個簡單的數值或字符串時,又怎麼辦?

這兩個問題,其實是同質的。如果一個對象的屬性,是另一個對象,就像Application裡會引入諸多的Component一樣, 這是很常見的。如後面會看到的$app->request中的request屬性就是一個對象。 那麼,在配置$app時,必然要配置到這個reqeust對象。 既然request也是一個對象,那麼他的配置要是按照Yii的規矩來,也就是用一個數組來配置它。 因此,上面提到的這兩個問題,其實是同質的。

那麼,怎麼實現呢?秘密在於setter函數。由於$app在進行配置時,最終會調用Yii::configure()函數。 該函數又不區分配置項是簡單的數值還是數組,就直接使用$object->$name=$value完成屬性的賦值。 那麼,對於對象屬性,其配置值$value是一個數組,為瞭使其正確配置。 你需要在其setter函數上做出正確的處理方式。 Yii應用yii\web\Application就是依靠定義專門的setter函數,實現自動處理配置項的。 比如,我們在Yii的配置文件中,可以看到一個配置項components,一般情況下,他的內容是這樣的:

'components' => [
    'request' => [
        // !!! insert a secret key in the following (if it is empty) -
        // this is required by cookie validation
        'cookieValidationKey' => 'v7mBbyetv4ls7t8UIqQ2IBO60jY_wf_U',
    ],
    'user' => [
        'identityClass' => 'common\models\User',
        'enableAutoLogin' => true,
    ],
    'log' => [
        'traceLevel' => YII_DEBUG ? 3 : 0,
        'targets' => [
            [
                'class' => 'yii\log\FileTarget',
                'levels' => ['error', 'warning'],
            ],
        ],
    ],
    'errorHandler' => [
        'errorAction' => 'site/error',
    ],
],

這是一個典型嵌套配置數組。那麼Yii是如何把他們配置好的呢? 聰明的你肯定想到瞭,Yii一定是定義瞭一個名為setComponents的setter函數。 當然,Yii並未將該函數放在yii\web\Application裡,而是放在父類yii\di\ServiceLocator裡面。 至於ServiceLocator是何方神聖,在後面服務定位器(Service Locator)部分會講到, 這裡你隻需要知道它是Application的父類,提供components屬性的setter方法就可以瞭:

public function setComponents($components)
{
    foreach ($components as $id => $component) {
        $this->set($id, $component);
    }
}

這裡有個成員函數,$this->set(),他是服務定位器用來註冊服務的方法。 我們暫時不講這個東西,留待服務定位器(Service Locator)部分再講。 現在隻要知道這個函數把配置文件中的components配置項搞定就可以瞭。

yii\base\Object::__construct()來看,對於所有Object,包括Component的屬性,都經歷這麼4個階段:

  1. 預初始化階段。這是最開始的階段,就是在構造函數__construct()的開頭可以設置property的默認值。
  2. 對象配置階段。也就是前面提到構造函數調用Yii::configure($this,$config)階段。 這一階段可以覆蓋前一階段設置的property的默認值,並補充沒有默認值的參數,也就是必備參數。$config通常由外部代碼傳入或者通過配置文件傳入。
  3. 後初始化階段。也就是構造函數調用init()成員函數。 通過在init()寫入代碼,可以對配置階段設置的值進行檢查,並規范類的property。
  4. 類方法調用階段。前面三個階段是不可分的,由類的構造函數一口氣調用的。 也就是說一個類一但實例化,那麼就至少經歷瞭前三個階段。 此時,該對象的狀態是確定且可靠的,不存在不確定的property。 所有的屬性要麼是默認值,要麼是傳入的配置值,如果傳入的配置有誤或者沖突,那麼也經過瞭檢查和規范。 也就是說,你就放心用吧。

You May Also Like