[原創(chuàng)]Swoft源碼剖析-Swoft中的注解機制

PHP中的注解

注解(Annotations)是Swoft里面很多重要功能特別是AOP乾蓬,IoC容器的基礎(chǔ)拗馒。
注解的定義是:“附加在數(shù)據(jù)/代碼上的元數(shù)據(jù)(metadata)。”框架可以基于這些元信息為代碼提供各種額外功能卧檐。

以另一個框架PHPUnit為例,注解@dataProvider聲明一個方法作為測試用例方法的數(shù)據(jù)提供器焰宣。當(dāng)PHPUnit框架執(zhí)行到某一個測試用例方法時霉囚,會迭代該數(shù)據(jù)提供器,并將其返回的數(shù)據(jù)作為參數(shù)傳入測試用例方法匕积,為測試用例方法提供一套用例所需的測試數(shù)據(jù)佛嬉。

//摘自phpseclib庫的單元測試
    public function formatLogDataProvider()
    {
        return array(
            array(
                //該參數(shù)會作為$message_log參數(shù)傳到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"http://$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);
    }

一般而言,在編程屆中注解是一種和注釋平行的概念闸天。
注釋提供對可執(zhí)行代碼的說明,單純用于開發(fā)人員閱讀斜做,不影響代碼的執(zhí)行苞氮;而注解往往充當(dāng)著對代碼的聲明和配置的作用,為可執(zhí)行代碼提供機器可用的額外信息瓤逼,在特定的環(huán)境下會影響程序的執(zhí)行笼吟。

但是由于官方對PHP的Annotation方案遲遲沒有達成一致(最新進展可以在 PHP: rfc看到),目前PHP沒有對注解的官方實現(xiàn)霸旗。主流的PHP框架中使用的注解都是借用T_DOC_COMMENT型注釋塊(/**型注釋*/)中的@Tag,定義自己的注解機制贷帮。

想對PHP注解的發(fā)展史要有更多了解的朋友可以參考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映射到對應(yīng)注解類精居。為此锄禽,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標(biāo)準(zhǔn)的常用的tag沃但,定義了屬性的類型\
     *                  Doctrine會根據(jù)該類型額外對注解參數(shù)進行檢查
     */
    private $name = "";

    /**
     * 若注解類提供構(gòu)造器,Doctrine會調(diào)用,一般會在此處對注解類對象的private屬性進行賦值
     * Breaker constructor.
     *
     * @param array $values //Doctrine注解使用處的參數(shù)數(shù)組,
     */
    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當(dāng)前的類加載器。Swoft的類加載器由Composer自動生成谆沃,這意味著注解類只要符合PSR-4規(guī)范即可自動加載钝凶。

//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會遍歷類列表加載類,獲取類級別,方法級別耕陷,屬性級別的所有注解對象掂名。結(jié)果存放在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內(nèi)部注解
        $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數(shù)組中猾警。
//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結(jié)構(gòu)拂蝎,并填充該Bean的相關(guān)信息
        $objectDefinition = new ObjectDefinition();
        $objectDefinition->setName($beanName);
        $objectDefinition->setClassName($className);
        $objectDefinition->setScope($scope);
        $objectDefinition->setRef($ref);

        if (!$reflectionClass->isInterface()) {
            // 解析屬性穴墅,并獲取屬性相關(guān)依賴注入的信息
            $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會調(diào)用相關(guān)的Collector搜集功能所需的信息,譬如進行事件注冊温自。
    舉個例子玄货,BootstrapParser的解析僅僅就是搜集注解。Collector在Swoft中是注解信息的最終裝載容器悼泌。一般而言@XXXX注解對應(yīng)的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, ""];
    }
}

由于框架執(zhí)行前必須完整的獲取各種注解到Collertor和生成Bean定義集合馆里,所以Swoft是不進行l(wèi)azyload的隘世。

注解的使用

現(xiàn)在我們終于可以用一個的例子來講解注解是如何運行。InitMbFunsEncoding是一個實現(xiàn)了Bootable的類鸠踪,他的作用是在應(yīng)用啟動時候設(shè)定系統(tǒng)的編碼以舒。但是僅僅實現(xiàn)了Bootable接口并不會讓框架在啟動時自動調(diào)用他。
因此我們需要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");
    }
}

