webinarium / php-properties
PHP 自动属性实现
Requires
- php: >=7.4
Requires (Dev)
- friendsofphp/php-cs-fixer: ^2.16
- phpunit/phpunit: ^7.0
README
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
注解,它将保持隐藏。
自定义(虚拟)属性
特质包含两个受保护函数——getters
和 setters
——这些函数可以在你的类中重写。这两个函数都返回关联的匿名函数数组,数组的键是虚拟属性的名字。
假设我们想要存储一些用户特定的设置,如用户语言和用户时区。我们可能会有很多这样的配置选项,我们不希望相关的数据库表因为相同的列而变得臃肿,而所有这些设置都可以存储在一个单独的 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