属性概述

(PHP 8)

属性提供了一种在代码声明中添加结构化、可机器读取的元数据信息的能力:类、方法、函数、参数、属性和类常量都可以作为属性的目标。然后,可以使用 反射 API 在运行时检查属性定义的元数据。因此,可以将属性视为嵌入到代码中的配置语言。

使用属性,可以将功能的通用实现与其在应用程序中的具体使用分离。在某种程度上,它类似于接口及其实现。但是,接口和实现是关于代码的,而属性是关于注释额外信息和配置的。接口可以由类实现,而属性也可以声明在方法、函数、参数、属性和类常量上。因此,它们比接口更灵活。

一个简单的属性使用示例是将具有可选方法的接口转换为使用属性。假设一个 ActionHandler 接口代表应用程序中的操作,其中一些操作处理程序的实现需要设置,而另一些则不需要。与其要求实现 ActionHandler 的所有类都实现一个方法 setUp(),不如使用一个属性。这种方法的一个好处是我们可以在多个地方使用属性。

示例 #1 使用属性实现接口的可选方法

<?php
interface ActionHandler
{
public function
execute();
}

#[
Attribute]
class
SetUp {}

class
CopyFile implements ActionHandler
{
public
string $fileName;
public
string $targetDirectory;

#[
SetUp]
public function
fileExists()
{
if (!
file_exists($this->fileName)) {
throw new
RuntimeException("File does not exist");
}
}

#[
SetUp]
public function
targetDirectoryExists()
{
if (!
file_exists($this->targetDirectory)) {
mkdir($this->targetDirectory);
} elseif (!
is_dir($this->targetDirectory)) {
throw new
RuntimeException("Target directory $this->targetDirectory is not a directory");
}
}

public function
execute()
{
copy($this->fileName, $this->targetDirectory . '/' . basename($this->fileName));
}
}

function
executeAction(ActionHandler $actionHandler)
{
$reflection = new ReflectionObject($actionHandler);

foreach (
$reflection->getMethods() as $method) {
$attributes = $method->getAttributes(SetUp::class);

if (
count($attributes) > 0) {
$methodName = $method->getName();

$actionHandler->$methodName();
}
}

$actionHandler->execute();
}

$copyAction = new CopyFile();
$copyAction->fileName = "/tmp/foo.jpg";
$copyAction->targetDirectory = "/home/user";

executeAction($copyAction);
添加备注

用户贡献的备注 3 条备注

33
Florian Krmer
1 年前
我尝试了 Harshdeep 的示例,它没有开箱即用,我认为它不完整,所以我编写了一个完整的、可工作的关于基于属性的序列化的简单示例。

<?php
declare(strict_types=1);

#[
Attribute(Attribute::TARGET_CLASS_CONSTANT|Attribute::TARGET_PROPERTY)]
class
JsonSerialize
{
public function
__construct(public ?string $fieldName = null) {}
}

class
VersionedObject
{
#[
JsonSerialize]
public const
version = '0.0.1';
}

class
UserLandClass extends VersionedObject
{
protected
string $notSerialized = 'nope';

#[
JsonSerialize('foobar')]
public
string $myValue = '';

#[
JsonSerialize('companyName')]
public
string $company = '';

#[
JsonSerialize('userLandClass')]
protected ?
UserLandClass $test;

public function
__construct(?UserLandClass $userLandClass = null)
{
$this->test = $userLandClass;
}
}

class
AttributeBasedJsonSerializer {

protected const
ATTRIBUTE_NAME = 'JsonSerialize';

public function
serialize($object)
{
$data = $this->extract($object);

return
json_encode($data, JSON_THROW_ON_ERROR);
}

protected function
reflectProperties(array $data, ReflectionClass $reflectionClass, object $object)
{
$reflectionProperties = $reflectionClass->getProperties();
foreach (
$reflectionProperties as $reflectionProperty) {
$attributes = $reflectionProperty->getAttributes(static::ATTRIBUTE_NAME);
foreach (
$attributes as $attribute) {
$instance = $attribute->newInstance();
$name = $instance->fieldName ?? $reflectionProperty->getName();
$value = $reflectionProperty->getValue($object);
if (
is_object($value)) {
$value = $this->extract($value);
}
$data[$name] = $value;
}
}

return
$data;
}

protected function
reflectConstants(array $data, ReflectionClass $reflectionClass)
{
$reflectionConstants = $reflectionClass->getReflectionConstants();
foreach (
$reflectionConstants as $reflectionConstant) {
$attributes = $reflectionConstant->getAttributes(static::ATTRIBUTE_NAME);
foreach (
$attributes as $attribute) {
$instance = $attribute->newInstance();
$name = $instance->fieldName ?? $reflectionConstant->getName();
$value = $reflectionConstant->getValue();
if (
is_object($value)) {
$value = $this->extract($value);
}
$data[$name] = $value;
}
}

return
$data;
}

protected function
extract(object $object)
{
$data = [];
$reflectionClass = new ReflectionClass($object);
$data = $this->reflectProperties($data, $reflectionClass, $object);
$data = $this->reflectConstants($data, $reflectionClass);

return
$data;
}
}

$userLandClass = new UserLandClass();
$userLandClass->company = 'some company name';
$userLandClass->myValue = 'my value';

$userLandClass2 = new UserLandClass($userLandClass);
$userLandClass2->company = 'second';
$userLandClass2->myValue = 'my second value';

$serializer = new AttributeBasedJsonSerializer();
$json = $serializer->serialize($userLandClass2);

var_dump(json_decode($json, true));
40
Harshdeep
2 年前
虽然示例展示了我们可以用属性实现什么,但要记住,属性背后的主要思想是将静态元数据附加到代码(方法、属性等)。

这些元数据通常包含“标记”和“配置”等概念。例如,您可以使用反射编写一个序列化器,它只序列化标记的属性(可以选择配置,例如序列化文件中字段的名称)。这让人想起为 C# 应用程序编写的序列化器。

也就是说,完整的反射和属性是相辅相成的。如果您的用例可以通过继承或接口满足,那么优先使用它们。属性最常见的用例是当您对提供的对象/类没有任何先验信息时。

<?php
interface JsonSerializable
{
public function
toJson() : array;
}
?>

与使用属性相比:
<?php

#[Attribute]
class
JsonSerialize
{
public function
__constructor(public ?string $fieldName = null) {}
}

class
VersionedObject
{
#[
JsonSerialize]
public const
version = '0.0.1';
}

public class
UserLandClass extends VersionedObject
{
#[
JsonSerialize('call it Jackson')]
public
string $myValue;
}

?>
上面的示例有点过于复杂,因为存在 VersionedObject 类,因为我想展示的是,使用属性标记,您不需要关心基类如何管理其属性(在覆盖的方法中不需要调用父类)。
-107
Justin
2 年前
允许多个函数使用相同的属性进行标记,会促进奇怪的设计模式。因为现在类中标记的函数的顺序变得很重要。类中函数的顺序应该保持任意。

最好将函数标记限制为只使用一个属性。这会迫使人们为每个属性实现一个函数,然后这些函数可以调用它们原本会用这些属性标记的所有其他函数。
To Top