我們在上文已經(jīng)提過框架啟動時會掃描PHP源碼

  • 將Bean的定義信息存放到ObjectDefinition數(shù)組中
  • 將注解信息存放到各個Collector中
    因此在框架的Bootstrap階段,可以從BootstrapCollector中直接獲取所有@Bootstrap型的Bean卵贱,實例化并Bean執(zhí)行滥沫。
<?php

\\Swoft\Bootstrap\Bootstrap.php;

//code ...

    /**
     * bootstrap
     */
    public function bootstrap()
    {
        $bootstraps = BootstrapCollector::getCollector();
        //根據(jù)注解類型的不同,注解中的屬性會有不同的作用键俱,譬如@Bootstrap的order就影響各個Bean的執(zhí)行順序兰绣。
        array_multisort(array_column($bootstraps, 'order'), SORT_ASC, $bootstraps);
        foreach ($bootstraps as $bootstrapBeanName => $name){
            //使用Bean的ObjectDefinition信息構(gòu)造實例或獲取現(xiàn)有實例
            /* @var Bootable $bootstrap*/
            $bootstrap = App::getBean($bootstrapBeanName);
            $bootstrap->bootstrap();
        }
    }

//code ...

以上就是Swoft注解機制的整體實現(xiàn)了。

Swoft源碼剖析系列目錄:http://www.reibang.com/p/2f679e0b4d58

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末编振,一起剝皮案震驚了整個濱河市缀辩,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖臀玄,帶你破解...
    沈念sama閱讀 221,635評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件瓢阴,死亡現(xiàn)場離奇詭異,居然都是意外死亡健无,警方通過查閱死者的電腦和手機荣恐,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,543評論 3 399
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來累贤,“玉大人叠穆,你說我怎么就攤上這事【矢啵” “怎么了硼被?”我有些...
    開封第一講書人閱讀 168,083評論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長渗磅。 經(jīng)常有香客問我祷嘶,道長,這世上最難降的妖魔是什么夺溢? 我笑而不...
    開封第一講書人閱讀 59,640評論 1 296
  • 正文 為了忘掉前任,我火速辦了婚禮烛谊,結(jié)果婚禮上风响,老公的妹妹穿的比我還像新娘。我一直安慰自己丹禀,他們只是感情好状勤,可當(dāng)我...
    茶點故事閱讀 68,640評論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著双泪,像睡著了一般持搜。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上焙矛,一...
    開封第一講書人閱讀 52,262評論 1 308
  • 那天葫盼,我揣著相機與錄音,去河邊找鬼村斟。 笑死贫导,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的蟆盹。 我是一名探鬼主播孩灯,決...
    沈念sama閱讀 40,833評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼逾滥!你這毒婦竟也來了峰档?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,736評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎讥巡,沒想到半個月后掀亩,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,280評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡尚卫,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,369評論 3 340
  • 正文 我和宋清朗相戀三年归榕,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片吱涉。...
    茶點故事閱讀 40,503評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡刹泄,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出怎爵,到底是詐尸還是另有隱情特石,我是刑警寧澤,帶...
    沈念sama閱讀 36,185評論 5 350
  • 正文 年R本政府宣布鳖链,位于F島的核電站姆蘸,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏芙委。R本人自食惡果不足惜逞敷,卻給世界環(huán)境...
    茶點故事閱讀 41,870評論 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望灌侣。 院中可真熱鬧推捐,春花似錦、人聲如沸侧啼。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,340評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽痊乾。三九已至皮壁,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間哪审,已是汗流浹背蛾魄。 一陣腳步聲響...
    開封第一講書人閱讀 33,460評論 1 272
  • 我被黑心中介騙來泰國打工曹仗, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留柏锄,地道東北人。 一個月前我還...
    沈念sama閱讀 48,909評論 3 376
  • 正文 我出身青樓橘券,卻偏偏與公主長得像茉稠,于是被迫代替她去往敵國和親描馅。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,512評論 2 359

推薦閱讀更多精彩內(nèi)容