1. 程式人生 > >詳解PHP反射API

詳解PHP反射API

具有完整的反射 API,添加了對類、介面、函式、方法和擴充套件進行_反向工程_的能力。 此外,反射 API 提供了方法來取出函式、類和方法中的文件註釋。

請注意部分內部 API 丟失了反射擴充套件工作所需的程式碼。 例如,一個內建的 PHP 類可能丟失了反射屬性的資料。這些少數的情況被認為是錯誤,不過, 正因為如此,它們應該被發現和修復。

知道人體構造和體內真氣分佈,你可以引導真氣到手指,練成一陽指、六脈神劍、彈指神通、九陰白骨爪等;也可以讓真氣匯聚,衝破任督二脈,開闢洞天;還可以逆轉全身經脈,練成蛤蟆功…內省的好處可見一斑。

反射讓程式碼感知自身結構,有什麼好處呢?反射API提供了三種在執行時對程式碼操作的能力:

  1. 設定訪問控制權: setAccessible 。可獲取私有的方法/屬性。 注意: setAccessible 只是讓方法/成員變數可以 invoke/getValue/setValue ,並不代表類定義的訪問存取許可權改變;
  2. 呼叫函式/方法: invoke/invokeArgs 。配合獲取函式引數的API,可以安全的傳參和呼叫函式, call_user_func(_array) 的增強版;
  3. 不依賴建構函式生成例項: newInstanceWithoutConstructor

以單例來說一下反射API的功能,單例類程式碼如下:

# foo.php
class Foo {
  private static $id;
  private static $instance;
 
  private function __construct() {
    ++ self::$id;
    fwrite(STDOUT, "construct, instance id: " . self::$id . "\n");
  }
 
  public static function getSingleton() {
    if (self::$instance === null) {
      self::$instance = new self();
    }
    return self::$instance;
  }
}

Foo 類中,建構函式是私有,獲取例項只能通過 getSingleton 方法,並且獲取到的是單例。但在反射API加持下,能獲取多個例項:

$instance1 = Foo::getSingleton();
var_dump($instance1);
 
$class = new ReflectionClass("Foo");
$constructor = $class->getConstructor();
if ((ReflectionProperty::IS_PUBLIC & $constructor->getModifiers()) === 0) {
    $constructor->setAccessible(true);
}
$instance2 = $class->newInstanceWithoutConstructor();
$constructor->invoke($instance2);
var_dump($instance2);
 
# 指令碼執行結果
construct, instance id: 1
object(Foo)#1 (0) {
}
construct, instance id: 2
object(Foo)#4 (0) {
}

我們成功的生成了兩個例項,並呼叫建構函式完成物件初始化。如果沒有反射API,這幾乎是不可能完成的工作。

反射API的部分類

描         述
Reflection 為類的摘要資訊提供靜態函式export()
ReflectionClass 類資訊和工具
ReflectionMethod 類方法資訊和工具
ReflectionParameter 方法引數資訊
ReflectionProperty 類屬性資訊
ReflectionFunction 函式資訊和工具
ReflectionExtension PHP擴充套件資訊
ReflectionException 錯誤類

使用反射API這些類,我們可以獲得在執行時訪問物件、函式和指令碼中的擴充套件的資訊。通過這些資訊我們可以用來分析類或者構建框架。

獲取類的資訊

我們在工作中使用過一些用於檢查類屬性的函式,例如:get_class_methods、getProduct等。這些方法對獲取詳細類資訊有很大的侷限性。

我們可以通過反射API類:Reflection 和 ReflectionClass 提供的靜態方法 export 來獲取類的相關資訊, export 可以提供類的幾乎所有的資訊,包括屬性和方法的訪問控制狀態、每個方法需要的引數以及每個方法在指令碼文件中的位置。這兩個工具類, export 靜態方法輸出結果是一致的,只是使用方式不同。

首先,構建一個簡單的類

<?php

class Student {
    public    $name;
    protected $age;
    private   $sex;

    public function __construct($name, $age, $sex)
    {
        $this->setName($name);
        $this->setAge($age);
        $this->setSex($sex);
    }

    public function setName($name)
    {
        $this->name = $name;
    }

    protected function setAge($age)
    {
        $this->age = $age;
    }

