Swoft是如何利用註解實現各類功能的

PHP中的註解

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

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

//摘自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);
    }

通常而言,在編程屆中註解是一種和註釋平行的概念。
註釋提供對可執行代碼的說明,單純用於開發人員閱讀,不影響代碼的執行;而註解每每充當着對代碼的聲明和配置的做用,爲可執行代碼提供機器可用的額外信息,在特定的環境下會影響程序的執行。github

可是因爲官方對PHP的Annotation方案遲遲沒有達成一致(最新進展能夠在 PHP: rfc看到),目前PHP沒有對註解的官方實現。主流的PHP框架中使用的註解都是借用T_DOC_COMMENT型註釋塊(/**型註釋*/)中的@Tag,定義本身的註解機制。編程

想對PHP註解的發展史要有更多瞭解的朋友能夠參考Rafael Dohms的這個PPT:https://www.slideshare.net/rdohms/annotations-in-php-they-exist/bootstrap

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;

/**
 * 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的註解類的定義工做就完成了。ssh

註解類加載器的註冊

在框架的bootstap階段,swoft會掃描全部的PHP源碼文件獲取並解析註解信息。ide

使用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

相關文章
相關標籤/搜索