资讯详情

资讯详情

Hyperf 注解完全指南:从基础概念到自定义注解与 ClassMap 类映射

后端Web框架微服务RPC框架异步编程【免费下载链接】hyperf A coroutine framework that focuses on hyperspeed and flexibility. Building microservice or middleware with ease.项目地址https://gitcode.com/hyperf/hyperf点击查看免费下载本篇技术指南围绕 Hyperf 协程框架的注解Annotation机制展开系统讲解注解在类、类方法、类属性上的使用方式以及ignore_annotations、自定义注解、注解收集器与class_map类映射等进阶能力。读完本文你将能够熟练运用 Hyperf 内置注解如Controller、RequestMapping、Inject、Value并具备从零编写自定义注解、按需接管注解收集逻辑、甚至无侵入替换框架核心类的能力。注解基础Hyperf 为什么如此依赖注解注解是 Hyperf 中非常强大的一项功能它允许以注解的形式大幅减少配置量并实现许多开箱即用的便利能力。整个框架的控制器路由、依赖注入、AOP 切面、RPC 服务注册等功能都建立在注解机制之上。什么是注解嵌入代码的声明式元数据注解功能为代码中的声明部分提供了添加结构化、机器可读元数据的能力。注解的目标可以是类、方法、函数、参数、属性以及类常量通过 PHP 的反射 API可以在运行时获取注解所定义的元数据。因此注解可以理解为一种直接嵌入代码的配置式语言。通过注解的使用应用中实现功能与使用功能得以相互解耦。某种程度上注解可以与接口interface和其实现implementation的关系相类比——但接口与实现是代码相关的注解则与声明额外信息和配置相关接口只能由类来实现而注解可以声明到方法、函数、参数、属性甚至类常量中因此注解比接口更加灵活。一个简单的例子可以说明这种灵活性假设接口ActionHandler代表应用中的一个操作其中部分 handler 实现需要setup部分不需要。如果要求所有类都实现ActionHandler接口并实现setUp()方法那么不需要 setup 的类也必须写出空实现而改用注解后只有真正需要 setup 的类才声明相应注解并且可以多次使用。注解是如何发挥作用的收集器 扫描器 消费方注解本身只是元数据定义必须配合应用程序才能发挥作用。在 Hyperf 中注解内的数据会被收集到Hyperf\Di\Annotation\AnnotationCollector类中供应用程序使用当然根据实际情况也可以收集到你自定义的类中随后在这些注解本身希望发挥作用的业务位置对已收集的注解元数据进行读取和利用最终实现期望的功能。从源码结构看注解的完整生命周期由三个核心角色组成相关实现位于 src/di/src/Annotation扫描器Scanner框架启动时Scanner::collect() 会对扫描路径内的每个类做反射解析分别遍历类的注解、属性注解、方法注解以及类常量注解并逐一触发对应注解对象的收集方法。也就是说除了文档中常说的类、类方法、类属性三类目标外源码层面还支持类常量上的注解。收集器CollectorAnnotationCollector是框架默认的元数据容器详见下文利用注解数据一节。消费方路由管理器、依赖注入容器、AOP 代理等组件在初始化时从收集器中读取元数据并转化为实际行为。忽略注解与第三方注解工具和平共处在某些场景下我们希望忽略某些注解。典型场景是接入自动生成文档的工具——不少此类工具都是通过注解的形式定义文档结构内容的而这些注解可能并不符合 Hyperf 的使用方式。此时可以在config/autoload/annotations.php中将相关注解设置为忽略use JetBrains\PhpStorm\ArrayShape; return [ scan [ // ignore_annotations 数组内的注解都会被注解扫描器忽略 ignore_annotations [ ArrayShape::class, ], ], ];该配置从源码层面可以得到印证ScanConfig在实例化时会读取config/autoload/annotations.php与各组件ConfigProvider中的annotations.scan.ignore_annotations配置见 src/di/src/Annotation/ScanConfig.php#L93-L96而 AnnotationReader::getAttributes() 在通过反射解析每个属性时会先判断注解类名是否位于忽略列表内若命中则直接continue跳过不进行实例化与收集。另外hyperf/di组件自身的ConfigProvider默认将mixin加入忽略列表见 src/di/src/ConfigProvider.php#L57-L59因此业务代码中即使出现mixin这类注解也不会干扰框架运行。注解的三种典型使用场景注解一共有 3 种常见应用对象分别是类、类方法和类属性。下面逐一介绍并对应到 Hyperf 内置注解的真实实现。类注解Controller 与 AutoController 的典范类注解定义在class关键词上方的注释块属性声明内。Hyperf 中常用的Controller和AutoController就是类注解的使用典范。下面的示例表明ClassAnnotation注解应用于Foo类?php #[ClassAnnotation] class Foo {}从源码看src/http-server/src/Annotation/Controller.php#L18-L23 中的Controller注解声明了prefix路由前缀、server所属服务默认http与options三个参数AutoController 在Controller基础上额外支持defaultMethods参数用于指定需要自动注册为路由的默认方法集合。类方法注解RequestMapping 路由映射的典范类方法注解定义在方法上方的注释块内。RequestMapping就是类方法注解的使用典范下面的示例表明MethodAnnotation注解应用于Foo::bar()方法?php class Foo { #[MethodAnnotation] public function bar() { // some code } }Hyperf 内置的 RequestMapping 通过#[Attribute(Attribute::TARGET_METHOD)]声明仅允许作用于方法构造函数接受path路由路径、methodsHTTP 方法默认[GET, POST]与options。源码中定义并导出了GET、POST、PUT、PATCH、DELETE、HEADER、OPTIONS七个方法常量且methods参数兼容字符串与数组两种写法——传入字符串时会被按逗号切分并自动转为大写如get, post会被规范化为[GET, POST]。类属性注解Value 与 Inject 依赖注入的典范类属性注解定义在属性上方的注释块内。Value和Inject就是类属性注解的使用典范下面的示例表明PropertyAnnotation注解应用于Foo类的$bar属性?php class Foo { #[PropertyAnnotation] private $bar; }其中Inject是依赖注入的核心注解见 src/di/src/Annotation/Inject.php通过#[Attribute(Attribute::TARGET_PROPERTY)]限定作用于属性构造参数包括value目标类缺省时自动从属性类型或 PHPDoc 推断、required是否必需默认true与lazy是否懒加载默认false开启时会将目标类名加上HyperfLazy\前缀交给懒加载代理处理。而Value注解见 src/config/src/Annotation/Value.php则用于将配置项直接注入到类属性中构造参数为配置键key由ValueAspect在运行时读取配置并写入属性。注解参数传递方式注解参数共支持以下几种传递形式传递主要的单个参数#[DemoAnnotation(value)]传递字符串参数#[DemoAnnotation(key1: value1, key2: value2)]传递数组参数#[DemoAnnotation(key: [value1, value2])]自定义注解从零编写一个注解类当内置注解无法满足业务需求时可以自定义注解。整体分为三个步骤创建注解类、按需实现收集逻辑、配置收集器以支持缓存。创建注解类继承 AbstractAnnotation?php namespace App\Annotation; use Attribute; use Hyperf\Di\Annotation\AbstractAnnotation; #[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD)] class Foo extends AbstractAnnotation { public function __construct(public array $bar, public int $baz 0) { } }使用注解类?php use App\Annotation\Foo; #[Foo(bar: [1, 2], baz: 3)] class IndexController extends AbstractController { // 利用注解数据 }注意#[Attribute(...)]中的目标限定Attribute::TARGET_CLASS、Attribute::TARGET_METHOD、Attribute::TARGET_PROPERTY等可以按位或组合约束该注解允许出现的位置。抽象类做了什么参数自动分配与自动收集在上面的示例中注解类继承了Hyperf\Di\Annotation\AbstractAnnotation抽象类。对于注解类来说这不是必须的——真正必须的是实现Hyperf\Di\Annotation\AnnotationInterface接口抽象类的作用在于提供极简的定义方式它已经替你实现了两项非常便捷的功能注解参数自动分配到类属性构造函数中使用public修饰的具名参数会自动成为注解对象的公开属性配合toArray()方法见 src/di/src/Annotation/AbstractAnnotation.php#L21-L29可将注解数据序列化为数组方便存储与传输。根据注解使用位置自动按规则收集到AnnotationCollectorcollectClass、collectClassConstant、collectMethod、collectProperty四个方法在抽象类中均已默认实现直接调用AnnotationCollector对应方法完成收集见 src/di/src/Annotation/AbstractAnnotation.php#L31-L49。因此大多数自定义注解只需像上面的Foo一样定义构造参数即可无需编写任何收集代码。自定义注解收集器实现 AnnotationInterface如果默认的收集规则不满足需求例如要把元数据收集到自己的容器中可以在注解类内重写收集逻辑。收集注解的具体执行流程由Hyperf\Di\Annotation\AnnotationInterface约束该接口要求实现以下方法见 src/di/src/Annotation/AnnotationInterface.phppublic function collectClass(string $className): void;—— 当注解定义在类上被扫描到时触发public function collectClassConstant(string $className, ?string $target): void;—— 当注解定义在类常量上被扫描到时触发public function collectMethod(string $className, ?string $target): void;—— 当注解定义在类方法上被扫描到时触发public function collectProperty(string $className, ?string $target): void;—— 当注解定义在类属性上被扫描到时触发其中?string $target参数表示注解所在的具体方法名、属性名或类常量名。也就是说虽然使用层面的注解目标常概括为类、类方法、类属性三类但接口层面对类常量同样提供了收集入口Scanner在扫描时也会遍历类的反射常量并调用collectClassConstant见 src/di/src/Annotation/Scanner.php#L67-L74。注册收集器以启用缓存因为框架实现了注解收集器缓存功能所以需要将自定义收集器配置到annotations.scan.collectors中框架才能自动缓存收集好的注解并在下次启动时复用。如果没有配置对应的收集器自定义注解只有在首次启动server时生效再次启动时不会生效。?php return [ // 注意在 config/autoload 文件下的配置文件则无 annotations 这一层 annotations [ scan [ collectors [ CustomCollector::class, ], ], ], ];从源码可以理解这背后的机制Scanner::scan()在扫描完成后会把每个收集器serialize()出的数据连同代理类映射一起写入runtime/container/scan.cache见 src/di/src/Annotation/Scanner.php#L122-L134下次启动时若扫描缓存可用config/config.php中scan_cacheable为true或app_env为prod见 ScanConfig.php#L123-L131则直接反序列化缓存数据并调用对应收集器的deserialize()恢复元数据。只有被列在collectors中的收集器才会参与缓存读写未注册的自定义收集器自然会在二次启动时丢失首次收集的数据。另外hyperf/di自身在ConfigProvider中默认注册了AnnotationCollector与AspectCollector两个收集器见 src/di/src/ConfigProvider.php#L52-L55这就是框架内置注解能够在多进程、多次启动场景下稳定生效的原因。读取注解元数据AnnotationCollector 静态 API在没有自定义注解收集方法时注解的元数据默认统一收集在Hyperf\Di\Annotation\AnnotationCollector类内。通过该类的静态方法可以方便地获取对应元数据用于逻辑判断或功能实现。其内部数据结构按类维度组织分别用_c类、_cc类常量、_p属性、_m方法四个键存放不同位置的注解见 src/di/src/Annotation/AnnotationCollector.php#L21-L39并提供以下常用读取 API方法作用getClassAnnotation(string $class, string $annotation)获取某个类上的指定注解实例getClassAnnotations(string $class)获取某个类上的全部注解getClassesByAnnotation(string $annotation)按注解反查所有使用它的类getClassMethodAnnotation(string $class, string $method)获取某方法上的全部注解getMethodsByAnnotation(string $annotation)按注解反查所有使用方法返回包含class、method、annotation的数组getClassPropertyAnnotation(string $class, string $property)获取某属性上的全部注解getPropertiesByAnnotation(string $annotation)按注解反查所有使用属性的类getClassConstantAnnotation(string $class, string $constant)获取某类常量上的全部注解getClassConstantsByAnnotation(string $annotation)按注解反查所有使用类常量的类getContainer()获取整个元数据容器这些 API 的正确性由单元测试直接验证在 src/di/tests/Annotation/ScannerTest.php#L102-L131 的testCollect用例中框架对一个同时标注了类注解、类常量注解、属性注解和方法注解的测试类见 src/di/tests/Stub/Collect/Foo.php执行Scanner::collect()后依次断言上述各类读取方法返回的注解实例与反查结果完全一致——这可以作为你编写自定义注解读取逻辑时的参考范式。ClassMap 功能无侵入替换框架类框架提供了class_map配置可以方便地直接替换需要加载的类实现不改动框架源码、不改动业务调用方的定制。class_map 的配置原理class_map位于annotations.scan配置下格式为需要映射的类名 类所在的文件地址。ScanConfig会将其读取为classMap见 src/di/src/Annotation/ScanConfig.php#L96而Scanner::collect()在收集某类注解前会先检查class_map如果原类已被动态替换即原类文件路径与映射地址不一致则跳过原类的收集避免新旧实现元数据冲突见 src/di/src/Annotation/Scanner.php#L38-L44。实战自动复制协程上下文下面以实现自动复制协程上下文为例演示class_map的完整用法。核心诉求是使用co()、parallel()等方法创建子协程时自动把父协程上下文如Request中的数据复制到子协程避免子协程中拿不到请求上下文。第一步实现一个用于复制上下文的Coroutine类。其中create()方法可以将父类的上下文复制到子类当中。为了避免命名冲突约定使用class_map作为文件夹名后跟要替换的命名空间对应的文件夹及文件例如class_map/Hyperf/Coroutine/Coroutine.php本示例摘自官方业务骨架示例项目?php declare(strict_types1); namespace App\Kernel\Context; use App\Kernel\Log\AppendRequestIdProcessor; use Hyperf\Context\Context; use Hyperf\Contract\StdoutLoggerInterface; use Hyperf\Engine\Coroutine as Co; use Psr\Container\ContainerInterface; use Psr\Http\Message\ServerRequestInterface; use Throwable; class Coroutine { protected LoggerInterface $logger; public function __construct(protected ContainerInterface $container) { $this-logger $container-get(StdoutLoggerInterface::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 Co::id(); $coroutine Co::create(function () use ($callable, $id) { try { // Shouldnt copy all contexts to avoid socket already been bound to another coroutine. Context::copy($id, [ AppendRequestIdProcessor::REQUEST_ID, ServerRequestInterface::class, ]); $callable(); } catch (Throwable $throwable) { $this-logger-warning((string) $throwable); } }); try { return $coroutine-getId(); } catch (Throwable $throwable) { $this-logger-warning((string) $throwable); return -1; } } }第二步实现一个与Hyperf\Coroutine\Coroutine一模一样的同名类。其中create()方法替换成上述实现其余静态方法保持原语义不变如id()、defer()、sleep()、parentId()、inCoroutine()、stats()、exists()文件位于class_map/Hyperf/Coroutine/Coroutine.php?php declare(strict_types1); namespace Hyperf\Coroutine; use App\Kernel\Context\Coroutine as Go; use Hyperf\Contract\StdoutLoggerInterface; use Hyperf\Engine\Coroutine as Co; use Hyperf\Engine\Exception\CoroutineDestroyedException; use Hyperf\Engine\Exception\RunningInNonCoroutineException; use Throwable; class Coroutine { /** * Returns the current coroutine ID. * Returns -1 when running in non-coroutine context. */ public static function id(): int { return Co::id(); } public static function defer(callable $callable): void { Co::defer(static function () use ($callable) { try { $callable(); } catch (Throwable $exception) { di()-get(StdoutLoggerInterface::class)-error((string) $exception); } }); } public static function sleep(float $seconds): void { usleep(intval($seconds * 1000 * 1000)); } /** * Returns the parent coroutine ID. * Returns 0 when running in the top level coroutine. * throws RunningInNonCoroutineException when running in non-coroutine context * throws CoroutineDestroyedException when the coroutine has been destroyed */ public static function parentId(?int $coroutineId null): int { return Co::pid($coroutineId); } /** * return int Returns the coroutine ID of the coroutine just created. * Returns -1 when coroutine create failed. */ public static function create(callable $callable): int { return di()-get(Go::class)-create($callable); } public static function inCoroutine(): bool { return Co::id() 0; } public static function stats(): array { return Co::stats(); } public static function exists(int $id): bool { return Co::exists($id); } }第三步配置class_map完成替换?php declare(strict_types1); use Hyperf\Coroutine\Coroutine; return [ scan [ paths [ BASE_PATH . /app, ], ignore_annotations [ mixin, ], class_map [ // 需要映射的类名 类所在的文件地址 Coroutine::class BASE_PATH . /class_map/Hyperf/Coroutine/Coroutine.php, ], ], ];配置完成后co()和parallel()等方法创建子协程时就会自动拿到父协程上下文中的数据例如Request。这与 Hyperf 协程组件的调用链完全吻合全局函数co()与go()最终都调用Coroutine::create()见 src/coroutine/src/Functions.php#L48-L63而parallel()则基于Parallel类批量创建协程见 src/coroutine/src/Functions.php#L24-L31它们都经过被替换的Hyperf\Coroutine\Coroutine静态入口上下文复制则依赖Hyperf\Context\Context的按协程 ID 读写机制见 src/context/src/Context.php。作为对比框架原生的 src/coroutine/src/Coroutine.php#L96-L105 已经提供了fork(callable, array $keys)方法实现带指定上下文键的协程创建class_map方案则适用于需要统一拦截create()入口、按全局规则复制上下文的场景。小结注解是 Hyperf 声明式编程的基石。掌握本文内容后你可以理解注解作为嵌入代码的配置式语言的本质以及扫描器 → 收集器 → 消费方的完整工作链路熟练使用ignore_annotations与第三方注解工具共存避免扫描器误解析在类、类方法、类属性以及类常量三个位置正确使用注解并理解Controller、RequestMapping、Inject、Value等内置注解的参数语义通过继承AbstractAnnotation快速创建自定义注解或实现AnnotationInterface定制收集逻辑并配置collectors使注解在扫描缓存开启后依然生效借助AnnotationCollector的静态 API 读取元数据驱动自己的路由、AOP 或业务逻辑使用class_map无侵入替换框架类实现如自动复制协程上下文等定制能力。如需进一步实践可深入阅读 src/di/src/Annotation 目录下的扫描器与收集器实现以及 src/di/tests/Annotation/ScannerTest.php 中完整的注解收集测试用例。赞分享后端Web框架微服务RPC框架异步编程【免费下载链接】hyperf A coroutine framework that focuses on hyperspeed and flexibility. Building microservice or middleware with ease.项目地址https://gitcode.com/hyperf/hyperf点击查看免费下载相关推荐Hyperf 注解Annotation完全指南从基础概念到自定义注解与 ClassMap 实战Hyperf 注解Annotation完全指南从基础概念到自定义注解与 ClassMap 实战 本文基于 Hyperf 官方文档《Annotation》编后端Web框架微服务RPC框架异步编程Hyperf 注解Annotation机制完全指南从底层原理到自定义注解与 ClassMap 实战Hyperf 注解Annotation机制完全指南从底层原理到自定义注解与 ClassMap 实战 Hyperf 是一个基于 Swoole 的高性能协程框后端微服务Hyperf 注解Annotation完整指南从元数据机制到自定义注解与 ClassMap 的实战解析Hyperf 注解Annotation完整指南从元数据机制到自定义注解与 ClassMap 的实战解析 注解是 Hyperf 框架中非常强大的一项功能它后端Web框架微服务RPC框架异步编程上一篇Sliver 仓库中的 Go 版 libphonenumber电话号码解析、格式化与区号提取全解下一篇Video2X深度解析现代AI视频超分辨率与帧插值框架的技术革命创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →