簡讀composer自動載入原始碼(個人筆記向)

JunYu發表於2022-02-12

個人筆記向,存在有問題的描述請多包涵

Composer 的型別

首先我們先執行指令

$ composer init
$ Package name (<vendor>/<name>) [admin/learncomposer]: zxyy/composerlearn
$ Description []: study composer
$ Author [JunYu <1016673080@qq.com>, n to skip]:
$ Minimum Stability []: stable
$ Package Type (e.g. library, project, metapackage, composer-plugin) []: library
$ License []: mit
Define your dependencies.
Would you like to define your dependencies (require) interactively [yes]? n
Would you like to define your dev dependencies (require-dev) interactively [yes]? n

第一行是初始化composer的指令。第二行是問你所寫的composer專案名是什麼 作者/專案名。第三行當然就是專案的簡介描述。第四行就是作者資訊,直接回車跳過。第五行就是最小相容版本。第六行就是問你你的這個composer專案是作為第三方擴充套件還是專案還是meta包,還是外掛。第七行是授權模式。之後的就是問你是否需要第三方的依賴。我這邊都寫了N。

然後就得到了這麼個檔案

composer.json

{
    "name": "zxyy/composerlearn",
    "description": "study composer",
    "type": "library",
    "license": "mit",
    "authors": [
        {
            "name": "JunYu",
            "email": "1016673080@qq.com"
        }
    ],
    "minimum-stability": "stable",
    "require": {}
}

關於這裡的type我主要說一下這個library和project的區別。前者的話所有的檔案資料都會在vendor裡面目錄結構為vendor/admin/names 後者則在一級目錄下。

composer自動載入

那麼說到composer那首先想到的就是自動載入

自動載入有classmap,psr-4,psr-0這幾個,還有一個file

我們先舉一個classmap的例子

composer.json

{
    "name": "zxyy/composerlearn",
    "description": "study composer",
    "type": "library",
    "license": "mit",
    "autoload": {
        "classmap": [
            "Test/ClassMap"
        ]
    },
    "authors": [
        {
            "name": "JunYu",
            "email": "1016673080@qq.com"
        }
    ],
    "minimum-stability": "stable",
    "require": {}
}

然後在目錄下建立一個Test/ClassMap資料夾且有一個Job類

Test/ClassMap/Job.php

<?php

namespace Test\ClassMap;

class Job
{

}

然後我們去終端執行

$ composer dump-autoload

然後我們再去看

vendor/composer/autoload_static.php

<?php

// autoload_static.php @generated by Composer

namespace Composer\Autoload;

class ComposerStaticInit63ea353d816bb31af54de6d6f84e4d3e
{
    public static $classMap = array (
        'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php',
        'Test\\ClassMap\\Job' => __DIR__ . '/../..' . '/Test/ClassMap/Job.php',
    );

    public static function getInitializer(ClassLoader $loader)
    {
        return \Closure::bind(function () use ($loader) {
            $loader->classMap = ComposerStaticInit63ea353d816bb31af54de6d6f84e4d3e::$classMap;

        }, null, ClassLoader::class);
    }
}

我們可以發現在名為ComposerStaticInit的類檔案中出現了一條

'Test\\ClassMap\\Job' => __DIR__ . '/../..' . '/Test/ClassMap/Job.php',的記錄,這條記錄就是獲取到了Job檔案的絕對路徑。

熟悉Laravel的同學們肯定是對file和psr-4有所瞭解。

在laravel中有一個叫App的名稱空間,那麼我們自己寫要怎麼去寫呢?

composer.json

{
    "name": "zxyy/composerlearn",
    "description": "study composer",
    "type": "library",
    "license": "mit",
    "autoload": {
        "classmap": [
            "Test/ClassMap"
        ],
        "psr-4": {
            "App\\":"app/"
        }
    },
    "authors": [
        {
            "name": "JunYu",
            "email": "1016673080@qq.com"
        }
    ],
    "minimum-stability": "stable",
    "require": {}
}

在psr-4裡面,我們前面所寫的是你所取的名稱空間名:對應的則是實際目錄的位置

所以我們現在就可以在檔案目錄中建立app\Psr4\Job.php

<?php

namespace App\Psr4;

class Job
{

}

然後我們再去執行一下

$ composer dump-autoload

看一下

vendor/composer/autoload_static.php

