[原创]Swoft源码剖析-Swoft中的注解机制

PHP中的注解

注解(Annotations)是Swoft里面很多重要功能特别是AOP,IoC容器的基础。
注解的定义是:“附加在数据/代码上的元数据(metadata)。”框架可以基于这些元信息为代码提供各种额外功能。

以另一个框架PHPUnit为例,注解@dataProvider声明一个方法作为测试用例方法的数据提供器。当PHPUnit框架执行到某一个测试用例方法时,会迭代该数据提供器,并将其返回的数据作为参数传入测试用例方法,为测试用例方法提供一套用例所需的测试数据。

//摘自phpseclib库的单元测试
    public function formatLogDataProvider()
    {
        return array(
            array(
                //该参数会作为$message_log参数传到testFormatLog()测试用例方法中
                array('hello world'),            
                array('<--'),               //$message_number_log     
                "<--\r\n00000000  68:65:6c:6c:6f:20:77:6f:72:6c:64                 hello world\r\n\r\n"//$expected
            ),
            array(
                array('hello', 'world'),
                array('<--', '<--'),
                "<--\r\n00000000  68:65:6c:6c:6f                                   hello\r\n\r\n" .
                "<--\r\n00000000  77:6f:72:6c:64                                   world\r\n\r\n"
            ),
        );
    }

    /**
     * @dataProvider formatLogDataProvider
     */
    public function testFormatLog(array $message_log, array $message_number_log, $expected)
    {
         $ssh = $this->createSSHMock();

        $result = $ssh->_format_log($message_log, $message_number_log);
        $this->assertEquals($expected, $result);
    }

一般而言,在编程届中注解是一种和注释平行的概念。
注释提供对可执行代码的说明,单纯用于开发人员阅读,不影响代码的执行;而注解往往充当着对代码的声明和配置的作用,为可执行代码提供机器可用的额外信息,在特定的环境下会影响程序的执行。

但是由于官方对PHP的Annotation方案迟迟没有达成一致(最新进展可以在 PHP: rfc看到),目前PHP没有对注解的官方实现。主流的PHP框架中使用的注解都是借用T_DOC_COMMENT型注释块(/**型注释*/)中的@Tag,定义自己的注解机制。

想对PHP注解的发展史要有更多了解的朋友可以参考Rafael Dohms的这个PPT:https://www.slideshare.net/rdohms/annotations-in-php-they-exist/

Doctrine注解引擎

Swoft没有重新造轮子,搞一个新的的注解方案,而是选择使用Doctrine的注解引擎

Doctrine的注解方案也是基于T_DOC_COMMENT型注释的,Doctrine使用反射获取代码的T_DOC_COMMENT型注释,并将注释中的特定类型@Tag映射到对应注解类。为此,Swoft首先要为每一个框架自定义的注解定义注解类。

注解定义

@Breaker注解的注解类定义如下。

<?php
//Swoft\Sg\Bean\Annotation\Breaker.php
namespace Swoft\Sg\Bean\Annotation;

/**
 * the annotation of breaker
 *
 * @Annotation //声明这是一个注解类
 * @Target("CLASS")//声明这个注解只可用在class级别的注释中
 */
class Breaker
{
    /**
     * the name of breaker
     *
     * @var string   //@var是PHPDoc标准的常用的tag,定义了属性的类型\
     *                  Doctrine会根据该类型额外对注解参数进行检查
     */
    private $name = "";

    /**
     * 若注解类提供构造器,Doctrine会调用,一般会在此处对注解类对象的private属性进行赋值
     * Breaker constructor.
     *
     * @param array $values //Doctrine注解使用处的参数数组,
     */
    public function __construct(array $values)
    {
        if (isset($values['value'])) {
            $this->name = $values['value'];
        }
        if (isset($values['name'])) {
            $this->name = $values['name'];
        }
    }

     //按需写的getter setter code....
}

简单几行,一个@Breaker的注解类的定义工作就完成了。

注解类加载器的注册

在框架的bootstap阶段,swoft会扫描所有的PHP源码文件获取并解析注解信息。

使用Doctrine首先需要提供一个类的自动加载方法,这里直接使用了swoft当前的类加载器。Swoft的类加载器由Composer自动生成,这意味着注解类只要符合PSR-4规范即可自动加载。

//Swoft\Bean\Resource\AnnotationResource.php
    /**
     * 注册加载器和扫描PHP文件
     *
     * @return array
     */
    protected function registerLoaderAndScanBean()
    {
            // code code....

            AnnotationRegistry::registerLoader(function ($class) {
                if (class_exists($class) || interface_exists($class)) {
                    return true;
                }

                return false;
            });

            // coco....

        return array_unique($phpClass);
    }

使用Doctrine获取注解对象

扫描各源码目录获取PHP类后,Sworft会遍历类列表加载类,获取类级别,方法级别,属性级别的所有注解对象。结果存放在AnnotationResource的$annotations成员中。

