注解语法
#[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 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
语法。
<?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
方法,因此注解也就不存在了。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。