...
public static $prefixLengthsPsr4 = array (
        'A' => 
        array (
            'App\\' => 4,
        ),
    );

public static $prefixDirsPsr4 = array (
    'App\\' => 
    array (
        0 => __DIR__ . '/../..' . '/app',
    ),
);
...

我們可以看到prefixlengthsPsr4和prefixDirsPsr4這倆個兄弟

前者是名稱空間的長度,以及首字母開頭

後者是名稱空間所對應的資料夾的絕對路徑

當然也可以做到一個名稱空間對應多個資料夾

composer.json

...
 "App\\":["app/","app1/"]
...

然後我們再去執行一下

$ composer dump-autoload

看一下

vendor/composer/autoload_static.php

...
public static $prefixDirsPsr4 = array (
        'App\\' => 
        array (
            0 => __DIR__ . '/../..' . '/app',
            1 => __DIR__ . '/../..' . '/app1',
        ),

);
...

然後我們可以發現這裡多一個app1的絕對路徑

psr-4還支援無名稱空間指定具體寫法如下

"psr-4": {
    "App\\":["app/","app1/"],
    "":"nullspace/"
}

……

public static $fallbackDirsPsr4 = array (
    0 => __DIR__ . '/../..' . '/NullSpace',
);

這個意思是說NullSpace目錄下的所有不帶名稱空間的檔案都通過psr-4載入

那麼psr-0其實在使用上就和psr-4是一樣的在此我就不多贅述。

我們來看最後一個就是files載入

使用laravel的朋友應該知道的,helps助手函式就是通過file載入在全域性的。

composer.json

...
"files": [
            "helpers.php",
            "app/hello.php"
        ]
...

然後我們再去執行一下

$ composer dump-autoload

看一下

vendor/composer/autoload_static.php

public static $files = array (
    'cf234aa6b2b7e8258c522027a10f3d31' => __DIR__ . '/../..' . '/helpers.php',
    '4c89b7b03f917434285aa13e4af37b9f' => __DIR__ . '/../..' . '/app/hello.php',
);

我們就可以發現我們獲取到了他們所處的絕對路徑

Composer的載入過程

首先我們可以看到vendor\autoload.php

<?php

// autoload.php @generated by Composer

require_once __DIR__ . '/composer/autoload_real.php';

return ComposerAutoloaderInit63ea353d816bb31af54de6d6f84e4d3e::getLoader();

我們可以發現先引入了/composer/autoload_real.php

返回了執行ComposerAutoloaderInit::getLoader()的結果。

接下來的文字將用...省略與我講解無關的程式碼內容

我們開啟/composer/autoload_real.php檔案

class ComposerAutoloaderInit63ea353d816bb31af54de6d6f84e4d3e
{
    private static $loader;

    public static function loadClassLoader($class)
    {
        if ('Composer\Autoload\ClassLoader' === $class) {
            require __DIR__ . '/ClassLoader.php';
        }
    }

    /**
     * @return \Composer\Autoload\ClassLoader
     */
    public static function getLoader()
    {
        // 如果你是第一次執行就不會進入這個地方的判斷
        if (null !== self::$loader) {
            return self::$loader;
        }

        spl_autoload_register(array('ComposerAutoloaderInit63ea353d816bb31af54de6d6f84e4d3e', 'loadClassLoader'), true, true);
        self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(\dirname(__FILE__)));
        spl_autoload_unregister(array('ComposerAutoloaderInit63ea353d816bb31af54de6d6f84e4d3e', 'loadClassLoader'));
    ...
    ...
}

好我們先講解以上的程式碼。

getLoader這個靜態函式,首先判斷,你是不是第一次執行,如果不是的話,就把已有的$loader也就是裝載資訊返回給你。

之後我們可以看到spl_autoload_register這個函式,函式的第一個傳參是一個陣列[要執行的類名要執行的方法],第二個傳參是是否丟擲錯誤,第三個是新增進autoload佇列隊首。

案例如下:

class A
{
    public static function demo()
    {
        echo '我是'.__ClASS__;
        require_once 'dede.php';

    }
}
class B
{
    public static function demo()
    {
        echo '我是'.__ClASS__;
    }
}
class C
{
    public static function demo()
    {
        echo '我是'.__ClASS__;
    }
}

spl_autoload_register(['A','demo'],false,true);
spl_autoload_register(['B','demo'],false,true);
spl_autoload_register(['C','demo'],false,true);
// 當我們例項化一個PHP無法找到的類時,PHP就會去執行autoload列表
// 當然如果執行了以後還是沒有這個類那就會理所當然的報錯。
new dede();

