注解語法
#[Route] #[Route()] #[Route("/path", ["get"])] #[Route(path: "/path", methods: ["get"])]
其實語法跟實例化類非常相似,只是少了個 new
關鍵詞而已。
要注意的是, 注解名不能是變量,只能是常量或常量表達式
//實例化類 $route = new Route(path: "/path", methods: ["get"]);
(path: "/path", methods: ["get"])
是 php8
的新語法,在傳參的時候可以指定參數名,不按照形參的順序傳參。
注解類作用范圍
在定義注解類時,你可以使用內置注解類 #[Attribute]
定義注解類的作用范圍,也可以省略,由 PHP 動態地根據使用場景自動定義范圍。
注解作用范圍列表:
- Attribute::TARGET_CLASS
- Attribute::TARGET_FUNCTION
- Attribute::TARGET_METHOD
- Attribute::TARGET_PROPERTY
- Attribute::TARGET_CLASS_CONSTANT
- Attribute::TARGET_PARAMETER
- Attribute::TARGET_ALL
- Attribute::IS_REPEATABLE
在使用時,
#[Attribute]
等同于#[Attribute(Attribute::TARGET_ALL)]
,為了方便,一般使用前者。
1~7都很好理解,分別對應類、函數、類方法、類屬性、類常量、參數、所有,前6項可以使用 |
或運算符隨意組合,比如Attribute::TARGET_CLASS | Attribute::TARGET_FUNCTION
。(Attribute::TARGET_ALL
包含前6項,但并不包含 Attribute::IS_REPEATABLE
)。
Attribute::IS_REPEATABLE
設置該注解是否可以重復,比如:
class IndexController { #[Route('/index')] #[Route('/index_alias')] public function index() { echo "hello!world" . PHP_EOL; } }
如果沒有設置 Attribute::IS_REPEATABLE
,Route
不允許使用兩次。
上述提到的,如果沒有指定作用范圍,會由 PHP 動態地確定范圍,如何理解?舉例:
<?php class Deprecated { } class NewLogger { public function newLogAction(): void { //do something } #[Deprecated('oldLogAction已廢棄,請使用newLogAction代替')] public function oldLogAction(): void { } } #[Deprecated('OldLogger已廢棄,請使用NewLogger代替')] class OldLogger { }
上述的自定義注解類 Deprecated
并沒有使用內置注解類 #[Attribute]
定義作用范圍,因此當它修飾類 OldLogger
時,它的作用范圍被動態地定義為 TARGET_CLASS
。當它修飾方法 oldLogAction
時,它的作用范圍被動態地定義為 TARGET_METHOD
。一句話概括,就是修飾哪,它的作用范圍就在哪
需要注意的是, 在設置了作用范圍之后,在編譯階段,除了內置注解類 #[Attribute]
,自定義的注解類是不會自動檢查作用范圍的。除非你使用反射類 ReflectionAttribute
的 newInstance
方法。
舉例:
<?php #[Attribute] function foo() { }
這里會報錯 Fatal error: Attribute "Attribute" cannot target function (allowed targets: class)
,因為內置注解類的作用范圍是 TARGET_CLASS
,只能用于修飾類而不能是函數,因為內置注解類的作用范圍僅僅是 TARGET_CLASS
,所以也不能重復修飾。
而自定義的注解類,在編譯時是不會檢查作用范圍的。
<?php #[Attribute(Attribute::TARGET_CLASS)] class A1 { } #[A1] function foo() {}
這樣是不會報錯的。那定義作用范圍有什么意義呢?看一個綜合實例。
<?php #[Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_FUNCTION | Attribute::IS_REPEATABLE)] class Route { protected $handler; public function __construct( public string $path = '', public array $methods = [] ) {} public function setHandler($handler): self { $this->handler = $handler; return $this; } public function run() { call_user_func([new $this->handler->class, $this->handler->name]); } } class IndexController { #[Route(path: "/index_alias", methods: ["get"])] #[Route(path: "/index", methods: ["get"])] public function index(): void { echo "hello!world" . PHP_EOL; } #[Route("/test")] public function test(): void { echo "test" . PHP_EOL; } } class CLIRouter { protected static array $routes = []; public static function setRoutes(array $routes): void { self::$routes = $routes; } public static function match($path) { foreach (self::$routes as $route) { if ($route->path == $path) { return $route; } } die('404' . PHP_EOL); } } $controller = new ReflectionClass(IndexController::class); $methods = $controller->getMethods(ReflectionMethod::IS_PUBLIC); $routes = []; foreach ($methods as $method) { $attributes = $method->getAttributes(Route::class); foreach ($attributes as $attribute) { $routes[] = $attribute->newInstance()->setHandler($method); } } CLIRouter::setRoutes($routes); CLIRouter::match($argv[1])->run();
php test.php /index php test.php /index_alias php test.php /test
在使用 newInstance
時,定義的作用范圍才會生效,檢測注解類定義的作用范圍和實際修飾的范圍是否一致,其它場景并不檢測。
注解命名空間
<?php namespace { function dump_attributes($attributes) { $arr = []; foreach ($attributes as $attribute) { $arr[] = ['name' => $attribute->getName(), 'args' => $attribute->getArguments()]; } var_dump($arr); } } namespace DoctrineORMMapping { class Entity { } } namespace DoctrineORMAttributes { class Table { } } namespace Foo { use DoctrineORMMappingEntity; use DoctrineORMMapping as ORM; use DoctrineORMAttributes; #[Entity("imported class")] #[ORMEntity("imported namespace")] #[DoctrineORMMappingEntity("absolute from namespace")] #[Entity("import absolute from global")] #[AttributesTable()] function foo() { } } namespace { class Entity {} dump_attributes((new ReflectionFunction('Foofoo'))->getAttributes()); } //輸出: array(5) { [0]=> array(2) { ["name"]=> string(27) "DoctrineORMMappingEntity" ["args"]=> array(1) { [0]=> string(14) "imported class" } } [1]=> array(2) { ["name"]=> string(27) "DoctrineORMMappingEntity" ["args"]=> array(1) { [0]=> string(18) "imported namespace" } } [2]=> array(2) { ["name"]=> string(27) "DoctrineORMMappingEntity" ["args"]=> array(1) { [0]=> string(23) "absolute from namespace" } } [3]=> array(2) { ["name"]=> string(6) "Entity" ["args"]=> array(1) { [0]=> string(27) "import absolute from global" } } [4]=> array(2) { ["name"]=> string(29) "DoctrineORMAttributesTable" ["args"]=> array(0) { } } }
跟普通類的命名空間一致。
其它要注意的一些問題
- 不能在注解類參數列表中使用
unpack
語法。
<?php class IndexController { #[Route(...["/index", ["get"]])] public function index() { } }
雖然在詞法解析階段是通過的,但是在編譯階段會拋出錯誤。
- 在使用注解時可以換行
<?php class IndexController { #[Route( "/index", ["get"] )] public function index() { } }
- 注解可以成組使用
<?php class IndexController { #[Route( "/index", ["get"] ), Other, Another] public function index() { } }
- 注解的繼承
注解是可以繼承的,也可以覆蓋。
<?php class C1 { #[A1] public function foo() { } } class C2 extends C1 { public function foo() { } } class C3 extends C1 { #[A1] public function bar() { } } $ref = new ReflectionClass(C1::class); print_r(array_map(fn ($a) => $a->getName(), $ref->getMethod('foo')->getAttributes())); $ref = new ReflectionClass(C2::class); print_r(array_map(fn ($a) => $a->getName(), $ref->getMethod('foo')->getAttributes())); $ref = new ReflectionClass(C3::class); print_r(array_map(fn ($a) => $a->getName(), $ref->getMethod('foo')->getAttributes()));
C3
繼承了 C1
的 foo
方法,也繼承了 foo
的注解。而 C2
覆蓋了 C1
的 foo
方法,因此注解也就不存在了。
推薦學習:《PHP8教程》