简读composer自动加载源码(个人笔记向)

Composer的基本学习

个人笔记向,存在有问题的描述请多包涵

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 协议》,转载必须注明作者和本文链接
讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!