當我們例項化一個PHP無法找到的類時,PHP就會去執行autoload列表。所以上面的案例執行後的結果如下

我是C我是B我是A

因為執行了spl三行以後 在autoload佇列中是 C-B-A

然後new dede的時候發現當前檔案中不存在,那就去執行了autoloader佇列,然後從輸出了我是C我是B我是A 輸出我是A以後require了dede.php dede.php裡面有dede這個類,所以例項化成功了。

還有一點要說明的是,實力化哪個類的時候出現錯誤,去觸發了autoload列表時,我們可以在函式中用變數獲取到類名(可以加上名稱空間)

class C
{
    public static function demo($class)
    {
        echo '觸發的類名是'.$class;
//        echo '我是'.__ClASS__;
    }
}
spl_autoload_register(['C','demo'],false,true);
// 當我們例項化一個PHP無法找到的類時,PHP就會去執行autoload列表
new \Test\dede();

執行結果

觸發的類名是Test\dede我是C

當然這樣子還是有錯誤資訊的,因為你沒這個類嘛。

寫一個簡單的按需載入demo:

start.php

<?php

class start
{
    public static function demo($class)
    {
        require_once __DIR__.'/'.$class.'.php';
    }

    public static function getLoader()
    {
        spl_autoload_register(['start','demo'],true,true);
    }
}

start::getLoader();
$test = new momo();

momo.php

<?php

class momo
{
    public function __construct()
    {
        echo '123';
    }
}

執行start結果:

123

誒其實最簡單的自動載入也就是這樣子。

好接下來我們繼續去讀composer的程式碼

spl_autoload_register(array('ComposerAutoloaderInit63ea353d816bb31af54de6d6f84e4d3e', 'loadClassLoader'), true, true);
self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(\dirname(__FILE__)));
spl_autoload_unregister(array('ComposerAutoloaderInit63ea353d816bb31af54de6d6f84e4d3e', 'loadClassLoader'));

如果接下來出現例項化,未找到的類就去執行composerAutoloaderInit::loadClassLoader

這邊$loader變數在例項化的時候並沒有找到,所以就執行了spl_autoload_register然後我們看loadClassLoader

public static function loadClassLoader($class)
    {
        var_dump($class);
        if ('Composer\Autoload\ClassLoader' === $class) {
            require __DIR__ . '/ClassLoader.php';
        }
    }

我們可以去看輸出的結果

"Composer\Autoload\ClassLoader"

如果此時為真,那麼就引入ClassLoader.php

那麼這個時候我們的

self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(\dirname(__FILE__)));

就可以執行成功了,例項化了類載入器,順便傳入了檔案路徑。

之後便是把註冊器取消註冊spl_autoload_unregister

我們繼續往下看

$useStaticLoader = PHP_VERSION_ID >= 50600 && !defined('HHVM_VERSION') && (!function_exists('zend_loader_file_encoded') || !zend_loader_file_encoded());

這句程式碼的意思就是,現在使用的PHP版本是否大於5.6 是否使用了HHVM虛擬機器(Facebook寫的可以自己去百度) 和有沒有使用zend加密PHP

if ($useStaticLoader) {
    require __DIR__ . '/autoload_static.php';

    call_user_func(\Composer\Autoload\ComposerStaticInit63ea353d816bb31af54de6d6f84e4d3e::getInitializer($loader));
} else {
    $map = require __DIR__ . '/autoload_namespaces.php';
    foreach ($map as $namespace => $path) {
        $loader->set($namespace, $path);
    }

    $map = require __DIR__ . '/autoload_psr4.php';
    foreach ($map as $namespace => $path) {
        $loader->setPsr4($namespace, $path);
    }

    $classMap = require __DIR__ . '/autoload_classmap.php';
    if ($classMap) {
        $loader->addClassMap($classMap);
    }
}

我這邊就看第一個分支(我已經不用5.6 也沒有用hhvm和zend)

require __DIR__ . '/autoload_static.php';

    call_user_func(\Composer\Autoload\ComposerStaticInit63ea353d816bb31af54de6d6f84e4d3e::getInitializer($loader));

call_user_func()可以執行回撥,這個就不用我展開解釋了吧?emm 說句不太正經的就是你return了一個function call_user_func幫你把它執行了~