    private function setSex($sex)
    {
        $this->sex = $sex;
    }
}

使用 ReflectionClass::export() 獲取類資訊

ReflectionClass::export('Student');

列印結果:

Class [ class Student ] {
    @@ D:\wamp\www\test2.php 3-29
    - Constants [0] { }
    - Static properties [0] { }
    - Static methods [0] { }
    - Properties [3] {
        Property [ public $name ]
        Property [ protected $age ]
        Property [ private $sex ]
    }
    - Methods [4] {
        Method [ public method __construct ] {
            @@ D:\wamp\www\test2.php 8 - 13
            - Parameters [3] {
                Parameter #0 [ $name ]
                Parameter #1 [ $age ]
                Parameter #2 [ $sex ]
            }
        }
        Method [ public method setName ] {
            @@ D:\wamp\www\test2.php 15 - 18
            - Parameters [1] {
                Parameter #0 [ $name ]
            }
        }
        Method [ protected method setAge ] {
            @@ D:\wamp\www\test2.php 20 - 23
            - Parameters [1] {
                Parameter #0 [ $age ]
            }
        }
        Method [ private method setSex ] {
            @@ D:\wamp\www\test2.php 25 - 28
            - Parameters [1] {
                Parameter #0 [ $sex ]
            }
        }
    }
}

ReflectionClass類提供了非常多的工具方法,官方手冊給的列表如下:

ReflectionClass::__construct — 初始化 ReflectionClass 類
ReflectionClass::export — 匯出一個類
ReflectionClass::getConstant — 獲取定義過的一個常量
ReflectionClass::getConstants — 獲取一組常量
ReflectionClass::getConstructor — 獲取類的建構函式
ReflectionClass::getDefaultProperties — 獲取預設屬性
ReflectionClass::getDocComment — 獲取文件註釋
ReflectionClass::getEndLine — 獲取最後一行的行數
ReflectionClass::getExtension — 根據已定義的類獲取所在擴充套件的 ReflectionExtension 物件
ReflectionClass::getExtensionName — 獲取定義的類所在的擴充套件的名稱
ReflectionClass::getFileName — 獲取定義類的檔名
ReflectionClass::getInterfaceNames — 獲取介面(interface)名稱
ReflectionClass::getInterfaces — 獲取介面
ReflectionClass::getMethod — 獲取一個類方法的 ReflectionMethod。
ReflectionClass::getMethods — 獲取方法的陣列
ReflectionClass::getModifiers — 獲取類的修飾符
ReflectionClass::getName — 獲取類名
ReflectionClass::getNamespaceName — 獲取名稱空間的名稱
ReflectionClass::getParentClass — 獲取父類
ReflectionClass::getProperties — 獲取一組屬性
ReflectionClass::getProperty — 獲取類的一個屬性的 ReflectionProperty
ReflectionClass::getReflectionConstant — Gets a ReflectionClassConstant for a class's constant
ReflectionClass::getReflectionConstants — Gets class constants
ReflectionClass::getShortName — 獲取短名
ReflectionClass::getStartLine — 獲取起始行號
ReflectionClass::getStaticProperties — 獲取靜態(static)屬性
ReflectionClass::getStaticPropertyValue — 獲取靜態(static)屬性的值
ReflectionClass::getTraitAliases — 返回 trait 別名的一個數組
ReflectionClass::getTraitNames — 返回這個類所使用 traits 的名稱的陣列
ReflectionClass::getTraits — 返回這個類所使用的 traits 陣列
ReflectionClass::hasConstant — 檢查常量是否已經定義
ReflectionClass::hasMethod — 檢查方法是否已定義
ReflectionClass::hasProperty — 檢查屬性是否已定義
ReflectionClass::implementsInterface — 介面的實現
ReflectionClass::inNamespace — 檢查是否位於名稱空間中
ReflectionClass::isAbstract — 檢查類是否是抽象類(abstract)
ReflectionClass::isAnonymous — 檢查類是否是匿名類
ReflectionClass::isCloneable — 返回了一個類是否可複製
ReflectionClass::isFinal — 檢查類是否宣告為 final
ReflectionClass::isInstance — 檢查類的例項
ReflectionClass::isInstantiable — 檢查類是否可例項化
ReflectionClass::isInterface — 檢查類是否是一個介面(interface)
ReflectionClass::isInternal — 檢查類是否由擴充套件或核心在內部定義
ReflectionClass::isIterateable — 檢查是否可迭代(iterateable)
ReflectionClass::isSubclassOf — 檢查是否為一個子類
ReflectionClass::isTrait — 返回了是否為一個 trait
ReflectionClass::isUserDefined — 檢查是否由使用者定義的
ReflectionClass::newInstance — 從指定的引數建立一個新的類例項
ReflectionClass::newInstanceArgs — 從給出的引數建立一個新的類例項。
ReflectionClass::newInstanceWithoutConstructor — 建立一個新的類例項而不呼叫它的建構函式
ReflectionClass::setStaticPropertyValue — 設定靜態屬性的值
ReflectionClass::__toString — 返回 ReflectionClass 物件字串的表示形式。

使用 Reflection::export() 獲取類資訊

$prodClass = new ReflectionClass('Student');
Reflection::export($prodClass);

列印結果

Class [ class Student ] {
    @@ D:\wamp\www\test2.php 3-29
    - Constants [0] { }
    - Static properties [0] { }
    - Static methods [0] { }
    - Properties [3] {
        Property [ public $name ]
        Property [ protected $age ]
        Property [ private $sex ]
    }
    - Methods [4] {
        Method [ public method __construct ] {
            @@ D:\wamp\www\test2.php 8 - 13
            - Parameters [3] {
                Parameter #0 [ $name ]
                Parameter #1 [ $age ]
                Parameter #2 [ $sex ]
            }
        }
        Method [ public method setName ] {
            @@ D:\wamp\www\test2.php 15 - 18
            - Parameters [1] {
                Parameter #0 [ $name ]
            }
        }
        Method [ protected method setAge ] {
            @@ D:\wamp\www\test2.php 20 - 23
            - Parameters [1] {
                Parameter #0 [ $age ]
            }
        }
        Method [ private method setSex ] {
            @@ D:\wamp\www\test2.php 25 - 28
            - Parameters [1] {
                Parameter #0 [ $sex ]
            }
        }
    }
}

建立 ReflectionClass物件後,就可以使用 Reflection 工具類輸出 Student 類的相關資訊。Reflection::export() 可以格式化和輸出任何實現 Reflector 介面的類的例項。

檢查類

前面我們瞭解的 ReflectionClass 工具類,知道此類提供了很多的工具方法用於獲取類的資訊。例如,我們可以獲取到 Student 類的型別,是否可以例項化

工具函式

function classData(ReflectionClass $class) {
    $details = '';
    $name = $class->getName();          // 返回要檢查的類名
    if ($class->isUserDefined()) {      // 檢查類是否由使用者定義
        $details .= "$name is user defined" . PHP_EOL;
    }
    if ($class->isInternal()) {         // 檢查類是否由擴充套件或核心在內部定義
        $details .= "$name is built-in" . PHP_EOL;
    }
    if ($class->isInterface()) {        // 檢查類是否是一個介面
        $details .= "$name is interface" . PHP_EOL;
    }
    if ($class->isAbstract()) {         // 檢查類是否是抽象類
        $details .= "$name is an abstract class" . PHP_EOL;
    }
    if ($class->isFinal()) {            // 檢查類是否宣告為 final
        $details .= "$name is a final class" . PHP_EOL;
    }
    if ($class->isInstantiable()) {     // 檢查類是否可例項化
        $details .= "$name can be instantiated" . PHP_EOL;
    } else {
        $details .= "$name can not be instantiated" . PHP_EOL;
    }
    return $details;
}

$prodClass = new ReflectionClass('Student');
print classData($prodClass);

列印結果

Student is user defined
Student can be instantiated

除了獲取類的相關資訊,還可以獲取 ReflectionClass 物件提供自定義類所在的檔名及檔案中類的起始和終止行等相關原始碼資訊。

function getClassSource(ReflectionClass $class) {
    $path  = $class->getFileName();  // 獲取類檔案的絕對路徑
    $lines = @file($path);           // 獲得由檔案中所有行組成的陣列
    $from  = $class->getStartLine(); // 提供類的起始行
    $to    = $class->getEndLine();   // 提供類的終止行
    $len   = $to - $from + 1;
    return implode(array_slice($lines, $from - 1, $len));
}

