php中文网

PHP 属性挂钩

php中文网

介绍

php 8.4 将于 2024 年 11 月发布,并将带来一个很酷的新功能:属性挂钩。

在本文中,我们将了解什么是属性挂钩以及如何在 php 8.4 项目中使用它们。

顺便说一句,您可能还有兴趣查看我的另一篇文章,其中向您展示了 php 8.4 中添加的新数组函数。

什么是 php 属性挂钩?

属性挂钩允许您为类属性定义自定义 getter 和 setter 逻辑,而无需编写单独的 getter 和 setter 方法。这意味着您可以直接在属性声明中定义逻辑,这样您就可以直接访问属性(例如 $user->firstname),而不必记住调用方法(例如 $user->getfirstname() 和 $user->setfirstname()) .

立即学习“PHP免费学习笔记(深入)”;

您可以在 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。

如果您有兴趣在我每次发布新帖子时获得更新,请随时订阅我的时事通讯。

继续创造精彩的东西! ?

以上就是PHP 属性挂钩的详细内容,更多请关注php中文网其它相关文章!