我們開啟這個autoload_static.php重點看這個getInitializer

public static function getInitializer(ClassLoader $loader)
{
return \Closure::bind(function () use ($loader) {
    $loader->prefixLengthsPsr4 = ComposerStaticInit63ea353d816bb31af54de6d6f84e4d3e::$prefixLengthsPsr4;
    $loader->prefixDirsPsr4 = ComposerStaticInit63ea353d816bb31af54de6d6f84e4d3e::$prefixDirsPsr4;
    $loader->fallbackDirsPsr4 = ComposerStaticInit63ea353d816bb31af54de6d6f84e4d3e::$fallbackDirsPsr4;
    $loader->classMap = ComposerStaticInit63ea353d816bb31af54de6d6f84e4d3e::$classMap;

}, null, ClassLoader::class);
}

我就簡單說一下這個Closure::bind

上案例了:

Class A
{
    private static $name = '我是A';
    public function getName(){
        return self::$name;
    }
}

Class B
{
    private static $name = '我是B';
    public function getName(){
        return self::$name;
    }
}

$closure = Closure::bind(function (){
    echo $this->getName();
},new A(),null);

$closure();

輸出

我是A
程式已結束,退出程式碼為 0

我們可以發現此時輸出的是A

因為我們在第二個形參處丟了個例項化A的物件進去。

你如果要在這個閉包中用到$this關鍵字你就必須要在第二個形參處傳入物件,否則就填寫null 不使用

$closure = Closure::bind(function (){
    echo $this->getName();
},null,null);

這樣子就會報錯

Fatal error: Uncaught Error: Using $this when not in object context in

那麼第三個是什麼呢?

$aa = new A();

$closure = Closure::bind(function () use ($aa){
    echo $aa::$name;
},null,A::class);

$closure();

輸出

我是A
程式已結束,退出程式碼為 0
$aa = new A();

$closure = Closure::bind(function () use ($aa){
    echo $aa::$name;
},null,null);

$closure();

輸出

Fatal error: Uncaught Error: Cannot access private property A::$name

由此我們可以得出第三個其實是newScope 就是一個作用域,我們填入以後我們就可以在這個閉包內使用到$aa 裡面這個物件裡面的私有成員。

好了接下來我們繼續去看getInitializer

其實這個閉包就是將autoload_static.php裡面所記錄的檔案的所有絕對路徑,賦值給autoload_real.php裡面的$loader實際上這個loader就是ClassLoader.php裡面的類。

然後程式碼就執行到

$loader->register(true);

這個register其實就是classloader.php中的。

好!接下來我們開啟對應的部分。

public function register($prepend = false)
{
    spl_autoload_register(array($this, 'loadClass'), true, $prepend);

    if (null === $this->vendorDir) {
        return;
    }

    if ($prepend) {
        self::$registeredLoaders = array($this->vendorDir => $this) + self::$registeredLoaders;
    } else {
        unset(self::$registeredLoaders[$this->vendorDir]);
        self::$registeredLoaders[$this->vendorDir] = $this;
    }
}

這裡我們可以看到$this::loadClass()這麼一回事,我們直接去看一下。

public function loadClass($class)
{
    var_dump($class);
    if ($file = $this->findFile($class)) {
        includeFile($file);

        return true;
    }

    return null;
}

我們還可以發現接收了一個$class那麼這個class就是那個需要載入的類了。

我們也可以從findFile這個函式可以發現一定有一個尋找檔案的步驟。

然後檔案尋找到以後,includeFile就是引入檔案了呀。

我們新建一個index.php 然後new一個之前使用classmap自動載入的類。

index.php

<?php

require_once "vendor/autoload.php";

new \Test\ClassMap\Job();

輸出的內容為

string(17) "Test\ClassMap\Job"

接下來我們去看findFile的部分

