理解 PHP 8 中的 Attributes (註解)

NiZerin發表於2020-06-11

說明

從 PHP 8 開始,我們將能夠開始使用 ‘註解’。

這些註解(在許多其他語言中也稱為註釋)的目標是以結構化的方式將後設資料新增到類、方法、變數等中。

註解的概念並不新鮮,我們使用文件塊來模擬它們的行為已經有很多年了。

不過,通過新增註解,我們現在有了語言中的語法來表示這種後設資料,而不是必須手動解析文件塊。

  • 那麼它們看起來是什麼樣子呢?
  • 我們如何建立自定義註解?
  • 有什麼注意事項嗎?

這些都是本帖將要回答的問題。

分析

首先,下面是原生註解的樣子:

use \Support\Attributes\ListensTo;

class ProductSubscriber
{
    <<ListensTo(ProductCreated::class)>>
    public function onProductCreated(ProductCreated $event) { /* … */ }

    <<ListensTo(ProductDeleted::class)>>
    public function onProductDeleted(ProductDeleted $event) { /* … */ }
}

我將在這篇文章的後面展示其他示例,但我認為事件訂閱者的示例是一個很好的例子,可以首先解釋註解的用法。

另外,我知道,這種語法可能不是您想要或看到的。您可能首選 @、或 @:、或註釋或…。不過,這種語法已經被合併了,所以我們最好學會處理和使用它。關於語法,唯一值得一提的是討論了所有能夠實現註解的語法方案,選擇此語法有很好的理由。您可以在 RFC 中閱讀有關它的簡短摘要,也可以在內部列表中閱讀關於 RFC 的完整討論。

首先,自定義註解是簡單的類,用 <<attribute>> 屬性對自身進行註釋;這個基本屬性在最初的 RFC 中被稱為 PhpAttribute ,但後來在另一個 RFC 中進行了更改。

下面是它看起來的樣子:

<<Attribute>>
class ListensTo
{
    public string $event;

    public function __construct(string $event)
    {
        $this->event = $event;
    }
}

就是這樣,很簡單,對吧?記住註解的目標:它們的目的是將後設資料新增到類和方法中,僅此而已。
例如,它們不應該也不能用於引數輸入驗證。換句話說:您不能訪問傳遞給註解中的方法的引數。以前有一個 RFC 允許這種行為,但這個 RFC 特別讓事情變得更簡單。

回到事件訂閱者示例:我們仍然需要讀取後設資料,並在某個地方註冊我們的訂閱者。我有使用 Laravel 的背景,我會使用服務提供商來做這件事,但也可以自由地想出其他解決方案。

class EventServiceProvider extends ServiceProvider
{
    // 在現實場景中,
    // 我們會自動解析並快取所有訂閱者。
    // 而不是使用手動陣列。
    private array $subscribers = [
        ProductSubscriber::class,
    ];

    public function register(): void
    {
        // 事件排程器從容器中解析
        $eventDispatcher = $this->app->make(EventDispatcher::class);

        foreach ($this->subscribers as $subscriber) {
            // 我們將解析所有註冊的監聽器。
            // 在 `Subscriber` 類中,
            // 並將其新增到排程器。
            foreach (
                $this->resolveListeners($subscriber) 
                as [$event, $listener]
            ) {
                $eventDispatcher->listen($event, $listener);
            }       
        }       
    }
}

請注意,如果您不熟悉 [$event,$listener]語法,您可以在我關於陣列析構的帖子中快速瞭解它。

現在讓我們看一看 ResolutionveListeners,這就是魔術觸發的地方。

private function resolveListeners(string $subscriberClass): array
{
    $reflectionClass = new ReflectionClass($subscriberClass);

    $listeners = [];

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

        foreach ($attributes as $attribute) {
            $listener = $attribute->newInstance();

            $listeners[] = [
                // 在註解上配置的事件
                $listener->event,

                // 此事件的監聽器
                [$subscriberClass, $method->getName()],
            ];
        }
    }

    return $listeners;
}

您可以看到,與解析註釋字串相比,這樣更容易讀取後設資料。不過,有兩個錯綜複雜的問題值得研究。

首先是 $attribute->newInstance() 的呼叫。這實際上是我們的自定義屬性類被例項化的地方。它將接受訂閱伺服器類的屬性定義中列出的引數,並將它們傳遞給建構函式。

這意味著,從技術上講,您甚至不需要構造自定義註解。您可以直接呼叫 $attribute->getArguments()。此外,例項化類意味著您可以靈活地使用建構函式以任何您喜歡的方式進行解析輸入。總而言之,我想說總是使用newInstance()例項化屬性會很好。

值得一提的是ReflectionMethod::getAttributes()的使用,該函式返回方法的所有屬性。
您可以向其傳遞兩個引數,以過濾其輸出。

