[精选] PHP8新特性:注解,你也该掌握了

注解语法

#[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_REPEATABLERoute不允许使用两次。

上述提到的,如果没有指定作用范围,会由 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],自定义的注解类是不会自动检查作用范围的。除非你使用反射类 ReflectionAttributenewInstance 方法。

举例:

#[Attribute]
function foo()
{
}

这里会报错 Fatal error: Attribute "Attribute" cannot target function (allowed targets: class),因为内置注解类的作用范围是TARGET_CLASS,只能用于修饰类而不能是函数,因为内置注解类的作用范围仅仅是TARGET_CLASS,所以也不能重复修饰。

而自定义的注解类,在编译时是不会检查作用范围的。

#[Attribute(Attribute::TARGET_CLASS)]
class A1
{
}

#[A1]
function foo() {}

这样是不会报错的。那定义作用范围有什么意义呢?看一个综合实例。

#[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 时,定义的作用范围才会生效,检测注解类定义的作用范围和实际修饰的范围是否一致,其它场景并不检测。

注解命名空间

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

{

    #[Route(...["/index", ["get"]])]

    public function index()

    {

    }

}

虽然在词法解析阶段是通过的,但是在编译阶段会抛出错误。

在使用注解时可以换行

class IndexController

{

    #[Route(

        "/index",

        ["get"]

    )]

    public function index()

    {

    }

}

注解可以成组使用

class IndexController

{

    #[Route(

        "/index",

        ["get"]

    ), Other, Another]

    public function index()

    {

    }

}

注解的继承
注解是可以继承的,也可以覆盖。

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 方法,因此注解也就不存在了。
本文转自:mp.weixin.qq.com/s/rYn6268joaCSXA8...

php
本作品采用《CC 协议》,转载必须注明作者和本文链接
最美的不是下雨天,而是和你一起躲过的屋檐!
《L03 构架 API 服务器》
你将学到如 RESTFul 设计风格、PostMan 的使用、OAuth 流程,JWT 概念及使用 和 API 开发相关的进阶知识。
《G01 Go 实战入门》
从零开始带你一步步开发一个 Go 博客项目,让你在最短的时间内学会使用 Go 进行编码。项目结构很大程度上参考了 Laravel。
讨论数量: 3

路由写到业务逻辑代码里面,就失去了路由的意义。

1年前 评论

其实我不太能理解php8注解的作用,又不能像Java那样通过反射来附加逻辑,除了限制外看不出有其他作用

1年前 评论
deatil 1年前

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!