webinarium/php-properties

1.1.0 2020-07-30 04:47 UTC

This package is auto-updated.

Last update: 2024-08-29 04:31:23 UTC


README

PHP Latest Stable Version Build Status Code Coverage Scrutinizer Code Quality

PHP 自动属性实现

总结

该库提供了一些辅助特质

  • 以模拟在 C# 中定义的自动/自定义属性,
  • 以模拟对象创建时的初始化,就像在 C# 中一样。

安装

推荐安装方式是通过 Composer

composer require webinarium/php-properties

问题

假设我们需要一个类来表示用户的实体(在 Web 开发中非常常见)。该类必须提供只读的 ID、可写的姓和名,以及一个辅助函数来从姓和名获取全名

class User
{
    protected int $id;
    protected string $firstName;
    protected string $lastName;

    public function getId(): int
    {
        return $this->id;
    }

    public function getFirstName(): string
    {
        return $this->firstName;
    }

    public function setFirstName(string $firstName)
    {
        $this->firstName = $firstName;
    }

    public function getLastName(): string
    {
        return $this->lastName;
    }

    public function setLastName(string $lastName)
    {
        $this->lastName = $lastName;
    }

    public function getFullName(): string
    {
        return $this->firstName . ' ' . $this->lastName;
    }
}

只有三个属性和一个额外的函数来获取全名,但我们的类已经因为很多 getter 和 setter 方法而变得臃肿。

与此同时,在 C# 中,同一个类可以这样实现:

public class User
{
    public int Id { get; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string FullName {
        get {
            return FirstName + " " + LastName;
        }
    }
}

简洁易懂——即使你不了解 C#,也能理解这段代码的含义。我们如何在 PHP 中实现同样的效果呢?

魔术方法

当然,第一个想法是使用魔术方法,因此我们可以将类重写如下

/**
 * @property-read int    $id
 * @property      string $firstName
 * @property      string $lastName
 * @property-read string $fullName
 */
class User
{
    protected int $id;
    protected string $firstName;
    protected string $lastName;

    public function __isset($name)
    {
        if ($name === 'fullName') {
            return true;
        }

        return property_exists($this, $name);
    }

    public function __get($name)
    {
        if ($name === 'fullName') {
            return $this->firstName . ' ' . $this->lastName;
        }

        return property_exists($this, $name)
            ? $this->$name
            : null;
    }

    public function __set($name, $value)
    {
        if ($name === 'id') {
            return;
        }

        if (property_exists($this, $name)) {
            $this->$name = $value;
        }
    }
}

嗯,它工作了,但代码量几乎和原始类一样多。假设有更多属性,其中一些是只读的(如 Id)和一些是“虚拟的”(如 fullName),你将在所有三个魔术方法中得到长的 switch 操作符。此外,我敢打赌你的 IDE 不会自动完成这些属性,因此我们必须在类中附加 @property 注解。

注解

我们无法更改 PHP 语法,但我们可以通过注解来扩展它。实际上,如果我们不得不在上面的示例中编写注解,为什么不重用它们,而不是每次引入新属性时都更新所有三个魔术方法。这正是这个库所做的事情,在 PropertyTrait 中提供所需的功能。

如果你在类中包含了 PropertyTrait,那么 @property 注解就变成了关于你的属性的一个必要声明。让我们使用特质重构我们的类

/**
 * @property-read  int    $id
 * @property       string $firstName
 * @property       string $lastName
 * @property-read  string $fullName
 */
class User
{
    use PropertyTrait;

    protected int $id;
    protected string $firstName;
    protected string $lastName;

    protected function getters(): array
    {
        return [
            'fullName' => fn (): string => $this->firstName . ' ' . $this->lastName,
        ];
    }
}

也许仍然没有 C# 版本那么优雅,但确实更接近,不是吗?

自动属性

使用 @property 注解,你可以公开任何现有的受保护或私有属性。要使属性为只读(或只写),请使用 @property-read(或 @property-write)注解。如果你没有为某些现有的非公共属性指定 @property 注解,它将保持隐藏。

自定义(虚拟)属性

特质包含两个受保护函数——getterssetters——这些函数可以在你的类中重写。这两个函数都返回关联的匿名函数数组,数组的键是虚拟属性的名字。

假设我们想要存储一些用户特定的设置,如用户语言和用户时区。我们可能会有很多这样的配置选项,我们不希望相关的数据库表因为相同的列而变得臃肿,而所有这些设置都可以存储在一个单独的 settings 数组中

/**
 * ...
 * @property string $language
 * @property string $timezone
 */
class User
{
    use PropertyTrait;

    ...
    protected array $settings;

    protected function getters(): array
    {
        return [
            'language' => fn (): string => $this->settings['language'] ?? 'en',
            'timezone' => fn (): string => $this->settings['timezone'] ?? 'UTC',
        ];
    }

    protected function setters(): array
    {
        return [

            'language' => function (string $value): void {
                $this->settings['language'] = $value;
            },

            'timezone' => function (string $value): void {
                $this->settings['timezone'] = $value;
            },
        ];
    }
}

实际上,你还可以通过为现有属性提供自定义的 getter 和 setter 函数来提供你的自定义 getter 和 setter。在这种情况下,你将覆盖属性的默认行为。

性能

注释非常昂贵。为了解决这个问题,特性将解析注释缓存到内存中,因此它们只解析一次(每个web请求)。下表展示了不同方式处理类属性的一些基准测试。每个数字表示读取一个属性100000(十万个)次所需的时间(秒)。这些数字是通过5次连续运行计算得出的。

对象创建时的初始化

C#还有一个很好的特性,允许你在创建对象时初始化对象属性。以下是从上面的User类中创建和设置新对象的经典方式

var user = new User();

user.FirstName = "Artem";
user.LastName  = "Rodygin";

或者创建对象时初始化属性

var user = new User
{
    FirstName = "Artem",
    LastName  = "Rodygin"
};

该库提供了一个名为DataTransferObjectTrait的特性,用于模拟这种初始化。假设我们在User PHP类中使用了这个特性,那么可以按照以下方式创建新对象

$user = new User([
    'firstName' => 'Artem',
    'lastName'  => 'Rodygin',
]);

特性定义了一个默认构造函数,它接受单个array参数。这是一个关联数组,其中键是要初始化的属性名称。如果某些键找不到属性,它将被简单地跳过。

开发

./bin/php-cs-fixer fix
./bin/phpunit --coverage-text