2024 日本 PHP 研討會

屬性概述

(PHP 8)

屬性提供在程式碼宣告中新增結構化、機器可讀取的中繼資料資訊的功能:類別、方法、函式、參數、屬性和類別常數都可以成為屬性的目標。接著,可以使用 Reflection API 在執行階段檢查屬性定義的中繼資料。因此,可以將屬性視為直接嵌入程式碼的設定語言。

透過屬性 (Attribute),可以將功能的泛型實作與其在應用程式中的具體使用方式分離。在某種程度上,它類似於介面 (Interface) 及其各自的實作。但是介面和實作是關於程式碼的,而屬性則是關於註釋額外資訊和配置。介面可以由類別實作,而屬性則可以宣告在方法、函式、參數、屬性和類別常數上。因此,它們比介面更具彈性。

屬性使用的一個簡單範例是將具有可選方法的介面轉換為使用屬性。假設一個代表應用程式中操作的 `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);
新增註解

使用者貢獻的註解 2 則註解

42
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` 類別的存在而稍微複雜了一些,因為我想展示的是,使用屬性標記時,您不需要關心基底類別如何管理其屬性(在覆寫的方法中沒有呼叫父類別)。
37
Florian Krmer
2 年前
我嘗試了 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));
To Top