ThinkPHP6 例項化 Http 類和依賴注入

lxdong12發表於2020-01-14

index.php 第二句

// 執行HTTP應用並響應
$http = (new App())->http;
public function __construct(string $rootPath = '')
{
    // 定義一些目錄地址
    $this->thinkPath   = dirname(__DIR__) . DIRECTORY_SEPARATOR;
    $this->rootPath    = $rootPath ? rtrim($rootPath, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR : $this->getDefaultRootPath();
    $this->appPath     = $this->rootPath . 'app' . DIRECTORY_SEPARATOR;
    $this->runtimePath = $this->rootPath . 'runtime' . DIRECTORY_SEPARATOR;

    // 將provider.php中返回的類的對映合併到 $bind 變數中,相同名稱的會覆蓋,如此可以通過在該檔案中定義方式改變框架所使用的某些核心類
    if (is_file($this->appPath . 'provider.php')) {
        $this->bind(include $this->appPath . 'provider.php');
    }

    // 將當前App例項儲存到靜態屬性 $instance 中
    static::setInstance($this);

    // 繫結當前例項到 $instances 陣列的中,後面再有用到這個類例項時直接取這裡的
    $this->instance('app', $this);
    $this->instance('think\Container', $this);
}

看下App類繼承的Container類中的 bind 方法

public function bind($abstract, $concrete = null)
{
    // 如果是陣列的話,就迴圈該陣列,陣列中的每個元素重走該 bind 方法,我們上面呼叫的 bind 就是先走了這裡
    if (is_array($abstract)) {
        foreach ($abstract as $key => $val) {
            $this->bind($key, $val);
        }

    // 如果是匿名函式,直接儲存到 bind 中
    } elseif ($concrete instanceof Closure) {
        $this->bind[$abstract] = $concrete;

    // 如果是一個類的例項,則儲存到 instances 陣列中
    } elseif (is_object($concrete)) {
        $this->instance($abstract, $concrete);

    // 我們前面呼叫 bind 方法在進過第一次 foreach 迴圈後就是走這裡了,繫結到 bind 變數中
    } else {
        // 獲取真實類名,因為有可能傳過來的是一個別名,當然在這裡兩個都不是,依然返回了它們自己
        $abstract = $this->getAlias($abstract);
        $this->bind[$abstract] = $concrete;
    }

    return $this;
}

再看看 getAlias 方法

public function getAlias(string $abstract): string
{
    // 檢查 $bind 中是否有儲存對應的類名
    if (isset($this->bind[$abstract])) {
        $bind = $this->bind[$abstract];
        // 如果有,並且是一個字串(還有可能是閉包),那這個字串有可能還是別名,繼續查詢 $bind
        if (is_string($bind)) {
            return $this->getAlias($bind);
        }
    }

    return $abstract;
}

App類中並沒有http這個成員變數,(new App())->http 會呼叫 App 類的魔術方法 __get

// 在 App 繼承的 Container類中
public function __get($name)
{
    return $this->get($name);
}

public function get($abstract)
{
    // 判斷 bind 變數中是否有繫結對應的類名或者 instances 變數中是否已儲存對應類的例項
   // 比如這裡 $abstract=http, 在 bind 中是有的 
    if ($this->has($abstract)) {
        // 獲取這個類的例項
        return $this->make($abstract);
    }

    throw new ClassNotFoundException('class not exists: ' . $abstract, $abstract);
}

讓我們看看 make方法裡做了什麼

/**
 * 建立類的例項 已經存在則直接獲取
 * @access public
 * @param string $abstract    類名或者標識
 * @param array  $vars        變數
 * @param bool   $newInstance 是否每次建立新的例項
 * @return mixed
 */
public function make(string $abstract, array $vars = [], bool $newInstance = false)
{
    $abstract = $this->getAlias($abstract);

    // 如果 instances 已儲存對應的例項,就直接返回
    if (isset($this->instances[$abstract]) && !$newInstance) {
        return $this->instances[$abstract];
    }

    // 如果是一個閉包
    if (isset($this->bind[$abstract]) && $this->bind[$abstract] instanceof Closure) {
        // em....這裡還沒看
        $object = $this->invokeFunction($this->bind[$abstract], $vars);
    } else {
        // 通過反射獲得所需要的類例項
        $object = $this->invokeClass($abstract, $vars);
    }

    if (!$newInstance) {
        // 儲存例項到 instances 陣列中
        $this->instances[$abstract] = $object;
    }

    return $object;
}

然後是 invokeClass 方法

public function invokeClass(string $class, array $vars = [])
{
    try {
        // 獲取 http 的反射類
        $reflect = new ReflectionClass($class);

        if ($reflect->hasMethod('__make')) {
            $method = new ReflectionMethod($class, '__make');

            // 如果有 __make 方法,並且是公有的靜態方法,執行 __make 方法並返回,後面在例項 Request 類時會走這裡。
            if ($method->isPublic() && $method->isStatic()) {
                $args = $this->bindParams($method, $vars);
                return $method->invokeArgs(null, $args);
            }
        }

        // 獲取類的建構函式
        $constructor = $reflect->getConstructor();

        // 繫結建構函式的引數,http 這個類的構造方法需要一個App類的例項作為引數,這個方法的註釋中有些到支援依賴注入,如何實現的依賴注入就在這個 bindParams 方法中
        $args = $constructor ? $this->bindParams($constructor, $vars) : [];

        // 
        $object = $reflect->newInstanceArgs($args);

        $this->invokeAfter($class, $object);

        return $object;
    } catch (ReflectionException $e) {
        throw new ClassNotFoundException('class not exists: ' . $class, $class, $e);
    }
}

bindParams 是如何實現依賴注入的

protected function bindParams($reflect, array $vars = []): array
{
    // 獲取方法的引數個數,0的話直接返回空陣列
    if ($reflect->getNumberOfParameters() == 0) {
        return [];
    }

    // 判斷陣列型別 數字陣列時按順序繫結引數
    reset($vars); // 將陣列的指標指到陣列的第一個元素
    $type   = key($vars) === 0 ? 1 : 0; // 獲取當前指標所在陣列元素的下標,判斷是不是0
    $params = $reflect->getParameters(); // 獲取方法所需的引數
    $args   = [];

    foreach ($params as $param) {
        $name      = $param->getName(); // app
        $lowerName = Str::snake($name); // 大寫轉下劃線
        $class     = $param->getClass(); // 獲取引數型別限定的類 think\App

        // 如果引數型別限定是一個類,這裡就是走的這個
        if ($class) {
            // getObjectParam 方法中再次呼叫了 make 方法來獲取需要的 App類,而這個 App 類已經在前面 new App()的時候儲存到 instances 變數中了,如此就完成了依賴注入
            // 假如 App 類的建構函式中需要其它類的例項作為引數,那就會再走一遍這個流程
            // 我的理解,依賴注入就是對類例項的需要通過傳參來實現,控制反轉就是這個傳參不需要使用者主動傳,交由Container完成
            $args[] = $this->getObjectParam($class->getName(), $vars);
        } elseif (1 == $type && !empty($vars)) {
        ...
    }
    return $args;
}
本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章