學習任何一門學問,往往都是從起基本的概念學起。萬丈高樓平地起,這些基本概念就是高樓的基石,必須做詳盡的分析。我們知道,Yii2是一款脈絡清晰的框架,理順了基礎的概念和基本功能,學習更高級和複雜的功能就容易多了。Yii2是一款純面向對象的框架,它對類的功能做了擴充:PHP類的功能分爲屬性和方法,而Yii2定義了類的三個功能:屬性(property),行爲(behavior)和事件(event)。
爲了更好的實現面向對象的編程,拿到一個現實的對象,要構造一個PHP對象與之對應,如果用Yii2框架去實現,那麼首先要想到的是這個對象有哪些屬性,哪些行爲,哪些事件。
今天先來說說屬性的概念。
我們舉個例子,我們需要對$user對象的name屬性做trim操作,那麼我們首先想到這麼做:
// $user is a instance of User
$user->name = trim($name);
然而,這樣做有個問題,就是如果我們需要對所有地方的User實例的name屬性都要進行trim操作,我們就需要改動多處;另外,假如哪一天我不僅要trim操作,還要首字母大寫,那我還得改動很多地方。萬一我遺漏了怎麼辦?
爲了解決這個問題,Yii2引入了自己的屬性概念。在yii\base\BaseObject中實現了。
注:Yii2.0.12之前是yii\base\Object,Yii2.0.13及以後,爲了考慮到”object”將在PHP7.2版本成爲受保留的專屬名詞,改爲了yii\base\BaseObject
屬性和成員變量
在 PHP 中,類的成員變量和屬性既有區別,又有聯繫,從訪問形式來看看,二者沒有什麼區別,但是,成員變量是就類的結構而言的概念,而屬性是就類的功能邏輯而言的概念。
在具體實踐中,常常會想用一個稍微特殊些的方法實現屬性的讀寫。這就需要用到魔術方法讀取器getter(),和設定器setter()。這兩個方法提供了這樣一種便利:對類的屬性進行某種加工之後再寫,對類的屬性進行某種加工之後再讀(預處理和後處理)。
成員變量和屬性的區別與聯繫在於:
- 成員變量是一個“內”概念,反映的是類的結構構成,與其相對應的是成員函數(類方法)。屬性是一個“外”概念,反映的是類的邏輯意義
- 成員變量沒有讀寫權限控制,而屬性可以指定爲只讀或只寫,或可讀可寫,這就有了權限這麼一說
- 成員變量不對讀出作任何後處理,不對寫入作任何預處理,而屬性則可以
- public成員變量可以視爲一個可讀可寫、沒有任何預處理或後處理的屬性。 而private成員變量由於外部不可見,與屬性“外”的特性不相符,所以不能視爲屬性。同樣的,protect屬性也不能視爲屬性
- 雖然大多數情況下,屬性要由成員變量來實現,但是二者並沒有必然的關係
在Yii中,由 yii\base\BaseObject 提供了對屬性的支持,因此,如果要使你的類支持屬性,只需要繼承此類即可。
getter 和 setter 方法
屬性是通過getter方法和setter方法來定義的。getter 方法是名稱以 get 開頭的方法,而 setter 方法名以 set 開頭,分別是對魔術方法getter()和setter()的進一步封裝。
getter方法
public function __get($name)
{
$getter = 'get' . $name;
if (method_exists($this, $getter)) {
return $this->$getter();
} elseif (method_exists($this, 'set' . $name)) {
....
}
....
}
setter方法
public function __set($name, $value)
{
$setter = 'set' . $name;
if (method_exists($this, $setter)) {
$this->$setter($value);
} elseif (method_exists($this, 'get' . $name)) {
...
} else {
...
}
}
方法名中 get 或 set 後面的部分就定義了該屬性的名字(get或者set後面的部分就是要加工的類的屬性)。 如下面代碼所示,getter 方法 getName() 和 setter 方法 setName() 操作的是 name 屬性:
namespace app\components;
use yii\base\BaseObject ;
class User extend BaseObject
{
private $_name;
public function getName() // 讀操作
{
return $this->_name; // 讀之前可以做後處理
}
public function setName($value) // 寫操作
{
$this->_name = trim($value); // 寫之前可以做預處理
}
}
小編心得:這種對某種操作的“預處理”和“後處理”在框架中到處可見,雖說還算不上一種設計思想,但是作爲一種技巧,可是大大提高了程序的可擴展性!
getter和setter方法創建了一個名爲name的屬性。在這個例子裏,它指向了一個私有成員變量$_name。getter和setter定義的屬性和類的成員變量一樣。二者的的主要區別在於:當這種屬性被讀取時,對應的 getter 方法將被調用; 而當屬性被賦值時,對應的 setter 方法就調用。如:
// 等效於$name = $user->getName();
$name = $user->name;
// $user = $user->setName('Jason');
$user->name = 'Jason';
如果我們像這樣去定義“屬性”,而不是簡單粗暴的定義一個名爲publice $name的成員變量,那麼就可以實現在任何地方,對User的實例的name屬性進行trim操作,不用擔心有漏網之魚。同樣,有了setter函數,我們如果還可以方便的再加上“首字母大寫”的操作:
public function setName($value)
{
$this->_name = ucfirst(trim($value));
}
同樣,在屬性讀之後的自定義處理:
public function getName()
{
return ucfirst($this->_name);
}
或者結合更多成員變量做更爲複雜的處理:
public function getName()
{
return ucfirst($this->_firstname).' '.ucfirst($this->_lastname);
}
只讀屬性和只寫屬性
只定義了 getter 沒有 setter 的屬性是隻讀屬性。 嘗試賦值給這樣的屬性將導致 yii\base\InvalidCallException (無效調用)異常。 類似的,只有 setter 方法而沒有 getter 方法定義的屬性是隻寫屬性,嘗試讀取這種屬性也會觸發相同異常,只不過只寫屬性的在現實應用中幾乎沒有。
通過 getter 和 setter 定義的屬性也有一些特殊規則和限制:
- 這類屬性的名字是不區分大小寫的。如
$user->name
和$user->Name
是同一個屬性。 因爲PHP方法名是不區分大小寫的。 - 如果此類屬性名和類public成員變量相同,以後者爲準。比如$user有age屬性(即擁有setter或getter方法),同時也擁有
public $age
成員變量。那麼無論在何種情況下只能用到成員變量public $age
,不會用到作爲屬性的age。這很好理解,因爲setter和getter都是魔術方法,只在被操作的成員變量不存在時調用,現在成員變量age存在了,那無論是$age->age
的讀操作還是$age->age = xxx
的寫操作都只會直接調用public $age
了。 - 屬性不支持可見性(訪問限制),無論setter還是getter方法都只能是public的。定義爲private和protected有違初衷。
這類屬性的 getter 和 setter 方法只能定義爲非靜態的,若定義爲靜態方法(static)則不會以相同方式處理。 - 既然Yii2屬性已然不同於PHP的成員變量,那麼property_exists()方法就不適宜用來判斷
yii\base\BaseObject
及其子類的屬性是否存在,而應該改用yii\base\BaseObject的hasProperty()/canGetProperty() /canSetProperty()
等方法。
屬性的實現步驟
下面幾步可以實現屬性:
- 繼承自 yii\base\BaseObject,如User
- 聲明一個用於保存該屬性的私有成員變量,如
$_name
,$_age
- 提供getter或setter函數,或兩者都提供,用於訪問、修改上面提到的私有成員變量。如果只提供了getter,那麼該屬性爲只讀屬性,只提供了setter,則爲只寫屬性。
class User extend BaseObject // 1.繼承yii\base\BaseObject
{
private $_name; // 2.聲明一個用於保存該屬性的私有成員變量
public function getName() // 3.提供一個getter或者setter
{
return $this->_name;
}
public function setName($value)
{
$this->_name = trim($value);
}
}
小編心得:成員變量對外不可見是比較好的編程習慣。將成員變量的讀寫操作分開比合在一起方便。
BaseObject中屬性的其他方法
hasProperty()
測試一個類是否存在某種屬性:
public function hasProperty($name, $checkVars = true)
{
return $this->canGetProperty($name, $checkVars) || $this->canSetProperty($name, false);
}
即定義了getter或setter,如果$checkVars爲true,那麼類如有同名的成員變量(public/protected/private)也會任何屬性存在。
canGetProperty()
測試某個屬性是否可讀:
public function canGetProperty($name, $checkVars = true)
{
return method_exists($this, 'get' . $name) || $checkVars && property_exists($this, $name);
}
$checkVars意義同上。
canSetProperty()
測試一個屬性是否可寫:
public function canSetProperty($name, $checkVars = true)
{
return method_exists($this, 'set' . $name) || $checkVars && property_exists($this, $name);
}
$checkVars意義同上,只要類定義了成員變量,不管是public還是private 還是 protected, 都認爲是可寫。
當然,gettter()和setter()方法是在遍歷所有成員變量,找不到所需的時候才調用的,因此屬性天生效率就要低一些。在一些類功能簡單,表示數據結構,數據集合時且不需要讀寫控制時,可以考慮直接使用成員變量作爲屬性。
另外,我們可以看到,在框架內部,幾乎都是使用$response = $app->getResponse()
來代替$response = $app->response
; 用$app->setRequest(xxx)
代替$app->request = xxx
的情況,幾乎從來看不到直接對屬性賦值的情況(如後者),從而避免對成員變量的遍歷——框架自身還是格外地注重效率的,至於便利性,則留給了開發者啦!