//Swoft\Bean\Resource\AnnotationResource.php
    /**
     * 解析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);//跳过Swoft内部注解
        $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数组中。
//Swoft\Bean\Wrapper\AbstractWrapper.php
    /**
     * 封装注解
     *
     * @param string $className
     * @param array  $annotations 注解3剑客,包含了类级别,方法级别,属性级别的注解对象,注解解析流程你会一直看到他
     *
     * @return array|null
     */
    public function doWrapper(string $className, array $annotations)
    {
        $reflectionClass = new \ReflectionClass($className);

        // 解析类级别的注解
        $beanDefinition = $this->parseClassAnnotations($className, $annotations['class']);

        //code...

        // parser bean annotation
        list($beanName, $scope, $ref) = $beanDefinition;

        // 初始化Bean结构,并填充该Bean的相关信息
        $objectDefinition = new ObjectDefinition();
        $objectDefinition->setName($beanName);
        $objectDefinition->setClassName($className);
        $objectDefinition->setScope($scope);
        $objectDefinition->setRef($ref);

        if (!$reflectionClass->isInterface()) {
            // 解析属性,并获取属性相关依赖注入的信息
            $properties = $reflectionClass->getProperties();
            $propertyAnnotations = $annotations['property']??[];
            $propertyInjections = $this->parseProperties($propertyAnnotations, $properties, $className);
            $objectDefinition->setPropertyInjections($propertyInjections);//PropertyInjection对象
        }

        // 解析方法
        $publicMethods = $reflectionClass->getMethods(\ReflectionMethod::IS_PUBLIC);
        $methodAnnotations = $annotations['method'] ??[];
        $this->parseMethods($methodAnnotations, $className, $publicMethods);
       
        return [$beanName, $objectDefinition];
    }
  • 在注解解析时Parser会调用相关的Collector搜集功能所需的信息,譬如进行事件注册。
    举个例子,BootstrapParser的解析仅仅就是搜集注解。Collector在Swoft中是注解信息的最终装载容器。一般而言@XXXX注解对应的Parser和Collect就是XXXXParser和XXXXCollect,知道这个惯例会大大方便你对Swoft源码的阅读。
//Swoft\Bean\Parser\BootstrapParser.php
/**
 * the parser of bootstrap annotation
 *
 * @uses      BootstrapParser
 * @version   2018年01月12日
 * @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 BootstrapParser extends AbstractParser
{
    /**
     * @param string    $className
     * @param Bootstrap $objectAnnotation
     * @param string    $propertyName
     * @param string    $methodName
     * @param mixed     $propertyValue
     *
     * @return array
     */
    public function parser(string $className, $objectAnnotation = null, string $propertyName = "", string $methodName = "", $propertyValue = null)
    {
        $beanName = $className;
        $scope    = Scope::SINGLETON;

        BootstrapCollector::collect($className, $objectAnnotation, $propertyName, $methodName, $propertyValue);

        return [$beanName, $scope, ""];
    }
}

由于框架执行前必须完整的获取各种注解到Collertor和生成Bean定义集合,所以Swoft是不进行lazyload的。

注解的使用

现在我们终于可以用一个的例子来讲解注解是如何运行。InitMbFunsEncoding是一个实现了Bootable的类,他的作用是在应用启动时候设定系统的编码。但是仅仅实现了Bootable接口并不会让框架在启动时自动调用他。
因此我们需要InitMbFunsEncoding为添加一个@Bootstrap(order=1)类注解,让他成为一个Bootstrap型的Bean。

//Swoft\Bootstrap\Boots.InitMbFunsEncoding.php
<?php

namespace Swoft\Bootstrap\Boots;
use Swoft\Bean\Annotation\Bootstrap;

/**
 * @Bootstrap(order=1)     
 * @uses      InitMbFunsEncoding
 * @version   2017-11-02
 * @author    huangzhhui <huangzhwork@gmail.com>
 * @copyright Copyright 2010-2017 Swoft software
 * @license   PHP Version 7.x {@link http://www.php.net/license/3_0.txt}
 */
class InitMbFunsEncoding implements Bootable
{
    /**
     * bootstrap
     */
    public function bootstrap()
    {
        mb_internal_encoding("UTF-8");
    }
}

我们在上文已经提过框架启动时会扫描PHP源码

  • 将Bean的定义信息存放到ObjectDefinition数组中
  • 将注解信息存放到各个Collector中
    因此在框架的Bootstrap阶段,可以从BootstrapCollector中直接获取所有@Bootstrap型的Bean,实例化并Bean执行。
<?php

\\Swoft\Bootstrap\Bootstrap.php;

//code ...

    /**
     * bootstrap
     */
    public function bootstrap()
    {
        $bootstraps = BootstrapCollector::getCollector();
        //根据注解类型的不同,注解中的属性会有不同的作用,譬如@Bootstrap的order就影响各个Bean的执行顺序。
        array_multisort(array_column($bootstraps, 'order'), SORT_ASC, $bootstraps);
        foreach ($bootstraps as $bootstrapBeanName => $name){
            //使用Bean的ObjectDefinition信息构造实例或获取现有实例
            /* @var Bootable $bootstrap*/
            $bootstrap = App::getBean($bootstrapBeanName);
            $bootstrap->bootstrap();
        }
    }

//code ...

以上就是Swoft注解机制的整体实现了。

Swoft源码剖析系列目录:https://www.jianshu.com/p/2f679e0b4d58

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,440评论 5 467
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,814评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,427评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,710评论 1 270
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,625评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,014评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,511评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,162评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,311评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,262评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,278评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,989评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,583评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,664评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,904评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,274评论 2 345
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,856评论 2 339

推荐阅读更多精彩内容