介紹
php 8.4 將于 2024 年 11 月發(fā)布,并將帶來(lái)一個(gè)很酷的新功能:屬性掛鉤。
在本文中,我們將了解什么是屬性掛鉤以及如何在 php 8.4 項(xiàng)目中使用它們。
順便說(shuō)一句,您可能還有興趣查看我的另一篇文章,其中向您展示了 php 8.4 中添加的新數(shù)組函數(shù)。
什么是 php 屬性掛鉤?
屬性掛鉤允許您為類屬性定義自定義 getter 和 setter 邏輯,而無(wú)需編寫單獨(dú)的 getter 和 setter 方法。這意味著您可以直接在屬性聲明中定義邏輯,這樣您就可以直接訪問(wèn)屬性(例如 $user->firstname),而不必記住調(diào)用方法(例如 $user->getfirstname() 和 $user->setfirstname()) .
您可以在 https://wiki.php.net/rfc/property-hooks 查看此功能的 rfc
如果您是 laravel 開(kāi)發(fā)人員,當(dāng)您閱讀本文時(shí),您可能會(huì)注意到鉤子看起來(lái)與 laravel 模型中的訪問(wèn)器和修改器非常相似。
我非常喜歡屬性掛鉤功能的外觀,我想當(dāng) php 8.4 發(fā)布時(shí)我將在我的項(xiàng)目中使用它。
要了解屬性掛鉤的工作原理,讓我們看一些示例用法。
“獲取”鉤子
您可以定義一個(gè) get 鉤子,每當(dāng)您嘗試訪問(wèn)屬性時(shí)都會(huì)調(diào)用該鉤子。
例如,假設(shè)您有一個(gè)簡(jiǎn)單的 user 類,它在構(gòu)造函數(shù)中接受名字和姓氏。您可能想要定義一個(gè) fullname 屬性,將名字和姓氏連接在一起。為此,您可以為 fullname 屬性定義一個(gè) get 掛鉤:
readonly class user { public string $fullname { get { return $this->firstname.' '.$this->lastname; } } public function __construct( public readonly string $firstname, public readonly string $lastname ) { // } } $user = new user(firstname: 'ash', lastname: 'allen'); echo $user->firstname; // ash echo $user->lastname; // allen echo $user->fullname; // ash allen
登錄后復(fù)制
在上面的示例中,我們可以看到我們?yōu)?fullname 屬性定義了一個(gè) get 鉤子,該鉤子返回一個(gè)通過(guò)將firstname和lastname屬性連接在一起計(jì)算得出的值。我們也可以使用類似于箭頭函數(shù)的語(yǔ)法來(lái)進(jìn)一步清理它:
readonly class user { public string $fullname { get => $this->firstname.' '.$this->lastname; } public function __construct( public readonly string $firstname, public readonly string $lastname, ) { // } } $user = new user(firstname: 'ash', lastname: 'allen'); echo $user->firstname; // ash echo $user->lastname; // allen echo $user->fullname; // ash allen
登錄后復(fù)制
類型兼容性
需要注意的是,getter 的返回值必須與屬性的類型兼容。
如果未啟用嚴(yán)格類型,則該值將根據(jù)屬性類型進(jìn)行類型轉(zhuǎn)換。例如,如果從聲明為字符串的屬性返回整數(shù),則該整數(shù)將轉(zhuǎn)換為字符串:
declare(strict_types=1); class user { public string $fullname { get { return 123; } } public function __construct( public readonly string $firstname, public readonly string $lastname, ) { // } } $user = new user(firstname: 'ash', lastname: 'allen'); echo $user->fullname; // "123"
登錄后復(fù)制
在上面的例子中,即使我們指定了 123 作為要返回的整數(shù),但“123”還是以字符串形式返回,因?yàn)樵搶傩允亲址?/p>
我們可以添加declare(strict_types=1);像這樣添加到代碼頂部以啟用嚴(yán)格的類型檢查:
declare(strict_types=1); class user { public string $fullname { get { return 123; } } public function __construct( public readonly string $firstname, public readonly string $lastname, ) { // } }
登錄后復(fù)制
現(xiàn)在這會(huì)導(dǎo)致拋出錯(cuò)誤,因?yàn)榉祷刂凳钦麛?shù),但屬性是字符串:
fatal error: uncaught typeerror: user::$fullname::get(): return value must be of type string, int returned
登錄后復(fù)制
“設(shè)置”鉤子
php 8.4 屬性鉤子還允許您定義集合鉤子。每當(dāng)您嘗試設(shè)置屬性時(shí)都會(huì)調(diào)用此函數(shù)。
您可以為 set hook 在兩種單獨(dú)的語(yǔ)法之間進(jìn)行選擇:
顯式定義要在屬性上設(shè)置的值
使用箭頭函數(shù)返回要在屬性上設(shè)置的值
讓我們看看這兩種方法。我們想象一下,當(dāng)在 user 類上設(shè)置名字和姓氏的首字母時(shí),我們想要將它們?cè)O(shè)置為大寫:
declare(strict_types=1); class user { public string $firstname { // explicitly set the property value set(string $name) { $this->firstname = ucfirst($name); } } public string $lastname { // use an arrow function and return the value // you want to set on the property set(string $name) => ucfirst($name); } public function __construct( string $firstname, string $lastname ) { $this->firstname = $firstname; $this->lastname = $lastname; } } $user = new user(firstname: 'ash', lastname: 'allen'); echo $user->firstname; // ash echo $user->lastname; // allen
登錄后復(fù)制
正如我們?cè)谏厦娴氖纠兴吹降模覀優(yōu)閒irstname 屬性定義了一個(gè)set hook,在將名稱設(shè)置為屬性之前,該鉤子將名稱的第一個(gè)字母大寫。我們還為 lastname 屬性定義了一個(gè) set hook,它使用箭頭函數(shù)返回要在屬性上設(shè)置的值。
類型兼容性
如果屬性有類型聲明,那么它的 set hook 也必須有兼容的類型集。下面的示例將返回錯(cuò)誤,因?yàn)?firstname 的 set hook 沒(méi)有類型聲明,但屬性本身有 string 的類型聲明:
class user { public string $firstname { set($name) => ucfirst($name); } public string $lastname { set(string $name) => ucfirst($name); } public function __construct( string $firstname, string $lastname ) { $this->firstname = $firstname; $this->lastname = $lastname; } }
登錄后復(fù)制
嘗試運(yùn)行上面的代碼將導(dǎo)致拋出以下錯(cuò)誤:
fatal error: type of parameter $name of hook user::$firstname::set must be compatible with property type
登錄后復(fù)制
一起使用“get”和“set”鉤子
您不限于單獨(dú)使用 get 和 set 掛鉤。您可以在同一房產(chǎn)中一起使用它們。
舉個(gè)簡(jiǎn)單的例子。我們假設(shè)我們的 user 類有一個(gè) fullname 屬性。當(dāng)我們?cè)O(shè)置屬性時(shí),我們會(huì)將全名分為名字和姓氏。我知道這是一種幼稚的方法,并且有更好的解決方案,但這純粹是為了舉例來(lái)突出顯示掛鉤屬性。
代碼可能看起來(lái)像這樣:
declare(strict_types=1); class user { public string $fullname { // dynamically build up the full name from // the first and last name get => $this->firstname.' '.$this->lastname; // split the full name into first and last name and // then set them on their respective properties set(string $name) { $splitname = explode(' ', $name); $this->firstname = $splitname[0]; $this->lastname = $splitname[1]; } } public string $firstname { set(string $name) => $this->firstname = ucfirst($name); } public string $lastname { set(string $name) => $this->lastname = ucfirst($name); } public function __construct(string $fullname) { $this->fullname = $fullname; } } $user = new user(fullname: 'ash allen'); echo $user->firstname; // ash echo $user->lastname; // allen echo $user->fullname; // ash allen
登錄后復(fù)制
在上面的代碼中,我們定義了一個(gè) fullname 屬性,它同時(shí)具有 get 和 set 鉤子。 get 掛鉤通過(guò)將名字和姓氏連接在一起來(lái)返回全名。 set 鉤子將全名拆分為名字和姓氏,并將它們?cè)O(shè)置在各自的屬性上。
您可能還注意到,我們沒(méi)有為 fullname 屬性本身設(shè)置值。相反,如果我們需要讀取 fullname 屬性的值,則會(huì)調(diào)用 get 掛鉤以根據(jù)名字和姓氏屬性構(gòu)建全名。我這樣做是為了強(qiáng)調(diào),您可以擁有一個(gè)不直接設(shè)置值的屬性,而是根據(jù)其他屬性計(jì)算該值。
在升級(jí)屬性上使用屬性掛鉤
屬性掛鉤的一個(gè)很酷的功能是您還可以將它們與構(gòu)造函數(shù)提升的屬性一起使用。
讓我們看一個(gè)不使用提升屬性的類的示例,然后看看使用提升屬性時(shí)它會(huì)是什么樣子。
我們的用戶類可能看起來(lái)像這樣:
readonly class user { public string $fullname { get => $this->firstname.' '.$this->lastname; } public string $firstname { set(string $name) => ucfirst($name); } public string $lastname { set(string $name) => ucfirst($name); } public function __construct( string $firstname, string $lastname, ) { $this->firstname = $firstname; $this->lastname = $lastname; } }
登錄后復(fù)制
我們可以將firstname和lastname屬性提升到構(gòu)造函數(shù)中,并直接在屬性上定義它們的設(shè)置邏輯:
readonly class user { public string $fullname { get => $this->firstname.' '.$this->lastname; } public function __construct( public string $firstname { set (string $name) => ucfirst($name); }, public string $lastname { set (string $name) => ucfirst($name); } ) { // } }
登錄后復(fù)制
只寫掛鉤屬性
如果您使用 setter 定義了一個(gè)掛鉤屬性,但實(shí)際上并未在該屬性上設(shè)置值,則該屬性將是只寫的。這意味著你無(wú)法讀取屬性的值,只能設(shè)置它。
讓我們采用前面示例中的 user 類,并通過(guò)刪除 get 掛鉤將 fullname 屬性修改為只寫:
declare(strict_types=1); class user { public string $fullname { // define a setter that doesn't set a value // on the "fullname" property. this will // make it a write-only property. set(string $name) { $splitname = explode(' ', $name); $this->firstname = $splitname[0]; $this->lastname = $splitname[1]; } } public string $firstname { set(string $name) => $this->firstname = ucfirst($name); } public string $lastname { set(string $name) => $this->lastname = ucfirst($name); } public function __construct( string $fullname, ) { $this->fullname = $fullname; } } $user = new user('ash allen'); echo $user->fullname; // will trigger an error!
登錄后復(fù)制
如果我們運(yùn)行上面的代碼,我們會(huì)在嘗試訪問(wèn) fullname 屬性時(shí)看到拋出以下錯(cuò)誤:
fatal error: uncaught error: property user::$fullname is write-only
登錄后復(fù)制
只讀掛鉤屬性
同樣,屬性也可以是只讀的。
例如,假設(shè)我們只希望從firstname 和lastname 屬性生成fullname 屬性。我們不想允許直接設(shè)置 fullname 屬性。我們可以通過(guò)從 fullname 屬性中刪除 set 鉤子來(lái)實(shí)現(xiàn)這一點(diǎn):
class user { public string $fullname { get { return $this->firstname.' '.$this->lastname; } } public function __construct( public readonly string $firstname, public readonly string $lastname, ) { $this->fullname = 'invalid'; // will trigger an error! } }
登錄后復(fù)制
如果我們嘗試運(yùn)行上面的代碼,則會(huì)拋出以下錯(cuò)誤,因?yàn)槲覀冊(cè)噲D直接設(shè)置 fullname 屬性:
uncaught error: property user::$fullname is read-only
登錄后復(fù)制
使用“readonly”關(guān)鍵字
即使我們的 php 類具有掛鉤屬性,您仍然可以將它們?cè)O(shè)置為只讀。例如,我們可能想讓 user 類只讀:
readonly class user { public string $firstname { set(string $name) => ucfirst($name); } public string $lastname { set(string $name) => ucfirst($name); } public function __construct( string $firstname, string $lastname, ) { $this->firstname = $firstname; $this->lastname = $lastname; } }
登錄后復(fù)制
但是,hook 屬性不能直接使用 readonly 關(guān)鍵字。例如,這個(gè)類將是無(wú)效的:
class user { public readonly string $fullname { get => $this->firstname.' '.$this->lastname; } public function __construct( string $firstname, string $lastname, ) { $this->firstname = $firstname; $this->lastname = $lastname; } }
登錄后復(fù)制
上面的代碼會(huì)拋出以下錯(cuò)誤:
fatal error: hooked properties cannot be readonly
登錄后復(fù)制
“property”魔法常數(shù)
在 php 8.4 中,引入了一個(gè)名為 __property__ 的新魔法常量。該常量可用于引用屬性掛鉤內(nèi)的屬性名稱。
讓我們看一個(gè)例子:
class user { // ... public string $lastname { set(string $name) { echo __property__; // lastname $this->{__property__} = ucfirst($name); // will trigger an error! } } public function __construct( string $firstname, string $lastname, ) { $this->firstname = $firstname; $this->lastname = $lastname; } }
登錄后復(fù)制
在上面的代碼中,我們可以看到在lastname屬性的setter中使用__property__將會(huì)輸出屬性名稱lastname。然而,還值得注意的是,嘗試使用此常量來(lái)嘗試設(shè)置屬性值將觸發(fā)錯(cuò)誤:
fatal error: uncaught error: must not write to virtual property user::$lastname
登錄后復(fù)制
有一個(gè)關(guān)于 __property__ 魔法常量的方便用例示例,您可以在 github 上查看:https://github.com/crell/php-rfcs/blob/master/property-hooks/examples.md。
接口中的掛鉤屬性
php 8.4 還允許您在接口中定義可公開(kāi)訪問(wèn)的掛鉤屬性。如果您想強(qiáng)制類使用鉤子實(shí)現(xiàn)某些屬性,這會(huì)很有用。
讓我們看一下聲明了掛鉤屬性的示例接口:
interface nameable { // expects a public gettable 'fullname' property public string $fullname { get; } // expects a public gettable 'firstname' property public string $firstname { get; } // expects a public settable 'lastname' property public string $lastname { set; } }
登錄后復(fù)制
在上面的接口中,我們定義任何實(shí)現(xiàn) nameable 接口的類都必須具有:
至少可公開(kāi)獲取的 fullname 屬性。這可以通過(guò)定義 get hook 或根本不定義 hook 來(lái)實(shí)現(xiàn)。
至少可公開(kāi)獲取的firstname 屬性。
至少可公開(kāi)設(shè)置的姓氏屬性。這可以通過(guò)定義具有設(shè)置鉤子的屬性或根本不定義鉤子來(lái)實(shí)現(xiàn)。但如果該類是只讀的,那么該屬性必須有一個(gè)設(shè)置的鉤子。
這個(gè)實(shí)現(xiàn) nameable 接口的類是有效的:
class user implements nameable { public string $fullname { get => $this->firstname.' '.$this->lastname; } public string $firstname { set(string $name) => ucfirst($name); } public string $lastname; public function __construct( string $firstname, string $lastname, ) { $this->firstname = $firstname; $this->lastname = $lastname; } }
登錄后復(fù)制
上面的類是有效的,因?yàn)?fullname 屬性有一個(gè) get 鉤子來(lái)匹配接口定義。 firstname 屬性只有一個(gè) set hook,但仍然可以公開(kāi)訪問(wèn),因此它滿足條件。 lastname 屬性沒(méi)有 get 掛鉤,但它是可公開(kāi)設(shè)置的,因此它滿足條件。
讓我們更新 user 類以強(qiáng)制執(zhí)行 fullname 屬性的 get 和 set 掛鉤:
interface nameable { public string $fullname { get; set; } public string $firstname { get; } public string $lastname { set; } }
登錄后復(fù)制
我們的 user 類將不再滿足 fullname 屬性的條件,因?yàn)樗鼪](méi)有定義 set hook。這會(huì)導(dǎo)致拋出以下錯(cuò)誤:
fatal error: class user contains 1 abstract methods and must therefore be declared abstract or implement the remaining methods (nameable::$fullname::set)
登錄后復(fù)制
抽象類中的掛鉤屬性
與接口類似,你也可以在抽象類中定義鉤子屬性。如果您想提供一個(gè)定義子類必須實(shí)現(xiàn)的掛鉤屬性的基類,這可能很有用。您還可以在抽象類中定義鉤子,并在子類中覆蓋它們。
例如,讓我們創(chuàng)建一個(gè) model 抽象類,定義一個(gè)必須由子類實(shí)現(xiàn)的 name 屬性:
abstract class model { abstract public string $fullname { get => $this->firstname.' '.$this->lastname; set; } abstract public string $firstname { get; } abstract public string $lastname { set; } }
登錄后復(fù)制
在上面的抽象類中,我們定義任何擴(kuò)展 model 類的類都必須具有:
至少可公開(kāi)獲取和設(shè)置的 fullname 屬性。這可以通過(guò)定義 get 和 set 鉤子或根本不定義鉤子來(lái)實(shí)現(xiàn)。我們還在抽象類中定義了 fullname 屬性的 get 鉤子,因此我們不需要在子類中定義它,但如果需要,可以覆蓋它。
至少可公開(kāi)獲取的firstname 屬性。這可以通過(guò)定義 get hook 或根本不定義 hook 來(lái)實(shí)現(xiàn)。
至少可公開(kāi)設(shè)置的姓氏屬性。這可以通過(guò)定義具有設(shè)置鉤子的屬性或根本不定義鉤子來(lái)實(shí)現(xiàn)。但如果該類是只讀的,那么該屬性必須有一個(gè)設(shè)置的鉤子。
然后我們可以創(chuàng)建一個(gè)擴(kuò)展 model 類的 user 類:
class User extends Model { public string $fullName; public string $firstName { set(string $name) => ucfirst($name); } public string $lastName; public function __construct( string $firstName, string $lastName, ) { $this->firstName = $firstName; $this->lastName = $lastName; } }
登錄后復(fù)制
結(jié)論
希望本文能讓您深入了解 php 8.4 屬性掛鉤的工作原理以及如何在 php 項(xiàng)目中使用它們。
如果這個(gè)功能一開(kāi)始看起來(lái)有點(diǎn)令人困惑,我也不會(huì)太擔(dān)心。當(dāng)我第一次看到它時(shí),我也有點(diǎn)困惑(特別是它們?nèi)绾闻c接口和抽象類一起工作)。但一旦你開(kāi)始修補(bǔ)它們,你很快就會(huì)掌握它的竅門。
我很高興看到這個(gè)功能將如何在野外使用,我期待著 php 8.4 發(fā)布時(shí)在我的項(xiàng)目中使用它。
如果您喜歡閱讀這篇文章,您可能有興趣查看我的 220 多頁(yè)電子書(shū)“battle ready laravel”,其中更深入地涵蓋了類似的主題。
或者,您可能想查看我的另一本 440 多頁(yè)電子書(shū)“consuming apis in laravel”,它教您如何使用 laravel 來(lái)使用來(lái)自其他服務(wù)的 api。
如果您有興趣在我每次發(fā)布新帖子時(shí)獲得更新,請(qǐng)隨時(shí)訂閱我的時(shí)事通訊。
繼續(xù)創(chuàng)造精彩的東西! ?