ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Doctrine Annotations 完全指南:基于 PHP Docblock 的注解解析与自定义注解实战

Doctrine Annotations 完全指南:基于 PHP Docblock 的注解解析与自定义注解实战 后端【免费下载链接】annotationsAnnotations Docblock Parser项目地址https://gitcode.com/gh_mirrors/an/annotations点击查看免费下载本文围绕 Doctrine 官方文档 docs/en/index.rst 及其延续章节 docs/en/annotations.rst 与 docs/en/custom.rst系统讲解doctrine/annotations组件的工作原理、安装方式、Reader 读取 API、缓存策略以及如何编写自定义注解类。读完本文你将能够在自己的 PHP 项目中安装并驱动该组件用语法在 docblock 中声明元数据通过 Reader 反射读取注解实例并掌握Target、Required、Enum、Attributes、NamedArgumentConstructor等全部注解类编写技巧。前置须知项目状态与弃用声明任何使用该组件的新读者都应当先了解一个关键事实PHP 8 引入了原生 attributes属性它们是注解的官方替代方案。官方文档在 docs/en/index.rst 开头即给出明确的 Deprecation notice本库被视为 feature complete功能已完备此后只接收 bugfix 与安全修复官方不建议在新项目中使用本库并鼓励下游库作者提供 attributes 作为 Doctrine Annotations 的替代方案迁移思路详见官方博客《annotations-to-attributes》。但这并不意味着该组件失去学习价值它是理解如何在无原生支持的语言特性上构建 DSL领域特定语法的经典范本也是大量存量项目尤其是 Doctrine ORM 旧版本配置仍在依赖的运行时组件。本文所有示例均以当前仓库 composer.json 的实际约束为准要求php: ^7.2 || ^8.0、ext-tokenizer、doctrine/lexer: ^2 || ^3、psr/cache: ^1 || ^2 || ^3。核心概念把 docblock 变成注解容器Doctrine Annotations 允许你为 PHP 类与函数实现自定义注解功能。由于注解并未内置于 PHP 语言本身该组件提供了一种方案将 PHP 的 docblock文档注释作为承载语法注解的容器。最简单的形态如下class Foo { /** * MyAnnotation(myPropertyvalue) */ private $bar; }其中MyAnnotation(myPropertyvalue)就是一条注解MyAnnotation是注解类名myPropertyvalue是传给它的参数。在 Doctrine 生态中注解被用于 ORM 的类映射配置如Entity、Column但该机制是通用组件任何项目都可以基于它构建自己的元数据约定。安装使用 Composer 安装即可$ composer require doctrine/annotations安装后组件以 PSR-4 方式自动加载命名空间前缀Doctrine\Common\Annotations\映射到 lib/Doctrine/Common/Annotations 目录见 composer.json 的autoload配置。创建第一个注解类注解类就是对后续在业务类中使用的那条注解配置的 PHP 类表示。前面示例中MyAnnotation(myPropertyvalue)对应的注解类长这样/** * Annotation */ final class MyAnnotation { public $myProperty; }关键点是注解类必须在其类级 docblock 中声明Annotation标记解析器才会把它识别为一个可实例化的注解类而不是普通的文档标签。Annotation本身属于内置保留注解在 ImplicitlyIgnoredAnnotationNames.php 的Reserved列表中可以看到Annotation、Attributes、Required、Target、NamedArgumentConstructor等都被预先声明为隐式忽略名称。读取注解Reader 与反射 API对注解的访问通过对包含注解的类或函数进行反射实现。所有读取器都实现Doctrine\Common\Annotations\Reader接口其中最常用的是AnnotationReaderuse Doctrine\Common\Annotations\AnnotationReader; $reflectionClass new ReflectionClass(Foo::class); $property $reflectionClass-getProperty(bar); $reader new AnnotationReader(); $myAnnotation $reader-getPropertyAnnotation( $property, MyAnnotation::class ); echo $myAnnotation-myProperty; // result: valueReader接口定义在 Reader.php包含 6 个核心方法getClassAnnotations/getClassAnnotation、getMethodAnnotations/getMethodAnnotation、getPropertyAnnotations/getPropertyAnnotationAnnotationReader在此基础上还实现了getFunctionAnnotations/getFunctionAnnotation对应 AnnotationReader.php 中的 8 个读取方法。IDE 支持部分 IDE 已内置对注解的支持可提供自动补全与高亮Eclipse通过 Symfony2 PluginPhpStorm通过 PHP Annotations Plugin 或 Symfony Plugin。深入docblock 中的各类注解是如何被处理的官方文档 docs/en/annotations.rst 用一个真实场景展示了注解处理的复杂性。假设有以下实体类namespace MyProject\Entities; use Doctrine\ORM\Mapping AS ORM; use Symfony\Component\Validator\Constraints AS Assert; /** * author Benjamin Eberlei * ORM\Entity * MyProject\Annotations\Foobarable */ class User { /** * ORM\Id ORM\Column ORM\GeneratedValue * dummy * var int */ private $id; /** * ORM\Column(typestring) * Assert\NotEmpty * Assert\Email * var string */ private $email; }一个 docblock 里同时混着四类注解处理方式完全不同文档注解如var、author被忽略永远不会因为错误使用注解而抛异常。这类注解名来自ImplicitlyIgnoredAnnotationNames::LISTImplicitlyIgnoredAnnotationNames.php其中除了var、author还包括param、return、throws、deprecated、inheritdoc、fixme等大量 phpDocumentor 1/2 时代的常见标签。通过 use 语句导入的注解use Doctrine\ORM\Mapping AS ORM使该命名空间下所有类都可以写作ORM\ClassNameAssert同理。注意别名大小写不敏感Assert与assert效果一致。无法识别的注解dummy它既不是文档注解也不在忽略列表内解析器无法确定如何处理。默认配置下解析该注解会抛出异常unknown annotation。完全限定的注解MyProject\Annotations\Foobarable直接转换为对应的类名并实例化。为什么需要全局注册表静默自动加载问题从代码直觉看ORM\Entity、Assert\Email以及完全限定注解似乎可以直接通过 PHP 自动加载器加载。但官方文档明确指出事实并非如此。出于错误处理的需要AnnotationReader内部所有类存在性检查都会把class_exists($name, $autoload)的第二个参数$autoload设为false即不触发自动加载。要正常工作需要静默自动加载器而很多自动加载器并不满足这一要求——静默自动加载并不属于 PSR-0 规范。因此 Doctrine Annotations 通过一个全局注册表实现自己的自动加载机制AnnotationRegistry::loadAnnotationClass()见 AnnotationRegistry.php。文档解释了这个架构选择注解类的自动加载问题没有其他直截了当的全局解决方案而且 PHP 自动加载本身就是全局的注册表全局化是合理取舍。让示例跑通忽略未知注解要让上面的User类正常工作需要如下配置use Doctrine\Common\Annotations\AnnotationReader; $reader new AnnotationReader(); AnnotationReader::addGlobalIgnoredName(dummy);创建AnnotationReader实例后把dummy加入全局忽略注解列表否则解析MyProject\Entities\User#id的 docblock 时dummy会触发异常。该静态方法在 AnnotationReader.php 中实现只是向$globalIgnoredNames数组追加一个键与之配套的还有addGlobalIgnoredNamespace()AnnotationReader.php用于按命名空间前缀批量忽略。设置与配置三种 Reader 组合使用基础用法AnnotationReader$reader new \Doctrine\Common\Annotations\AnnotationReader();这会创建一个除内存PHP 数组外无任何缓存的简单读取器。由于解析 docblock 是相对昂贵的操作应当用缓存读取器包装它。另外需要注意 AnnotationReader.php 构造函数中的一项环境检查若 PHP 环境加载了 Zend Optimizer 或 Zend OPcache 且opcache.save_comments或zend_optimizerplus.save_comments被关闭构造会直接抛出AnnotationException::optimizerPlusSaveComments()——因为 docblock 注释被 OPcache 剥离后注解将完全不可读这个异常是在保护你免受静默失败。缓存读取器PsrCachedReader要缓存注解可以创建Doctrine\Common\Annotations\PsrCachedReader。它装饰原读取器并把所有注解结果存入 PSR-6 缓存池use Doctrine\Common\Annotations\AnnotationReader; use Doctrine\Common\Annotations\PsrCachedReader; $cache ... // instantiate a PSR-6 Cache pool $reader new PsrCachedReader( new AnnotationReader(), $cache, $debug true );debug标志用于开发期为true时每次读取都会通过文件修改时间filemtime校验缓存是否过期若被注解的 PHP 类文件发生变化则自动刷新缓存。从源码看PsrCachedReaderPsrCachedReader.php的缓存键设计是类注解用ClassName、属性注解用ClassName$property、方法注解用ClassName#method均经rawurlencode编码并在debug模式下额外存储[C]前缀的时间戳键用于新鲜度比对其getLastModification()还会递归检查父类、接口和 trait 的修改时间保证继承链上的任何改动都会触发缓存失效。文档特别给出缓存模式下的警告AnnotationReader假设一个 docblock 中的所有注解都在同一次处理中完成。这意味着如果某些注解类在首次请求时不存在、未加载且无法通过AnnotationRegistry自动加载那么在启用缓存后它们将永远不可见、不可访问——除非清空缓存并重新请求这一次所有注解类都已定义齐全。索引读取器IndexedReader默认情况下读取器返回数字索引的注解数组。若希望注解以类名作为键索引用IndexedReader包装即可use Doctrine\Common\Annotations\AnnotationReader; use Doctrine\Common\Annotations\IndexedReader; $reader new IndexedReader(new AnnotationReader());从实现看IndexedReader.php 只是把委托读取器返回的数组按get_class($annot)重新建键。文档对组合顺序给出明确警告绝不要把 IndexedReader 包在缓存读取器里面只能反过来即PsrCachedReader(IndexedReader(...))之外的正确组合是IndexedReader(PsrCachedReader(...))。这样你就能用同一份缓存复用数字索引或类名索引的结果否则会因缓存内容格式数字 vs 索引不一致而出现故障。忽略缺失注解异常默认情况下AnnotationReader在遇到以下情况的注解时会抛出异常不属于文档注解忽略列表未通过 use 语句导入不是真实存在的完全限定类。如果项目中的 docblock 并不严格遵循这些要求可以对特定名称关闭该行为$reader new \Doctrine\Common\Annotations\AnnotationReader(); AnnotationReader::addGlobalIgnoredName(foo);PHP Imports 机制默认情况下读取器会解析 PHP 文件的 use 语句以获得导入规则并注册到注解处理流程中对应 PhpParser.php 的parseUseStatements()它基于TokenParser逐行扫描源码甚至兼容反射方法提供getUseStatements()的运行时捷径。文档强调只有启用 PHP Imports才能校验注解用法的正确性并在注解拼写错误时抛出异常该机制默认开启。为便于升级迁移仍可显式关闭但文档注明未来版本将移除该开关$reader new \Doctrine\Common\Annotations\AnnotationReader(); $reader-setEnabledPhpImports(false);自定义注解类进阶官方文档 docs/en/custom.rst 是自定义注解的完整教程。若要定义自己的注解只需把注解类归组到一个命名空间下并在类级 docblock中写上Annotationnamespace MyCompany\Annotations; /** Annotation */ class Bar { // some code }值注入构造函数 vs 公有属性解析器会检查注解类构造函数是否有参数有参数把注解参数数组整体传给构造函数无参数直接把值注入公有属性。两种写法示例namespace MyCompany\Annotations; /** * Annotation * * Some Annotation using a constructor */ class Bar { private $foo; public function __construct(array $values) { $this-foo $values[foo]; } } /** * Annotation * * Some Annotation without a constructor */ class Foo { public $bar; }若注解类既不写构造函数也没有对应公有属性访问不存在的属性会触发基类 Annotation.php 中__get/__set抛出的BadMethodCallException提示 Unknown property ... on annotation ...而继承该基类的注解类其公有$value属性与接受array $data的构造函数也提供了默认的键值注入能力。命名参数构造函数向 PHP 8 Attributes 对齐自 Annotations v1.11 起提供新的实例化策略目标是让注解类与 PHP 8 attribute 特性兼容。你需要声明一个参数名与注解语法中命名参数一致的构造函数然后二选一启用使用NamedArgumentConstructor标签v1.12 起可用实现Doctrine\Common\Annotations\NamedArgumentConstructorAnnotation接口v1.11 起可用v1.12 起弃用。使用NamedArgumentConstructor时构造函数的第一个参数被视为默认参数从而同时支持命名传参和位置传参两种写法namespace MyCompany\Annotations; /** * Annotation * NamedArgumentConstructor */ class Bar implements NamedArgumentConstructorAnnotation { private $foo; public function __construct(string $foo) { $this-foo $foo; } } /** Usable with Bar(foobaz) */ /** Usable with Bar(baz) */结合 PHP 8 的构造器属性提升constructor property promotion可以进一步简化为namespace MyCompany\Annotations; /** * Annotation * NamedArgumentConstructor */ class Bar implements NamedArgumentConstructorAnnotation { public function __construct(private string $foo) {} }使用接口方式v1.11 时代写法v1.12 起弃用的等价实现namespace MyCompany\Annotations; use Doctrine\Common\Annotations\NamedArgumentConstructorAnnotation; /** Annotation */ class Bar implements NamedArgumentConstructorAnnotation { private $foo; public function __construct(private string $foo) {} } /** Usable with Bar(foobaz) */限制注解作用域TargetTarget指明注解类型适用于哪些类元素可定义一个或多个目标CLASS—— 允许用于类 docblockPROPERTY—— 允许用于属性 docblockMETHOD—— 允许用于方法 docblockFUNCTION—— 允许用于函数 docblockALL—— 允许用于类、属性、方法、函数 docblockANNOTATION—— 允许嵌套在其他注解内部使用。如果注解出现在不允许的上下文中会抛出AnnotationException。示例namespace MyCompany\Annotations; /** * Annotation * Target({METHOD,PROPERTY}) */ class Bar { // some code } /** * Annotation * Target(CLASS) */ class Foo { // some code }属性类型校验var 与 Attributes/Attribute解析器会依据注解属性上的 phpdocvar注解校验传入参数的数据类型也可以改用AttributesAttribute声明。类型不匹配时会抛出AnnotationException。基于var的完整类型清单对应文档示例均为Target({METHOD,PROPERTY})的注解类属性/** var mixed */ public $mixed; /** var boolean */ public $boolean; /** var bool */ public $bool; /** var float */ public $float; /** var string */ public $string; /** var integer */ public $integer; /** var array */ public $array; /** var SomeAnnotationClass */ public $annotation; /** var arrayinteger */ public $arrayOfIntegers; /** var arraySomeAnnotationClass */ public $arrayOfAnnotations;基于Attributes/Attribute的声明方式注意这里Attribute的属性必须传type/** * Annotation * Target({METHOD,PROPERTY}) * Attributes({ * Attribute(stringProperty, type string), * Attribute(annotProperty, type SomeAnnotationClass), * }) */ class Foo { public function __construct(array $values) { $this-stringProperty $values[stringProperty]; $this-annotProperty $values[annotProperty]; } // some code }必填字段RequiredRequired标记的字段在注解被使用时必须指定否则抛出AnnotationException提示该值不能为 null。声明必填字段/** * Annotation * Target(ALL) */ class Foo { /** Required */ public $requiredField; }使用对比/** Foo(requiredFieldvalue) */ public $direction; // Valid /** Foo */ public $direction; // Required field missing, throws an AnnotationException枚举值EnumEnum标记的注解属性是只接受一组固定标量值的字段。需要表示固定取值集合时应当使用Enum解析器会校验给定值不匹配即抛出AnnotationException。声明枚举属性/** * Annotation * Target(ALL) */ class Direction { /** * Enum({NORTH, SOUTH, EAST, WEST}) */ public $value; }使用对比/** Direction(NORTH) */ public $direction; // Valid value /** Direction(NORTHEAST) */ public $direction; // Invalid value, throws an AnnotationException常量支持注解解析器支持使用全局常量与类常量以下用法均被允许namespace MyCompany\Entity; use MyCompany\Annotations\Foo; use MyCompany\Annotations\Bar; use MyCompany\Entity\SomeClass; /** * Foo(PHP_EOL) * Bar(Bar::FOO) * Foo({SomeClass::FOO, SomeClass::BAR}) * Bar({SomeClass::FOO_KEY SomeClass::BAR_VALUE}) */ class User { }文档特别提醒常量与缓存的配合问题缓存读取器每次从缓存加载注解时不会重新求值常量。当常量被修改后必须清理缓存。实际使用读取自定义注解与完整 Reader API把前面定义的注解应用到业务类上namespace MyCompany\Entity; use MyCompany\Annotations\Foo; use MyCompany\Annotations\Bar; /** * Foo(barfoo) * Bar(foobar) */ class User { }随后通过反射读取这些注解$reflClass new ReflectionClass(MyCompany\Entity\User); $classAnnotations $reader-getClassAnnotations($reflClass); foreach ($classAnnotations AS $annot) { if ($annot instanceof \MyCompany\Annotations\Foo) { echo $annot-bar; // prints foo; } else if ($annot instanceof \MyCompany\Annotations\Bar) { echo $annot-foo; // prints bar; } }Reader提供了一整套从类、属性、方法、函数 docblock 中取回注解类实例的 API完整清单如下目标读取全部注解读取单个注解类getClassAnnotations(\ReflectionClass $class)getClassAnnotation(\ReflectionClass $class, $annotationName)方法getMethodAnnotations(\ReflectionMethod $method)getMethodAnnotation(\ReflectionMethod $method, $annotationName)属性getPropertyAnnotations(\ReflectionProperty $property)getPropertyAnnotation(\ReflectionProperty $property, $annotationName)函数getFunctionAnnotations(\ReflectionFunction $function)getFunctionAnnotation(\ReflectionFunction $function, $annotationName)单注解方法如getClassAnnotation在命中目标注解时返回其实例未命中返回null实现上都是先取全部注解再按instanceof匹配对应 AnnotationReader.php 与 Reader.php 的接口约定。函数级 API 只存在于AnnotationReader具体类中未在Reader接口中声明——如果你需要函数注解注意传入的类型是ReflectionFunction。小结与迁移建议综上doctrine/annotations是一套完整、严谨的 docblock 注解解析方案AnnotationReader负责反射解析PsrCachedReader负责以 PSR-6 缓存加速IndexedReader提供按类名索引的结果组织自定义注解通过Annotation、Target、Required、Enum、Attributes/Attribute、NamedArgumentConstructor等内置标记获得完整的声明式能力。对新建项目官方立场明确优先使用 PHP 8 原生 attributes 而非本库对存量项目本文列出的 Reader 组合、缓存键设计、常量缓存失效规则、OPcache 注释保留要求是排查注解读不到缓存不刷新类问题的关键知识。仓库内的 tests/Doctrine/Tests/Common/Annotations/AnnotationReaderTest.php、DocParserTest.php 以及 Fixtures 目录下的注解夹具类如Route、Secure、AnnotationWithRequiredAttributes等覆盖了本文涉及的绝大多数行为可作为进一步研读源码与验证行为的入口。赞分享后端【免费下载链接】annotationsAnnotations Docblock Parser项目地址https://gitcode.com/gh_mirrors/an/annotations点击查看免费下载相关推荐使用 prisma deploy 同步服务定义与数据模型Flags 详解与源码级部署流程解析使用 prisma deploy 同步服务定义与数据模型Flags 详解与源码级部署流程解析 prisma deploy 是 Prisma CLI 中负责将本后端3层排查法搞定 IsaacLab 远程可视化黑屏从连不上到 60fps 稳定推流的完整实战3层排查法搞定 IsaacLab 远程可视化黑屏从连不上到 60fps 稳定推流的完整实战 IsaacLab 远程可视化LIVESTREAM 推流最常见的人工智能强化学习机器人具身智能深度学习深入解析 GelEdgeDBSchema 注解Annotations标准注解、自定义注解与 DDL 全指南深入解析 GelEdgeDBSchema 注解Annotations标准注解、自定义注解与 DDL 全指南 Schema 注解Annotations数据库图数据库关系型数据库上一篇如何快速掌握TensorFlow Probability从自动微分到分布式计算的完整指南下一篇PushSharp社区贡献终极指南如何参与开源推送库开发与维护创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表