[譯注] 原文中用了 Attribute 而不是常見的 Annotation 來表示注解特性,不知他們出於何種原因。Attribute 這個單詞我們在計算機領域可能更多的等同於 Property了,比如 xml 中元素的屬性。不過查了一下詞典,也許可以認為他們想表達“定語”。語法格式似乎學的C#。
概述
通過在代碼中做相應聲明,注解提供了為代碼添加結構化的機器可理解的元數據信息的能力,支持范圍:類、方法、函數、參數、屬性及類常量。后續在代碼運行時階段,這些定義的元數據可用Reflection API進行理解。因此,注解可被認為是一種直接嵌入代碼的配置語言。
類比於接口及其實現,注解也能讓應用內某個功能特性的通用實現與其具體運行時狀態進行解耦,不同的是接口用於代碼的解耦,注解則是注釋了代碼運行過程中實際所需的額外信息與配置。
注解的一種簡單用法是將將某個帶有某些可選方法的接口換成用注解實現。
實例 #1 使用注解實現可選方法接口的場景
<?php
interface ActionHandler
{
public function execute();
}
#[Attribute]
class SetUp {}
/**
* 必須實現 ActionHandler 的 execute 方法
*/
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()
{
mkdir($this->targetDirectory);
}
public function execute()
{
copy($this->fileName, $this->targetDirectory . '/' . basename($this->fileName));
}
}
<?php
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 注解語法
<?php
// a.php
namespace MyExample;
use Attribute;
#[Attribute]
class MyAttribute
{
const VALUE = 'value';
private $value;
public function __construct($value = null)
{
$this->value = $value;
}
}
<?php
// b.php
namespace Another;
use MyExample\MyAttribute;
#[MyAttribute]
#[\MyExample\MyAttribute]
#[MyAttribute(1234)]
#[MyAttribute(value: 1234)]
#[MyAttribute(MyAttribute::VALUE)]
#[MyAttribute(array("key" => "value"))]
#[MyAttribute(100 + 200)]
class Thing
{
}
#[MyAttribute(1234), MyAttribute(5678)]
class AnotherThing
{
}
聲明注解類
盡管從語法上並不嚴格要求,但還是建議為每個注解創建一個實際的類,以便被全局命名空間導入。最簡單的形式是聲明一個空類,並帶上 #[Attribute] 注解。
實例 #3 限制注解能使用的場景
<?php
namespace Example;
use Attribute;
#[Attribute]
class MyAttribute
{
}
可以限制注解被指定的類型。
實例 #4 簡單注解類
<?php
namespace Example;
use Attribute;
#[Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_FUNCTION)]
class MyAttribute
{
}
如果將上述 MyAttribute 聲明到另一種類型的場景,會導致在調用 ReflectionAttribute::newInstance() 方法是拋出異常。
默認情況下,一個注解處,同一個注解類只能被用一次。如果確實需要重復,可以聲明為允許重復。
實例 #5 使用 IS_REPEATABLE
<?php
namespace Example;
use Attribute;
#[Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_FUNCTION | Attribute::IS_REPEATABLE)]
class MyAttribute
{
}
通過Reflection API解析注解內容
Reflection API 提供了 getAttributes() 方法用於訪問注解,該方法返回由 ReflectionAttribute 實例構成的數組,可以通過這些實例查詢注解名、參數,並將注解所代表的類進行實例化。
調用 newInstance() 方法后,這些注解類才會被實例化為對象,因此可以在此之前進行參數的校驗,或進行相應錯誤處理。
實例 #6 用Reflection API解析注解
<?php
#[Attribute]
class MyAttribute
{
public $value;
public function __construct($value)
{
$this->value = $value;
}
}
#[MyAttribute(value: 1234)]
class Thing
{
}
function dumpAttributeData($reflection) {
$attributes = $reflection->getAttributes();
foreach ($attributes as $attribute) {
var_dump($attribute->getName());
var_dump($attribute->getArguments());
var_dump($attribute->newInstance());
}
}
dumpAttributeData(new ReflectionClass(Thing::class));
/*
string(11) "MyAttribute"
array(1) {
["value"]=>
int(1234)
}
object(MyAttribute)#3 (1) {
["value"]=>
int(1234)
}
*/
可以通過指定注解類來避免遍歷所有注解。
實例 #7 用Reflection API 解析特定注解
<?php
function dumpMyAttributeData($reflection) {
$attributes = $reflection->getAttributes(MyAttribute::class);
foreach ($attributes as $attribute) {
var_dump($attribute->getName());
var_dump($attribute->getArguments());
var_dump($attribute->newInstance());
}
}
dumpAttributeData(new ReflectionClass(Thing::class));