# 註解 註解是 Hyperf 非常強大的一項功能,可以透過註解的形式減少很多的配置,以及實現很多非常方便的功能。 ## 概念 ### 什麼是註解? 註解功能提供了程式碼中的宣告部分都可以新增結構化、機器可讀的元資料的能力, 註解的目標可以是類、方法、函式、引數、屬性、類常量。 透過 反射 API 可在執行時獲取註解所定義的元資料。 因此註解可以成為直接嵌入程式碼的配置式語言。 透過註解的使用,在應用中實現功能、使用功能可以相互解耦。 某種程度上講,它可以和介面(interface)與其實現(implementation)相比較。 但介面與實現是程式碼相關的,註解則與宣告額外資訊和配置相關。 介面可以透過類來實現,而註解也可以宣告到方法、函式、引數、屬性、類常量中。 因此它們比介面更靈活。 註解使用的一個簡單例子:將介面(interface)的可選方法改用註解實現。 我們假設介面 ActionHandler 代表了應用的一個操作: 部分 action handler 的實現需要 setup,部分不需要。 我們可以使用註解,而不用要求所有類必須實現 ActionHandler 介面並實現 setUp() 方法。 因此帶來一個好處——可以多次使用註解。 ### 註解是如何發揮作用的? 我們有說到註解只是元資料的定義,需配合應用程式才能發揮作用,在 Hyperf 裡,註解內的資料會被收集到 `Hyperf\Di\Annotation\AnnotationCollector` 類供應用程式使用,當然根據您的實際情況,也可以收集到您自定義的類去,隨後在這些註解本身希望發揮作用的地方對已收集的註解元資料進行讀取和利用,以達到期望的功能實現。 ### 忽略某些註解 在一些情況下我們可能希望忽略某些 註解,比如我們在接入一些自動生成文件的工具時,有不少工具都是透過註解的形式去定義文件的相關結構內容的,而這些註解可能並不符合 Hyperf 的使用方式,我們可以透過在 `config/autoload/annotations.php` 內將相關注解設定為忽略。 ```php use JetBrains\PhpStorm\ArrayShape; return [ 'scan' => [ // ignore_annotations 陣列內的註解都會被註解掃描器忽略 'ignore_annotations' => [ ArrayShape::class, ], ], ]; ``` ## 使用註解 註解一共有 3 種應用物件,分別是 `類`、`類方法` 和 `類屬性`。 ### 使用類註解 類註解定義是在 `class` 關鍵詞上方的註釋塊內,比如常用的 `Controller` 和 `AutoController` 就是類註解的使用典範,下面的程式碼示例則為一個正確使用類註解的示例,表明 `ClassAnnotation` 註解應用於 `Foo` 類。 ```php [ 'scan' => [ 'collectors' => [ CustomCollector::class, ], ], ], ]; ``` ### 利用註解資料 在沒有自定義註解收集方法時,預設會將註解的元資料統一收集在 `Hyperf\Di\Annotation\AnnotationCollector` 類內,透過該類的靜態方法可以方便的獲取對應的元資料用於邏輯判斷或實現。 ### ClassMap 功能 框架提供了 `class_map` 配置,可以方便使用者直接替換需要載入的類。 比如以下我們實現一個可以自動複製協程上下文的功能: 首先,我們實現一個用於複製上下文的 `Coroutine` 類。其中 `create()` 方法,可以將父類的上下文複製到子類當中。 為了避免命名衝突,約定使用 `class_map` 做為資料夾名,後跟要替換的名稱空間的資料夾及檔案。 如: `class_map/Hyperf/Utils/Coroutine.php` ```php container = $container; $this->logger = $container->get(StdoutLoggerInterface::class); if ($container->has(FormatterInterface::class)) { $this->formatter = $container->get(FormatterInterface::class); } } /** * @return int Returns the coroutine ID of the coroutine just created. * Returns -1 when coroutine create failed. */ public function create(callable $callable): int { $id = Utils\Coroutine::id(); $result = SwooleCoroutine::create(function () use ($callable, $id) { try { // 按需複製,禁止複製 Socket,不然會導致 Socket 跨協程呼叫從而報錯。 Context::copy($id, [ ServerRequestInterface::class, ]); call($callable); } catch (Throwable $throwable) { if ($this->formatter) { $this->logger->warning($this->formatter->format($throwable)); } else { $this->logger->warning((string) $throwable); } } }); return is_int($result) ? $result : -1; } } ``` 然後,我們實現一個跟 `Hyperf\Coroutine\Coroutine` 一模一樣的物件。其中 `create()` 方法替換成我們上述實現的方法。 `class_map/Hyperf/Coroutine/Coroutine.php` ```php get(Co::class)->create($callable); } public static function inCoroutine(): bool { return Coroutine::id() > 0; } } ``` 然後配置一下 `class_map`,如下: ```php [ 'paths' => [ BASE_PATH . '/app', ], 'ignore_annotations' => [ 'mixin', ], 'class_map' => [ // 需要對映的類名 => 類所在的檔案地址 Coroutine::class => BASE_PATH . '/class_map/Hyperf/Utils/Coroutine.php', ], ], ]; ``` 這樣 `co()` 和 `parallel()` 等方法,就可以自動拿到父協程,上下文中的資料,比如 `Request`。