$prodClass = new ReflectionClass('Student');
var_dump(getClassSource($prodClass));

列印結果

string 'class Student {
    public    $name;
    protected $age;
    private   $sex;

    public function __construct($name, $age, $sex)
    {
        $this->setName($name);
        $this->setAge($age);
        $this->setSex($sex);
    }

    public function setName($name)
    {
        $this->name = $name;
    }

    protected function setAge($age)
    {
        $this->age = $age;
    }

    private function setSex($sex)
    {
        $this->sex = $sex;
    }
}
' (length=486)

我們看到 getClassSource 接受一個 ReflectionClass 物件作為它的引數,並返回相應類的原始碼。該函式忽略了錯誤處理,在實際中應該要檢查引數和結果程式碼!

檢查方法

類似於檢查類,ReflectionMethod 物件可以用於檢查類中的方法。

獲得 ReflectionMethod 物件的方法有兩種:

第一種是通過 ReflectionClass::getMethods() 獲得 ReflectionMethod 物件的陣列,這種方式的好處是不用提前知道方法名,會返回類中所有方法的 ReflectionMethod  物件。

第二種是直接使用 ReflectionMethod  類例項化物件,這種方式只能獲取一個類方法物件,需要提前知道方法名。

ReflectionMethod 物件的工具方法:

ReflectionMethod::__construct — ReflectionMethod 的建構函式
ReflectionMethod::export — 輸出一個回撥方法
ReflectionMethod::getClosure — 返回一個動態建立的方法呼叫介面,譯者注:可以使用這個返回值直接呼叫非公開方法。
ReflectionMethod::getDeclaringClass — 獲取反射函式呼叫引數的類表達
ReflectionMethod::getModifiers — 獲取方法的修飾符
ReflectionMethod::getPrototype — 返回方法原型 (如果存在)
ReflectionMethod::invoke — Invoke
ReflectionMethod::invokeArgs — 帶引數執行
ReflectionMethod::isAbstract — 判斷方法是否是抽象方法
ReflectionMethod::isConstructor — 判斷方法是否是構造方法
ReflectionMethod::isDestructor — 判斷方法是否是析構方法
ReflectionMethod::isFinal — 判斷方法是否定義 final
ReflectionMethod::isPrivate — 判斷方法是否是私有方法
ReflectionMethod::isProtected — 判斷方法是否是保護方法 (protected)
ReflectionMethod::isPublic — 判斷方法是否是公開方法
ReflectionMethod::isStatic — 判斷方法是否是靜態方法
ReflectionMethod::setAccessible — 設定方法是否訪問
ReflectionMethod::__toString — 返回反射方法物件的字串表達

ReflectionClass::getMethods()

我們可以通過 ReflectionClass::getMethods() 獲得 ReflectionMethod 物件的陣列。

$prodClass = new ReflectionClass('Student');
$methods = $prodClass->getMethods();
var_dump($methods);

列印結果

array (size=4)
  0 => &
    object(ReflectionMethod)[2]
      public 'name' => string '__construct' (length=11)
      public 'class' => string 'Student' (length=7)
  1 => &
    object(ReflectionMethod)[3]
      public 'name' => string 'setName' (length=7)
      public 'class' => string 'Student' (length=7)
  2 => &
    object(ReflectionMethod)[4]

可以看到我們獲取到了 Student 的 ReflectionMethod 物件陣列,每個元素是一個物件,其中有兩個公共的屬性,name 為方法名,class 為所屬類。我們可以呼叫物件方法來獲取方法的資訊。

ReflectionMethod

直接使用 ReflectionMethod 類獲取類方法有關資訊

$method = new ReflectionMethod('Student', 'setName');
var_dump($method);

列印結果

object(ReflectionMethod)[1]
  public 'name' => string 'setName' (length=7)
  public 'class' => string 'Student' (length=7)

注意

在PHP5中,如果被檢查的方法只返回物件(即使物件是通過引用賦值或傳遞的),那麼 ReflectionMethod::retursReference() 不會返回 true。只有當被檢測的方法已經被明確宣告返回引用(在方法名前面有&符號)時,ReflectionMethod::returnsReference() 才返回 true。

