Swoft 源碼剖析 - Swoft 中的註解機制

做者:bromine
連接:https://www.jianshu.com/p/ef7...
來源:簡書
著做權歸做者全部,本文已得到做者受權轉載,並對原文進行了從新的排版。
Swoft Github: https://github.com/swoft-clou...php

PHP中的註解

註解(Annotations)是Swoft裏面不少重要功能特別是AOP,IoC容器的基礎。
註解的定義是:「附加在數據/代碼上的元數據(metadata)。」框架能夠基於這些元信息爲代碼提供各類額外功能。git

以另外一個框架PHPUnit爲例,註解@dataProvider聲明一個方法做爲測試用例方法的數據提供器。當PHPUnit框架執行到某一個測試用例方法時,會迭代該數據提供器,並將其返回的數據做爲參數傳入測試用例方法,爲測試用例方法提供一套用例所需的測試數據。github

//摘自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,定義本身的註解機制。bootstrap

想對PHP註解的發展史要有更多瞭解的朋友能夠參考Rafael Dohms的這個PPT:https://www.slideshare.net/rd...segmentfault

Doctrine註解引擎

Swoft沒有從新造輪子,搞一個新的的註解方案,而是選擇使用 Doctrine的註解引擎數組

Doctrine的註解方案也是基於T_DOC_COMMENT型註釋的,Doctrine使用反射獲取代碼的T_DOC_COMMENT型註釋,並將註釋中的特定類型@Tag映射到對應註解類。爲此,Swoft首先要爲每個框架自定義的註解定義註解類。app

註解定義

@Breaker註解的註解類定義以下。框架

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

/**
 * @Annotation //聲明這是一個註解類
 * @Target("CLASS")//聲明這個註解只可用在class級別的註釋中
 */
class 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的註解類的定義工做就完成了。ssh

註解類加載器的註冊

在框架的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;
        });

        // code code....

    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
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)
 */
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://segmentfault.com/a/11...
相關文章
相關標籤/搜索