什么是注解(Annotation)呢?
注解也叫元数据,用于对代码进行说明,可以对类、接口、字段、方法、参数等进行注解。注解是一种分散式的元数据,与源代码紧密绑定。
注解有什么用途呢?
- 生成文档,通过代码中标识的元数据生成文档,比如Java使用注解生成的javadoc文档。
- 编译检查,通过代码中标识的元数据让编译器在编译期间进行检查验证。
- 编译时动态处理,编译时通过代码中的元数据进行动态处理,比如生成动态代码。
- 运行时动态处理,运行时通过代码中的标识的元数据进行动态处理,例如使用反射注入实例。
注解与XML有什么异同点呢?
早期XML是各大框架的青睐者,因为它以松耦合的方式完成了框架中几乎所有的配置,但随着项目越来越大,XML的内容也越来越复杂,维护成本变的越来越高。
于是就有人提出一种标记式高耦合的配置方式“注解”,于是乎方法上可以进行注解,类上可以注解,字段上也可以注解,反正几乎需要配置的地方都可以进行注解。
关于注解和XML两种不同的配置模式,争论了很多年,各有优缺。注解提供了更大的便捷性,易于维护修改,但耦合度高。而XML相对于注解则正好相反。如果为了追求低耦合就需要抛弃高效率,追求高效率必然会遇到耦合。
注解本质上是什么呢?
The common interface extended by all annotation types
注解的本质其实就是继承了Annotation接口的接口,一个注解准确意义上来讲,只不过是一种特殊的注释而已,如果没有解析它的代码,它可能连注释都不如。
解析一个类或方法的注解往往有两种方式:
- 一种是编译期直接扫描
编译器在对代码编译成为字节码的过程中会检测某个类或方法是否被注解修饰,此时会对注解进行处理。 - 一种是运行期反射
PHP有注解吗?
注解是指附加在数据或代码上的元数据(metadata),框架可以基于元数据为代码提供各种额外的功能,在Swoft中注解是实现AOP、IoC容器的基础。
如果想象代码是具有生命的个体,那么注解就相当于为代码中某些鲜活个体贴上去一张标签,简单来说注解如同标签。
在编码层面上来看,注解和注释是一种平行的概念。注释提供对可执行代码的说明,单纯用于开发人员阅读,并不影响代码的执行。而注解往往充当着对代码的声明和配置的作用,它为可执行代码提供机器可用的额外信息,在特定的环境下会影响程序的执行。
PHP官方对注解的方案并没达成一致,目前PHP没有对注解的官方实现。主流的PHP框架使用的注解都是采用T_DOC_COMMENT
类型注释块中的@Tag
,来定义自己的注解机制。
Swoft中的注解是如何实现的呢?
Swoft没有重复造轮子,而是选择采用Doctrine
的注解引擎。Doctriine
的注解方案是基于T_DOC_COMMENT
型注释的,Doctrine
使用反射获取代码的T_DOC_COMMENT
型注释,并将注释中的特定类型@Tag
映射到对应注解类。
定义注解
因此,Swoft首先要为每个框架自定义的注解定义注解类。自定义注解时,方法注解和属性注解依赖于类注解。框架只主动扫描类注解,从类注解的Wrapper
类中获取应该扫描哪些方法注解和属性注解。
Swoft框架内置的注解类位于vendor/swoft/http-server/src/Bean/Annotation
文件夹下,比如在控制器中经常使用使用@Controller
注解的定义。
<?php
namespace Swoft\Http\Server\Bean\Annotation;
/**
*
* 控制器自动解析注解路由
*
* @Annotation //声明一个注解类
* @Target("CLASS") // 声明注解只能用在class类级别
*
* @uses Controller
* @version 2017年08月22日
* @author stelin <phpcrazy@126.com>
* @copyright Copyright 2010-2016 Swoft software
* @license PHP Version 7.x {@link http://www.php.net/license/3_0.txt}
*/
class Controller
{
/**
* @var string 控制器前缀
*/
private $prefix = '';
/**
* AutoController constructor.
*
* @param array $values
*/
public function __construct(array $values)
{
if (isset($values['value'])) {
$this->prefix = $values['value'];
}
if (isset($values['prefix'])) {
$this->prefix = $values['prefix'];
}
}
/**
* 获取controller前缀
*
* @return string
*/
public function getPrefix(): string
{
return $this->prefix;
}
}
-
@Annotation
用于声明当前类是一个注解类 -
@Target("CLASS")
用于声明当前类只能用于class
类级别的注解中 -
@var
是PHPDoc标准中的tag
,定义了属性的类型,Doctrine会根据类型额外对注解参数进行检查。 -
@param
Doctrine注解使用的参数
如果注解类提供了构造器__constructor
,Doctrine会调用并在此处对注解类对象的私有private
属性进行赋值。
自定义注解类
想要定义一个注解,首先需要新建注解类,例如:
$ vim app/Module/Test/Annotations/Test.php
<?php
namespace App\Module\Test\Annotations;
/**
* 注解
* @Annotation
* @Target("ALL")
* @package Module/Test/Annotations
*/
class Test
{
/**
* @var string $name
*/
private $name = "";
public function __construct(array $values)
{
if(isset($values["name"])){
$this->name = $values["name"];
}
}
/**
* @return string
*/
public function getName():string
{
return $this->name;
}
/**
* @param string $name
*/
public function setName(string $name)
{
$this->name = $name;
}
}
自定义注解类时需要注意的的是
- 类注解要添加
@Annotation
用于声明这是一个注解类 - 类名不需要添加
Annotation
后缀 - 类注解
@Target()
为Doctrine\Common\Annotations\Annotation\Target.php
,Target
函数的参数可选择ALL | CLASS | METHOD | PROPERTY | ANNOTATION
,表示注解使用的级别。
自定义注解类后就可以在控制器中使用自定义的注解标签, 例如在IndexController.php
控制器添加自定义标签。
use App\Module\Test\Annotations\Test;
/**
* Class IndexController
* @Controller()
* @Test(name='index')
*/
class IndexController
{}
注意这里的@Test(name='index')
相当于new Test([name=>"index"])
注解解析类Parser
注解只是配置的另种展现形式,任何逻辑都不要在注解类中处理。
<?php
namespace App\Module\Test\Parser;
use Swoft\Bean\Parser\AbstractParser;
use App\Module\Test\Collector\TestCollector;
class TestParser extends AbstractParser
{
public function parser(string $className, $objectAnnotation = null, string $propertyName = "", string $methodName = "", $propertyValua = null)
{
TestCollector::collect($className, $objectAnnotation, $propertyName, $methodName, $propertyName);
}
}
注意parser
方法参数的含义parser(string $className, $objectAnnotation = null, string $propertyName = "", string $methodName = "", $propertyValua = null)
-
string $className
当前注解所在的类名 -
$objectAnnotation
当前注解实例化的注解对象, -
$propertyName
当前注解所在的属性,如果是属性注解。 -
$methodName
当前注解所在的方法名,如果是方法注解。 -
$propertyValue
当前注解所在的属性,如果是属性注解。
注解解析类Parser
只需要做一件事情,就是把注解类存入到注解收集器类。因此这里也不要处理逻辑,因为此刻程序还处于初始化阶段,没有请求数据。
注解收集类Collector
- 注解收集类
Collector
只是存取$objectAnnotation
注解实例,方便后面使用。 - 注解收集类
Collector
被注解解析类Parser
调用
<?php
namespace App\Module\Test\Collector;
use Swoft\Bean\CollectorInterface;
class TestCollector implements CollectorInterface
{
private static $test = [];
public static function collect(string $className, $objectAnnotation=null, string $propertyName="", string $methodName="", $propertyValue=null)
{
self::$test[$className][$methodName] = $objectAnnotation;
}
public static function getCollector()
{
return self::$test;
}
}
注解封装类Wrapper
当类注解被实例化时会触发注解封装类{注解标签名}Wrapper
,注解封装类Wrapper
来决定是否触发注解解析类Parser
。
<?php
namespace App\Module\Test\Wrapper;
use App\Module\Test\Annotations\Test;
use Swoft\Bean\Wrapper\AbstractWrapper;
/*注解封装类*/
class TestWrapper extends AbstractWrapper
{
/**
* @var array 解析哪些类级注解
*/
protected $classAnnotations = [];
/**
* @var array 解析哪些属性级注解
*/
protected $propertyAnnotations = [];
/**
* @var array 解析哪些方法级注解
*/
protected $methodAnnotations = [Test::class];
/**
* 是否解析类注解
* @param array $annotations
* @return bool
*/
public function isParseClassAnnotations(array $annotations):bool
{
return false;
}
/**
* 是否解析属性注解
* @param array $annotations
* @return bool
*/
public function isParsePropertyAnnotations(array $annotations):bool
{
return false;
}
/**
* 是否解析方法注解
* @param array $annotations
* @return bool
*/
public function isParseMethodAnnotations(array $annotations):bool
{
return false;
}
}
加载注解
注解类加载器的注册是在框架的启动bootstrap
阶段进行的,Swoft会扫描所有PHP源码文件并获取和解析注解信息。
使用Doctrine首先需要提供一个类的自动加载方法,这里直接使用Swoft当前的类加载器。Swoft的类加载器是由Composer自动生成的,这意味着注解类只要符合PSR-4规范即可。
$ /vendor/swoft/framework/src/Bean/Resource/AnnotationResource.php
<?php
namespace Swoft\Bean\Resource;
use Doctrine\Common\Annotations\AnnotationReader;
use Doctrine\Common\Annotations\AnnotationRegistry;
use Swoft\Bean\Wrapper\WrapperInterface;
use Swoft\Helper\ComposerHelper;
/**
* Annotation resource
*/
abstract class AnnotationResource extends AbstractResource
{
/**
* 注册加载器和扫描PHP文件
*
* @return array
*/
protected function registerLoaderAndScanBean()
{
$phpClass = [];
foreach ($this->scanNamespaces as $namespace => $dir) {
AnnotationRegistry::registerLoader(function ($class) {
if (class_exists($class) || interface_exists($class)) {
return true;
}
return false;
});
$scanClass = $this->scanPhpFile($dir, $namespace);
$phpClass = array_merge($phpClass, $scanClass);
}
return array_unique($phpClass);
}
}
使用注解
使用Doctrine获取注解对象
解析注解
当扫描源码目录并获取PHP类后,Swoft会遍历类列表并加载类,获取类级别、方法级别、属性级别的所有注解对象。再将结果存放到AnnontationResource
类的$annotations
成员属性中。
$ /vendor/swoft/framework/src/Bean/Resource/AnnotationResource.php
<?php
namespace Swoft\Bean\Resource;
use Doctrine\Common\Annotations\AnnotationReader;
use Doctrine\Common\Annotations\AnnotationRegistry;
use Swoft\Bean\Wrapper\WrapperInterface;
use Swoft\Helper\ComposerHelper;
/**
* Annotation resource
*/
abstract class AnnotationResource extends AbstractResource
{
/**
* 解析bean注解
*
* @param string $className
*
* @return null
*/
public function parseBeanAnnotations(string $className)
{
if (!class_exists($className) && !interface_exists($className)) {
return null;
}
// 注解解析器
$reader = new AnnotationReader();
$reader = $this->addIgnoredNames($reader);
$reflectionClass = new \ReflectionClass($className);
$classAnnotations = $reader->getClassAnnotations($reflectionClass);
// 没有类注解不解析其它注解
if (empty($classAnnotations)) {
return;
}
foreach ($classAnnotations as $classAnnotation) {
$this->annotations[$className]['class'][get_class($classAnnotation)] = $classAnnotation;
}
// 解析属性
$properties = $reflectionClass->getProperties();
foreach ($properties as $property) {
if ($property->isStatic()) {
continue;
}
$propertyName = $property->getName();
$propertyAnnotations = $reader->getPropertyAnnotations($property);
foreach ($propertyAnnotations as $propertyAnnotation) {
$this->annotations[$className]['property'][$propertyName][get_class($propertyAnnotation)] = $propertyAnnotation;
}
}
// 解析方法
$publicMethods = $reflectionClass->getMethods(\ReflectionMethod::IS_PUBLIC);
foreach ($publicMethods as $method) {
if ($method->isStatic()) {
continue;
}
$methodName = $method->getName();
// 解析方法注解
$methodAnnotations = $reader->getMethodAnnotations($method);
foreach ($methodAnnotations as $methodAnnotation) {
$this->annotations[$className]['method'][$methodName][get_class($methodAnnotation)][] = $methodAnnotation;
}
}
}
}
Doctrine完成的功能仅仅是将注解映射到将要使用@Annotation
声明的注解类,Swoft需要自行处理注解对象并获取注解中的信息。
这一步有两个重要的功能:
- 扫描搜集Bean的所有信息包括Bean名称、类名、该Bean各个需要注入的属性信息等,存入
ObjectDefinition
数组中。
/**
* 类注解封装
*
* @param string $className
* @param array $annotation
* @param array $classAnnotations
*/
private function parseClassAnnotations(string $className, array $annotation, array $classAnnotations)
{
foreach ($classAnnotations as $classAnnotation) {
$annotationClassName = get_class($classAnnotation);
$classNameTmp = str_replace('\\', '/', $annotationClassName);
$classFileName = basename($classNameTmp);
$beanNamespaceTmp = dirname($classNameTmp, 2);
$beanNamespace = str_replace('/', '\\', $beanNamespaceTmp);
$annotationWrapperClassName = "{$beanNamespace}\\Wrapper\\{$classFileName}Wrapper";
if (!class_exists($annotationWrapperClassName)) {
continue;
}
/* @var WrapperInterface $wrapper */
$wrapper = new $annotationWrapperClassName($this);
// wrapper extend
foreach ($this->componentNamespaces as $componentNamespace) {
$annotationWrapperExtendClassName = "{$componentNamespace}\\Bean\\Wrapper\\Extend\\{$classFileName}Extend";
if (!class_exists($annotationWrapperExtendClassName)) {
continue;
}
$extend = new $annotationWrapperExtendClassName();
$wrapper->addExtends($extend);
}
$objectDefinitionAry = $wrapper->doWrapper($className, $annotation);
if ($objectDefinitionAry != null) {
list($beanName, $objectDefinition) = $objectDefinitionAry;
$this->definitions[$beanName] = $objectDefinition;
}
}
}
$objectDefinitionAry = $wrapper->doWrapper($className, $annotation);
- 在注解解析时
Parser
会调用相关的Collector
集合搜集所需的信息,比如进行事件注册。
由于Swoft框架执行前必须完整的获取各种注解到收集器Collector
并生成Bean
定义集合,所以Swoft是不进行懒加载lazyload
的。
使用注解
Swoft框架启动时会扫描PHP源码,将Bean
的定义信息存放到ObjectDefinition
数组中,将注解信息存放到各个Collector
中。因此在框架的启动Bootstrap
阶段,可以从BootstrapCollector
启动收集器中直接获得@Bootstrap
型的Bean,实例化并Bean执行。
PHP Annotations
PHP Annontations是PHPStorm支持的注解插件