首頁  >  文章  >  後端開發  >  PHP 屬性掛鉤

PHP 屬性掛鉤

PHPz
PHPz原創
2024-08-21 20:44:501006瀏覽

PHP  Property Hooks

介紹

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鉤子沒有類型聲明,但屬性本身有字串類型聲明:

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

In the code above, we've defined a fullName property that has both a get and set hook. The get hook returns the full name by concatenating the first and last name together. The set hook splits the full name into the first and last name and sets them on their respective properties.

You may have also noticed that we're not setting a value on the fullName property itself. Instead, if we need to read the value of the fullName property, the get hook will be called to build up the full name from the first and last name properties. I've done this to highlight that you can have a property that doesn't have a value set directly on it, but instead, the value is calculated from other properties.

Using Property Hooks on Promoted Properties

A cool feature of property hooks is that you can also use them with constructor promoted properties.

Let's check out an example of a class that isn't using promoted properties and then look at what it might look like using promoted properties.

Our User class might look like so:

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;
    }
}

We could promote the firstName and lastName properties into the constructor and define their set logic directly on the property:

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);
        }
    ) {
        //
    }
}  

Write-only Hooked Properties

If you define a hooked property with a setter that doesn't actually set a value on the property, then the property will be write-only. This means you can't read the value of the property, you can only set it.

Let's take our User class from the previous example and modify the fullName property to be write-only by removing the get hook:

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!

If we were to run the code above, we'd see the following error being thrown when attempting to access the fullName property:

Fatal error: Uncaught Error: Property User::$fullName is write-only

Read-only Hooked Properties

Similarly, a property can be read-only.

For example, imagine we only ever want the fullName property to be generated from the firstName and lastName properties. We don't want to allow the fullName property to be set directly. We can achieve this by removing the set hook from the fullName property:

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!
    }
}

If we were to try and run the code above, the following error would be thrown because we're trying to set the fullName property directly:

Uncaught Error: Property User::$fullName is read-only

Using the "readonly" keyword

You can still make our PHP classes readonly even if they have hooked properties. For example, we may want to make the User class readonly:

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;
    }
}

However, a hooked property cannot use the readonly keyword directly. For example, this class would be invalid:

class User
{
    public readonly string $fullName {
        get => $this->firstName.' '.$this->lastName;
    }

    public function __construct(
        string $firstName,
        string $lastName,
    ) {
        $this->firstName = $firstName;
        $this->lastName = $lastName;
    }
}

The above code would throw the following error:

Fatal error: Hooked properties cannot be readonly

The "PROPERTY" Magic Constant

In PHP 8.4, a new magic constant called __PROPERTY__ has been introduced. This constant can be used to reference the property name within the property hook.

Let's take a look at an example:

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;
    }
}

In the code above, we can see that using __PROPERTY__ inside the lastName property's setter will output the property name lastName. However, it's also worth noting that trying to use this constant in an attempt to set the property value will trigger an error:

Fatal error: Uncaught Error: Must not write to virtual property User::$lastName

There's a handy use case example for the __PROPERTY__ magic constant that you can check out on GitHub: https://github.com/Crell/php-rfcs/blob/master/property-hooks/examples.md.

Hooked Properties in Interfaces

PHP 8.4 also allows you to define publicly accessible hooked properties in interfaces. This can be useful if you want to enforce that a class implements certain properties with hooks.

Let's take a look at an example interface with hooked properties declared:

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; }
}

In the interface above, we're defining that any classes implementing the Nameable interface must have:

  • A fullName property that is at least publicly gettable. This can be achieved by defining a get hook or not defining a hook at all.
  • A firstName property that is at least publicly gettable.
  • A lastName property that is at least publicly settable. This can be achieved by defining a property which has a set hook or not defining a hook at all. But if the class is read-only then the property must have a set hook.

This class that implements the Nameable interface would be valid:

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;
    }
}

The class above would be valid because the fullName property has a get hook to match the interface definition. The firstName property only has a set hook, but is still publicly accessible so it satisfies the criteria. The lastName property doesn't have a get hook, but it is publicly settable so it satisfies the criteria.

Let's update our User class to enforce a get and set hook for the fullName property:

interface Nameable
{
    public string $fullName { get; set; }

    public string $firstName { get; }

    public string $lastName { set; }
}

Our User class would no longer satisfy the criteria for the fullName property because it doesn't have a set hook defined. It would cause the following error to be thrown:

Fatal error: Class User contains 1 abstract methods and must therefore be declared abstract or implement the remaining methods (Nameable::$fullName::set)

Hooked Properties in Abstract Classes

Similar to interfaces, you can also define hooked properties in abstract classes. This can be useful if you want to provide a base class that defines hooked properties that child classes must implement. You can also define the hooks in the abstract class and have them be overridden in the child classes.

For example, let's make a Model abstract class that defines a name property that must be implemented by child classes:

abstract class Model
{
    abstract public string $fullName {
        get => $this->firstName.' '.$this->lastName;
        set;
    }

    abstract public string $firstName { get; }

    abstract public string $lastName { set; }
}

In the abstract class above, we're defining that any classes that extend the Model class must have:

  • A fullName property that is at least publicly gettable and settable. This can be achieved by defining a get and set hook or not defining a hook at all. We've also defined the get hook for the fullName property in the abstract class so we don't need to define it in the child classes, but it can be overridden if needed.
  • A firstName property that is at least publicly gettable. This can be achieved by defining a get hook or not defining a hook at all.
  • A lastName property that is at least publicly settable. This can be achieved by defining a property which has a set hook or not defining a hook at all. But if the class is read-only then the property must have a set hook.

We could then create a User class that extends the Model class:

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;
    }
}

Conclusion

Hopefully, this article should have given you an insight into how PHP 8.4 property hooks work and how you might be able to use them in your PHP projects.

I wouldn't worry too much if this feature seems a little confusing at first. When I first saw it, I was a little confused too (especially with how they work with interfaces and abstract classes). But once you start tinkering with them, you'll soon get the hang of it.

I'm excited to see how this feature will be used in the wild and I'm looking forward to using it in my projects when PHP 8.4 is released.

If you enjoyed reading this post, you might be interested in checking out my 220+ page ebook "Battle Ready Laravel" which covers similar topics in more depth.

Or, you might want to check out my other 440+ page ebook "Consuming APIs in Laravel" which teaches you how to use Laravel to consume APIs from other services.

If you're interested in getting updated each time I publish a new post, feel free to sign up for my newsletter.

Keep on building awesome stuff! ?

以上是PHP 屬性掛鉤的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn