介紹
php 8.4 將于 2024 年 11 月發布,并將帶來一個很酷的新功能:屬性掛鉤。
在本文中,我們將了解什么是屬性掛鉤以及如何在 php 8.4 項目中使用它們。
順便說一句,您可能還有興趣查看我的另一篇文章,其中向您展示了 php 8.4 中添加的新數組函數。
什么是 php 屬性掛鉤?
屬性掛鉤允許您為類屬性定義自定義 getter 和 setter 邏輯,而無需編寫單獨的 getter 和 setter 方法。這意味著您可以直接在屬性聲明中定義邏輯,這樣您就可以直接訪問屬性(例如 $user->firstname),而不必記住調用方法(例如 $user->getfirstname() 和 $user->setfirstname()) .
您可以在 https://wiki.php.net/rfc/property-hooks 查看此功能的 rfc
如果您是 laravel 開發人員,當您閱讀本文時,您可能會注意到鉤子看起來與 laravel 模型中的訪問器和修改器非常相似。
我非常喜歡屬性掛鉤功能的外觀,我想當 php 8.4 發布時我將在我的項目中使用它。
要了解屬性掛鉤的工作原理,讓我們看一些示例用法。
“獲取”鉤子
您可以定義一個 get 鉤子,每當您嘗試訪問屬性時都會調用該鉤子。
例如,假設您有一個簡單的 user 類,它在構造函數中接受名字和姓氏。您可能想要定義一個 fullname 屬性,將名字和姓氏連接在一起。為此,您可以為 fullname 屬性定義一個 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
登錄后復制
在上面的示例中,我們可以看到我們為 fullname 屬性定義了一個 get 鉤子,該鉤子返回一個通過將firstname和lastname屬性連接在一起計算得出的值。我們也可以使用類似于箭頭函數的語法來進一步清理它:
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
登錄后復制
類型兼容性
需要注意的是,getter 的返回值必須與屬性的類型兼容。
如果未啟用嚴格類型,則該值將根據屬性類型進行類型轉換。例如,如果從聲明為字符串的屬性返回整數,則該整數將轉換為字符串:
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"
登錄后復制
在上面的例子中,即使我們指定了 123 作為要返回的整數,但“123”還是以字符串形式返回,因為該屬性是字符串。
我們可以添加declare(strict_types=1);像這樣添加到代碼頂部以啟用嚴格的類型檢查:
declare(strict_types=1); class user { public string $fullname { get { return 123; } } public function __construct( public readonly string $firstname, public readonly string $lastname, ) { // } }
登錄后復制
現在這會導致拋出錯誤,因為返回值是整數,但屬性是字符串:
fatal error: uncaught typeerror: user::$fullname::get(): return value must be of type string, int returned
登錄后復制
“設置”鉤子
php 8.4 屬性鉤子還允許您定義集合鉤子。每當您嘗試設置屬性時都會調用此函數。
您可以為 set hook 在兩種單獨的語法之間進行選擇:
顯式定義要在屬性上設置的值
使用箭頭函數返回要在屬性上設置的值
讓我們看看這兩種方法。我們想象一下,當在 user 類上設置名字和姓氏的首字母時,我們想要將它們設置為大寫:
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
登錄后復制
正如我們在上面的示例中所看到的,我們為firstname 屬性定義了一個set hook,在將名稱設置為屬性之前,該鉤子將名稱的第一個字母大寫。我們還為 lastname 屬性定義了一個 set hook,它使用箭頭函數返回要在屬性上設置的值。
類型兼容性
如果屬性有類型聲明,那么它的 set hook 也必須有兼容的類型集。下面的示例將返回錯誤,因為 firstname 的 set hook 沒有類型聲明,但屬性本身有 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; } }
登錄后復制
嘗試運行上面的代碼將導致拋出以下錯誤:
fatal error: type of parameter $name of hook user::$firstname::set must be compatible with property type
登錄后復制
一起使用“get”和“set”鉤子
您不限于單獨使用 get 和 set 掛鉤。您可以在同一房產中一起使用它們。
舉個簡單的例子。我們假設我們的 user 類有一個 fullname 屬性。當我們設置屬性時,我們會將全名分為名字和姓氏。我知道這是一種幼稚的方法,并且有更好的解決方案,但這純粹是為了舉例來突出顯示掛鉤屬性。
代碼可能看起來像這樣:
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
登錄后復制
在上面的代碼中,我們定義了一個 fullname 屬性,它同時具有 get 和 set 鉤子。 get 掛鉤通過將名字和姓氏連接在一起來返回全名。 set 鉤子將全名拆分為名字和姓氏,并將它們設置在各自的屬性上。
您可能還注意到,我們沒有為 fullname 屬性本身設置值。相反,如果我們需要讀取 fullname 屬性的值,則會調用 get 掛鉤以根據名字和姓氏屬性構建全名。我這樣做是為了強調,您可以擁有一個不直接設置值的屬性,而是根據其他屬性計算該值。
在升級屬性上使用屬性掛鉤
屬性掛鉤的一個很酷的功能是您還可以將它們與構造函數提升的屬性一起使用。
讓我們看一個不使用提升屬性的類的示例,然后看看使用提升屬性時它會是什么樣子。
我們的用戶類可能看起來像這樣:
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; } }
登錄后復制
我們可以將firstname和lastname屬性提升到構造函數中,并直接在屬性上定義它們的設置邏輯:
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); } ) { // } }
登錄后復制
只寫掛鉤屬性
如果您使用 setter 定義了一個掛鉤屬性,但實際上并未在該屬性上設置值,則該屬性將是只寫的。這意味著你無法讀取屬性的值,只能設置它。
讓我們采用前面示例中的 user 類,并通過刪除 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!
登錄后復制
如果我們運行上面的代碼,我們會在嘗試訪問 fullname 屬性時看到拋出以下錯誤:
fatal error: uncaught error: property user::$fullname is write-only
登錄后復制
只讀掛鉤屬性
同樣,屬性也可以是只讀的。
例如,假設我們只希望從firstname 和lastname 屬性生成fullname 屬性。我們不想允許直接設置 fullname 屬性。我們可以通過從 fullname 屬性中刪除 set 鉤子來實現這一點:
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! } }
登錄后復制
如果我們嘗試運行上面的代碼,則會拋出以下錯誤,因為我們試圖直接設置 fullname 屬性:
uncaught error: property user::$fullname is read-only
登錄后復制
使用“readonly”關鍵字
即使我們的 php 類具有掛鉤屬性,您仍然可以將它們設置為只讀。例如,我們可能想讓 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; } }
登錄后復制
但是,hook 屬性不能直接使用 readonly 關鍵字。例如,這個類將是無效的:
class user { public readonly string $fullname { get => $this->firstname.' '.$this->lastname; } public function __construct( string $firstname, string $lastname, ) { $this->firstname = $firstname; $this->lastname = $lastname; } }
登錄后復制
上面的代碼會拋出以下錯誤:
fatal error: hooked properties cannot be readonly
登錄后復制
“property”魔法常數
在 php 8.4 中,引入了一個名為 __property__ 的新魔法常量。該常量可用于引用屬性掛鉤內的屬性名稱。
讓我們看一個例子:
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; } }
登錄后復制
在上面的代碼中,我們可以看到在lastname屬性的setter中使用__property__將會輸出屬性名稱lastname。然而,還值得注意的是,嘗試使用此常量來嘗試設置屬性值將觸發錯誤:
fatal error: uncaught error: must not write to virtual property user::$lastname
登錄后復制
有一個關于 __property__ 魔法常量的方便用例示例,您可以在 github 上查看:https://github.com/crell/php-rfcs/blob/master/property-hooks/examples.md。
接口中的掛鉤屬性
php 8.4 還允許您在接口中定義可公開訪問的掛鉤屬性。如果您想強制類使用鉤子實現某些屬性,這會很有用。
讓我們看一下聲明了掛鉤屬性的示例接口:
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; } }
登錄后復制
在上面的接口中,我們定義任何實現 nameable 接口的類都必須具有:
至少可公開獲取的 fullname 屬性。這可以通過定義 get hook 或根本不定義 hook 來實現。
至少可公開獲取的firstname 屬性。
至少可公開設置的姓氏屬性。這可以通過定義具有設置鉤子的屬性或根本不定義鉤子來實現。但如果該類是只讀的,那么該屬性必須有一個設置的鉤子。
這個實現 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; } }
登錄后復制
上面的類是有效的,因為 fullname 屬性有一個 get 鉤子來匹配接口定義。 firstname 屬性只有一個 set hook,但仍然可以公開訪問,因此它滿足條件。 lastname 屬性沒有 get 掛鉤,但它是可公開設置的,因此它滿足條件。
讓我們更新 user 類以強制執行 fullname 屬性的 get 和 set 掛鉤:
interface nameable { public string $fullname { get; set; } public string $firstname { get; } public string $lastname { set; } }
登錄后復制
我們的 user 類將不再滿足 fullname 屬性的條件,因為它沒有定義 set hook。這會導致拋出以下錯誤:
fatal error: class user contains 1 abstract methods and must therefore be declared abstract or implement the remaining methods (nameable::$fullname::set)
登錄后復制
抽象類中的掛鉤屬性
與接口類似,你也可以在抽象類中定義鉤子屬性。如果您想提供一個定義子類必須實現的掛鉤屬性的基類,這可能很有用。您還可以在抽象類中定義鉤子,并在子類中覆蓋它們。
例如,讓我們創建一個 model 抽象類,定義一個必須由子類實現的 name 屬性:
abstract class model { abstract public string $fullname { get => $this->firstname.' '.$this->lastname; set; } abstract public string $firstname { get; } abstract public string $lastname { set; } }
登錄后復制
在上面的抽象類中,我們定義任何擴展 model 類的類都必須具有:
至少可公開獲取和設置的 fullname 屬性。這可以通過定義 get 和 set 鉤子或根本不定義鉤子來實現。我們還在抽象類中定義了 fullname 屬性的 get 鉤子,因此我們不需要在子類中定義它,但如果需要,可以覆蓋它。
至少可公開獲取的firstname 屬性。這可以通過定義 get hook 或根本不定義 hook 來實現。
至少可公開設置的姓氏屬性。這可以通過定義具有設置鉤子的屬性或根本不定義鉤子來實現。但如果該類是只讀的,那么該屬性必須有一個設置的鉤子。
然后我們可以創建一個擴展 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; } }
登錄后復制
結論
希望本文能讓您深入了解 php 8.4 屬性掛鉤的工作原理以及如何在 php 項目中使用它們。
如果這個功能一開始看起來有點令人困惑,我也不會太擔心。當我第一次看到它時,我也有點困惑(特別是它們如何與接口和抽象類一起工作)。但一旦你開始修補它們,你很快就會掌握它的竅門。
我很高興看到這個功能將如何在野外使用,我期待著 php 8.4 發布時在我的項目中使用它。
如果您喜歡閱讀這篇文章,您可能有興趣查看我的 220 多頁電子書“battle ready laravel”,其中更深入地涵蓋了類似的主題。
或者,您可能想查看我的另一本 440 多頁電子書“consuming apis in laravel”,它教您如何使用 laravel 來使用來自其他服務的 api。
如果您有興趣在我每次發布新帖子時獲得更新,請隨時訂閱我的時事通訊。
繼續創造精彩的東西! ?