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