但是,為了理解這種過濾,您首先需要了解關於註解的另一件事。
這對您來說可能是顯而易見的,但我還是想快速地提一下:可以向同一個方法、類、屬性或常量新增幾個屬性。

<<Route(Http::POST, '/products/create')>>
<<Autowire>>
class ProductsCreateController
{
    public function __invoke() { /* … */ }
}

記住這一點,就很清楚為什麼Reflect*::getAttributes()返回一個陣列,那麼讓我們看看如何過濾它的輸出。

假設您正在解析控制器路由,您只對Route註解感興趣。您可以輕鬆地將該類作為篩選器傳遞:

$attributes = $reflectionClass->getAttributes(Route::class);

第二個引數更改過濾的方式。
您可以傳入ReflectionAttribute::IS_INSTANCEOF,它將返回實現給定介面的所有註解。

例如,假設您正在解析容器定義,這依賴於幾個屬性,您可以這樣做:

$attributes = $reflectionClass->getAttributes(
    ContainerAttribute::class, 
    ReflectionAttribute::IS_INSTANCEOF
);

技術理論

現在您已經瞭解了註解在實踐中是如何工作的,是時候進行更多的理論了,確保您徹底理解它們。
首先,我在前面簡單地提到了這一點,可以在幾個地方新增註解。

在類中,以及匿名類中;

<<ClassAttribute>>
class MyClass { /* … */ }

$object = new <<ObjectAttribute>> class () { /* … */ };

屬性和常量;

<<PropertyAttribute>>
public int $foo;

<<ConstAttribute>>
public const BAR = 1;

方法和功能;

<<MethodAttribute>>
public function doSomething(): void { /* … */ }

<<FunctionAttribute>>
function foo() { /* … */ }

以及閉包;

$closure = <<ClosureAttribute>> fn() => /* … */;

方法和功能的引數;

function foo(<<ArgumentAttribute>> $bar) { /* … */ }

它們可以在註釋之前或之後宣告;

/** @return void */
<<MethodAttribute>>
public function doSomething(): void { /* … */ }

並且可以接受無引數、一個引數或多個引數,這些引數由屬性的建構函式定義:

<<Listens(ProductCreatedEvent::class)>>
<<Autowire>>
<<Route(Http::POST, '/products/create')>>

至於可以傳遞給註解的允許引數,您已經看到允許使用類常量、::class和標量型別。不過,關於這一點還有更多要說的:註解只接受常量表示式作為輸入引數。

這意味著允許標量表示式-偶數位移位-以及::class、常量、陣列和陣列解包、布林表示式和 NULL 合併運算子。可以在原始碼中找到允許作為常量表示式的所有內容的列表。

<<AttributeWithScalarExpression(1+1)>>
<<AttributeWithClassNameAndConstants(PDO::class, PHP_VERSION_ID)>>
<<AttributeWithClassConstant(Http::POST)>>
<<AttributeWithBitShift(4 >> 1, 4 << 1)>>

註解配置

預設情況下,可以在多個位置新增註解,如上所列。
但是,可以對它們進行配置,使其只能在特定位置使用。
例如,您可以將其設定為ClassAttribute只能在類上使用,而不能在其他地方使用。
選擇加入此行為是通過將標誌傳遞給註解類上的註解屬性來完成的。

<<Attribute(Attribute::TARGET_CLASS)>>
class ClassAttribute
{
}

以下標誌可用:

Attribute::TARGET_CLASS
Attribute::TARGET_FUNCTION
Attribute::TARGET_METHOD
Attribute::TARGET_PROPERTY
Attribute::TARGET_CLASS_CONSTANT
Attribute::TARGET_PARAMETER
Attribute::TARGET_ALL

這些是位掩碼標誌,因此您可以使用二進位制或操作符將它們組合在一起。

<<Attribute(Attribute::TARGET_METHOD|Attribute::TARGET_FUNCTION)>>
class ClassAttribute
{
}

另一個配置標誌是關於重複性的。預設情況下,同一註解不能應用兩次,除非它特別標記為可重複。這與使用位標誌的目標配置相同。

<<Attribute(Attribute::IS_REPEATABLE)>>
class ClassAttribute
{
}

請注意,所有這些標誌只在呼叫$attribute->newInstance()時驗證,而不是更早。

內建註解

一旦基本RFC被接受,就出現了向核心新增內建註解的新機會。
<<deposated>>註解就是這樣的一個示例,而<<JIT>>註解就是一個很流行的示例-如果您不確定最後一個註解是關於什麼的,您可以閱讀我關於JIT是什麼的帖子。

我相信將來我們會看到越來越多的內建註解。

最後要注意的是,對於那些擔心泛型的人來說:語法不會與它們衝突,如果它們被新增到 PHP 中,那麼我們是安全的!

我已經想好了一些註解的用例,你呢?

本作品採用《CC 協議》,轉載必須註明作者和本文連結

By: Laravel-China 寧澤林
Blog: nizer.in

相關文章