檢查方法引數

在PHP5中,宣告類方法時可以限制引數中物件的型別,因此檢查方法的引數變得非常必要。

類似於檢查方法,ReflectionParameter 物件可以用於檢查類中的方法,該物件可以告訴你引數的名稱,變數是否可以按引用傳遞,還可以告訴你引數型別提示和方法是否接受空值作為引數。

獲得 ReflectionParameter 物件的方法有同樣兩種,這和獲取 ReflectionMethod 物件非常類似:

第一種是通過 ReflectionMethod::getParameters() 方法返回 ReflectionParameter 物件陣列,這種方法可以獲取到一個方法的全部引數物件。

第二種是直接使用 ReflectionParameter  類例項化獲取物件,這種方法只能獲取到單一引數的物件。

ReflectionParameter 物件的工具方法:

rameter::allowsNull — Checks if null is allowed
ReflectionParameter::canBePassedByValue — Returns whether this parameter can be passed by value
ReflectionParameter::__clone — Clone
ReflectionParameter::__construct — Construct
ReflectionParameter::export — Exports
ReflectionParameter::getClass — Get the type hinted class
ReflectionParameter::getDeclaringClass — Gets declaring class
ReflectionParameter::getDeclaringFunction — Gets declaring function
ReflectionParameter::getDefaultValue — Gets default parameter value
ReflectionParameter::getDefaultValueConstantName — Returns the default value's constant name if default value is constant or null
ReflectionParameter::getName — Gets parameter name
ReflectionParameter::getPosition — Gets parameter position
ReflectionParameter::getType — Gets a parameter's type
ReflectionParameter::hasType — Checks if parameter has a type
ReflectionParameter::isArray — Checks if parameter expects an array
ReflectionParameter::isCallable — Returns whether parameter MUST be callable
ReflectionParameter::isDefaultValueAvailable — Checks if a default value is available
ReflectionParameter::isDefaultValueConstant — Returns whether the default value of this parameter is constant
ReflectionParameter::isOptional — Checks if optional
ReflectionParameter::isPassedByReference — Checks if passed by reference
ReflectionParameter::isVariadic — Checks if the parameter is variadic
ReflectionParameter::__toString — To string

ReflectionMethod::getParameters()

同獲取方法,此方法會返回一個數組,包含方法每個引數的 ReflectionParameter 物件

$method = new ReflectionMethod('Student', 'setName');
$params = $method->getParameters();
var_dump($params);

列印結果

array (size=1)
  0 => &
    object(ReflectionParameter)[2]
      public 'name' => string 'name' (length=4)

ReflectionParameter

我們來了解一下這種方式,為了更好的理解,我修改一下 Student 類的 setName方法,增加兩個引數 a, b

...
    public function setName($name, $a, $b)
    {
        $this->name = $name;
    }
...

首先我們看一下 ReflectionParameter 類的構造方法

public ReflectionParameter::__construct ( string $function , string $parameter )

可以看到該類例項化時接收兩個引數:

$function:當需要獲取函式為公共函式時只需傳函式名稱即可。當該函式是某個類方法時,需要傳遞一個數組,格式為:array('class', 'function')。

$parameter:這個引數可以傳遞兩種,第一種為引數名(無$符號),第二種為引數索引。注意:無論是引數名還是索引,該引數都必須存在,否則會報錯。

下面舉例:

$params = new ReflectionParameter(array('Student', 'setName'), 1);
var_dump($params);

列印結果

object(ReflectionParameter)[1]
  public 'name' => string 'a' (length=1)

我們再定義一個函式測試一下

function foo($a, $b, $c) { }
$reflect = new ReflectionParameter('foo', 'c');
var_dump($reflect);

列印結果

object(ReflectionParameter)[2]
  public 'name' => string 'c' (length=1)

結語

php的反射API功能非常的強大,它可以將一個類的詳細資訊獲取出來。我們可以通過反射API編寫個類來動態呼叫Module物件,該類可以自由載入第三方外掛並整合進已有的系統。而不需要把第三方的程式碼硬編碼進原有的程式碼中。雖然實際開發中使用反射情況比較少,但瞭解反射API對工作中對程式碼結構的瞭解和開發業務模式幫助