public function findFile($class)
{
    // class map lookup
    //$this->classMap 其實就是 autoload_static.php public static $classMap
    if (isset($this->classMap[$class])) {
        return $this->classMap[$class];
    }
    //$this->classMapAuthoritative 布林屬性 如果True 或者 屬於丟失類中就直接不載入
    if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) {
        return false;
    }
    // apcu是PHP的一個自帶快取功能,判斷你的檔案路徑是不是在快取裡面。
    // 詳情可見https://www.php.net/manual/en/book.apcu.php
    if (null !== $this->apcuPrefix) {
        $file = apcu_fetch($this->apcuPrefix.$class, $hit);
        if ($hit) {
            return $file;
        }
    }
    // 到這裡其實就是psr-4 和 psr-0這些方法的載入了
    $file = $this->findFileWithExtension($class, '.php');

    // 如果你用了hhvm虛擬機器那就執行下面這個判斷,組成的檔案字尾為.hh
    // Search for Hack files if we are running on HHVM
    if (false === $file && defined('HHVM_VERSION')) {
        $file = $this->findFileWithExtension($class, '.hh');
    }
    // 如果apcu快取裡木得 那就加進去
    if (null !== $this->apcuPrefix) {
        apcu_add($this->apcuPrefix.$class, $file);
    }

    if (false === $file) {
        // Remember that this class does not exist.
        $this->missingClasses[$class] = true;
    }

    return $file;
}

幾乎每句程式碼我都註釋了一下。

當classmap中不存在之後,composer就會為我們去psr-4或0也就是你設定的別的自動載入模式裡面找。

$file = $this->findFileWithExtension($class, '.php');

接下來我們來看一下這個findFilewithExtension

此時我的index.php

<?php

require_once "vendor/autoload.php";

new \App\Psr4\Job();

這邊只讀psr-4因為psr-0其實流程上大家自己看也差不多的。

private function findFileWithExtension($class, $ext)
    {
        // PSR-4 lookup
        $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext;
        $first = $class[0];
        if (isset($this->prefixLengthsPsr4[$first])) {
            $subPath = $class;
            while (false !== $lastPos = strrpos($subPath, '\\')) {
                $subPath = substr($subPath, 0, $lastPos);
                $search = $subPath . '\\';
                if (isset($this->prefixDirsPsr4[$search])) {
                    $pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1);
                    foreach ($this->prefixDirsPsr4[$search] as $dir) {
                        if (file_exists($file = $dir . $pathEnd)) {
                            return $file;
                        }
                    }
                }
            }
        }

        // PSR-4 fallback dirs
        foreach ($this->fallbackDirsPsr4 as $dir) {
            if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) {
                return $file;
            }
        }
}

$logicalPathPsr4 其實就是更具psr-4規則拼裝好的一個檔案路徑

接下來我們去看autoload_static.php中生成的資料 再結合這個部分的程式碼我相信就可以理解了

    public static $prefixLengthsPsr4 = array (
        'A' => 
        array (
            'App\\' => 4,
        ),
    );

    public static $prefixDirsPsr4 = array (
        'App\\' => 
        array (
            0 => __DIR__ . '/../..' . '/app',
            1 => __DIR__ . '/../..' . '/app1',
        ),
    );

我來逐句解釋其含義

$logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext;
$first = $class[0];

首先將斜線改為相對系統所對應的分割線也就是DIRECTORY_SEPARATOR的作用

我此時的$classApp\Psr4\Job

然後取出首字母,也就是我這邊的A

if (isset($this->prefixLengthsPsr4[$first])){
                $subPath = $class;
    while (false !== $lastPos = strrpos($subPath, '\\')) {
                var_dump($lastPos);// 第一次下標為8
                $subPath = substr($subPath, 0, $lastPos);
                var_dump(substr($subPath, 0, $lastPos));
                $search = $subPath . '\\';
                if (isset($this->prefixDirsPsr4[$search])) {
                    $pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1);
                    var_dump($pathEnd);
                    var_dump($logicalPathPsr4);
                    foreach ($this->prefixDirsPsr4[$search] as $dir) {
                        if (file_exists($file = $dir . $pathEnd)) {
                            return $file;
                        }
                    }
                }
            }

判斷是否存在$prefixLengthsPsr4[A]這個元素

存在的話將其設定為$subPath並且計算出最後一個反斜線出現的下標—–藉助strrpos函式

開始迴圈while第一次得出下標為8,藉助substr提取出檔案目錄為App\Psr4

然後加上反斜線成為App\Psr4\prefixDirsPsr4對比結果發現不存在,那麼開始第二次迴圈。$search成為了App\

成功進入判斷$pathEnd='\Psr4\Job.php'

進入foreach去組合絕對路徑去判斷檔案是否存在,存在就返回路徑。

最後執行到includeFile()

而includeFile?也就是如下:

function includeFile($file)
{
    include $file;
}

一般的載入流程大概就是這樣子了。

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章