注解语法
() | |
"/path", ["get"]) | (|
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 | |
{ | |
'/index') | (|
'/index_alias') | (|
public function index() | |
{ | |
echo "hello!world" . PHP_EOL; | |
} | |
} |
如果没有设置 Attribute::IS_REPEATABLE
,Route
不允许使用两次。
上述提到的,如果没有指定作用范围,会由 PHP 动态地确定范围,如何理解?举例:
class Deprecated | |
{ | |
} | |
class NewLogger | |
{ | |
public function newLogAction(): void | |
{ | |
//do something | |
} | |
public function oldLogAction(): void | |
{ | |
} | |
} | |
class OldLogger | |
{ | |
} |
上述的自定义注解类 Deprecated
并没有使用内置注解类 #[Attribute]
定义作用范围,因此当它修饰类 OldLogger
时,它的作用范围被动态地定义为 TARGET_CLASS
。当它修饰方法 oldLogAction
时,它的作用范围被动态地定义为TARGET_METHOD
。一句话概括,就是修饰哪,它的作用范围就在哪
需要注意的是, 在设置了作用范围之后,在编译阶段,除了内置注解类 #[Attribute]
,自定义的注解类是不会自动检查作用范围的。除非你使用反射类 ReflectionAttribute
的 newInstance
方法。
举例:
function foo() | |
{ | |
} |
这里会报错 Fatal error: Attribute "Attribute" cannot target function (allowed targets: class)
,因为内置注解类的作用范围是TARGET_CLASS
,只能用于修饰类而不能是函数,因为内置注解类的作用范围仅仅是TARGET_CLASS
,所以也不能重复修饰。
而自定义的注解类,在编译时是不会检查作用范围的。
Attribute::TARGET_CLASS) | (|
class A1 | |
{ | |
} | |
function foo() {} |
这样是不会报错的。那定义作用范围有什么意义呢?看一个综合实例。
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 | |
{ | |
path: "/index_alias", methods: ["get"]) | (|
path: "/index", methods: ["get"]) | (|
public function index(): void | |
{ | |
echo "hello!world" . PHP_EOL; | |
} | |
"/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
时,定义的作用范围才会生效,检测注解类定义的作用范围和实际修饰的范围是否一致,其它场景并不检测。
注解命名空间
namespace { | |
function dump_attributes($attributes) { | |
$arr = []; | |
foreach ($attributes as $attribute) { | |
$arr[] = ['name' => $attribute->getName(), 'args' => $attribute->getArguments()]; | |
} | |
var_dump($arr); | |
} | |
} | |
namespace Doctrine\ORM\Mapping { | |
class Entity { | |
} | |
} | |
namespace Doctrine\ORM\Attributes { | |
class Table { | |
} | |
} | |
namespace Foo { | |
use Doctrine\ORM\Mapping\Entity; | |
use Doctrine\ORM\Mapping as ORM; | |
use Doctrine\ORM\Attributes; | |
#[Entity("imported class")] | |
#[ORM\Entity("imported namespace")] | |
#[\Doctrine\ORM\Mapping\Entity("absolute from namespace")] | |
#[\Entity("import absolute from global")] | |
#[Attributes\Table()] | |
function foo() { | |
} | |
} | |
namespace { | |
class Entity {} | |
dump_attributes((new ReflectionFunction('Foo\foo'))->getAttributes()); | |
} | |
//输出: | |
array(5) { | |
[0]=> | |
array(2) { | |
["name"]=> | |
string(27) "Doctrine\ORM\Mapping\Entity" | |
["args"]=> | |
array(1) { | |
[0]=> | |
string(14) "imported class" | |
} | |
} | |
[1]=> | |
array(2) { | |
["name"]=> | |
string(27) "Doctrine\ORM\Mapping\Entity" | |
["args"]=> | |
array(1) { | |
[0]=> | |
string(18) "imported namespace" | |
} | |
} | |
[2]=> | |
array(2) { | |
["name"]=> | |
string(27) "Doctrine\ORM\Mapping\Entity" | |
["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) "Doctrine\ORM\Attributes\Table" | |
["args"]=> | |
array(0) { | |
} | |
} | |
} |
跟普通类的命名空间一致。
其它要注意的一些问题
不能在注解类参数列表中使用 unpack
语法。
class IndexController | |
{ | |
"/index", ["get"]]) | (...[|
public function index() | |
{ | |
} | |
} |
虽然在词法解析阶段是通过的,但是在编译阶段会抛出错误。
在使用注解时可以换行
class IndexController | |
{ | |
( | |
"/index", | |
["get"] | |
) | |
public function index() | |
{ | |
} | |
} |
注解可以成组使用
class IndexController | |
{ | |
( | |
"/index", | |
["get"] | |
), | ,|
public function index() | |
{ | |
} | |
} |
注解的继承
注解是可以继承的,也可以覆盖。
class C1 | |
{ | |
public function foo() { } | |
} | |
class C2 extends C1 | |
{ | |
public function foo() { } | |
} | |
class C3 extends C1 | |
{ | |
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 方法,因此注解也就不存在了。
本文转自: