composer自动加载过程初步学习

composer听说过好久了,但是一直都没细节的去了解,简单查阅资料大部分是讲解命令,比如:composer install、composer update作用。autoload怎么实现的,暂时没有找到特别详细的。

 

先来了解下autoload。

首先,autoload有psr-0和psr-4两种规范。

psr-0:

composer自动加载过程初步学习_第1张图片

psr-4:

composer自动加载过程初步学习_第2张图片

其实最早找到的是英文版,不过甭管是中文还是英文,说实话刚看理解不了到底是啥意思。psr-4相对来说更好明白一点,psr-0有点绕不明白,虽然说是个被抛弃的规范,由于composer还是兼容psr-0,还是得学习下。继续看文字有点不懂,干脆就直接看源码,观察其实现autoload的原理。

 

先执行命令:composer require monolog/monolog

monolog库是初步学习的常用例子,库使用和结果反馈都非常简单清晰。这里作用是为了生成composer目录

核心目录如下所示:

composer自动加载过程初步学习_第3张图片

monolog当前版本为:1.23.0,基于psr-4规范实现自动加载,不足以了解其他规范,因此,在composer.json内修改:

composer自动加载过程初步学习_第4张图片

执行composer install

这部分内容是为了加载自定义的类,classmap和files又是什么呢?看了源码后就明白了,我们可以通过四种方式实现加载,这里先在项目多创建几个目录和php文件

  1. ./src/Person.php
  2. ./common/Utils.php
  3. ./vendor/src/Person.php
  4. ./vendor/src/two/name/Person.php

开始关注和代码相关的内容~

require 'vendor/autoload.php';

composer的初步使用,就是在项目的入口文件require该指定文件,

调用autoload_real.php内的getLoader()静态方法。

spl_autoload_register(array('ComposerAutoloaderInit9dd8e0b0864cf05cbb09cc2bda0092ef', 'loadClassLoader'), true, true);

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

spl_autoload_unregister(array('ComposerAutoloaderInit9dd8e0b0864cf05cbb09cc2bda0092ef', 'loadClassLoader'));

先注册类内loadClassLoader()静态方法autoload,实例同级目录下的ClassLoader对象,撤销注册方法loadClassLoader()

这块代码功能是实例化autoload的核心类ClassLoader。

//1、PHP版本大于等于5.6.0
//2、非HHVM运行环境
//3、不存在zend_loader_file_encoded方法
//4、若存在zend_loader_file_encoded方法,且执行结果为false
$useStaticLoader = PHP_VERSION_ID >= 50600 && !defined('HHVM_VERSION') && (!function_exists('zend_loader_file_encoded') || !zend_loader_file_encoded());

$useStaticLoader变量产生的分支,仅在与加载文件的路径是否存在../部分,若为true,基于当前目录进行定位,通过../向上级目录查找;若为false,以项目根目录进行定位向下级目录查找。

false:

//引入psr-0目录关系
$map = require __DIR__ . '/autoload_namespaces.php';
foreach ($map as $namespace => $path) {
    $loader->set($namespace, $path);
}

//引入psr-4目录关系
$map = require __DIR__ . '/autoload_psr4.php';
foreach ($map as $namespace => $path) {
    $loader->setPsr4($namespace, $path);

}

//引入classmap目录关系
$classMap = require __DIR__ . '/autoload_classmap.php';
if ($classMap) {
    $loader->addClassMap($classMap);
}

目前来说false几率很低,而且本质上也一样,所以这部分细节不展开。

true:

//导入autoload_static.php 文件。里面包含了psr-0、psr-4、classmap、files四种映射
require_once __DIR__ . '/autoload_static.php';

class ComposerStaticInit9dd8e0b0864cf05cbb09cc2bda0092ef
{
    public static $files = array (
        'fa210f1e33fcb32837b5896ada32b141' => __DIR__ . '/../..' . '/common/Utils.php',
    );
    public static $prefixLengthsPsr4 = array (
        'o' =>
        array (
            'one\\name\\' => 9,
        ),
        'P' =>
        array (
            'Psr\\Log\\' => 8,
        ),
        'M' =>
        array (
            'Monolog\\' => 8,
        ),
    );
    public static $prefixDirsPsr4 = array (
        'one\\name\\' =>
        array (
            0 => __DIR__ . '/..' . '/src',
        ),
        'Psr\\Log\\' =>
        array (
            0 => __DIR__ . '/..' . '/psr/log/Psr/Log',
        ),
        'Monolog\\' =>
        array (
            0 => __DIR__ . '/..' . '/monolog/monolog/src/Monolog',
        ),
    );
    public static $prefixesPsr0 = array (
        't' =>
        array (
            'two\\name\\' =>
            array (
                0 => __DIR__ . '/..' . '/src',
            ),
        ),
    );
    public static $classMap = array (
        'Person' => __DIR__ . '/../..' . '/src/Person.php',
    );
    public static function getInitializer(ClassLoader $loader)
    {
        return \Closure::bind(function () use ($loader) {
            $loader->prefixLengthsPsr4 = ComposerStaticInit9dd8e0b0864cf05cbb09cc2bda0092ef::$prefixLengthsPsr4;
            $loader->prefixDirsPsr4 = ComposerStaticInit9dd8e0b0864cf05cbb09cc2bda0092ef::$prefixDirsPsr4;
            $loader->prefixesPsr0 = ComposerStaticInit9dd8e0b0864cf05cbb09cc2bda0092ef::$prefixesPsr0;
            $loader->classMap = ComposerStaticInit9dd8e0b0864cf05cbb09cc2bda0092ef::$classMap;
        }, null, ClassLoader::class);
    }
}

先来看看类的详细内容,

五个静态属性:$files、$prefixLengthsPsr4、$prefixDirsPsr4、$prefixesPsr0、$classMap

一个静态方法:getInitializer()

$files对应./common目录下的所有文件

$prefixLengthsPsr4、$prefixDirsPsr4对应psr-4

$prefixesPsr0对应psr-0

$classMap对应./src/Person.php

getInitializer()返回了一个Closure对象

这块我觉得特别有意思,专门了解了下闭包函数和Closure::bind的作用,从想要达到的效果来看,把autoload_static.php内的静态属性,赋值到ClassLoader实例的私有属性。

回到autoload_real.php,

//执行getInitializer()静态方法,返回一个闭包函数,再执行
//作用是将autoload_static.php 内的四种映射 复制到 ClassLoader类内
//private变量无法直接赋值,使用Closure::bind()方法,改变类作用域,这个就是闭包函数的作用
//通过call_user_func()函数执行闭包函数,对映射关系进行处理    
call_user_func(\Composer\Autoload\ComposerStaticInit9dd8e0b0864cf05cbb09cc2bda0092ef::getInitializer($loader));
$loader->register(true);

=>

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

注册一个autoload调用方法,这就是后续最最关键的自动加载入口。

 

有没有发现上面的两种方式,都只引用了classmap、psr-0、psr-4,files都没操作过,实现过程就在最后一段逻辑里。

$files数组的键是一种不知名密文,对应的值为文件路径(包含文件名),直接使用了一个for循环,逐个require,同时设置了$GLOBAL变量,以键作为新键,标记$files内的文件是否载入。

也就是说,定义在$files里的文件,会在初始化阶段全部require,另外三种类似请求-再响应加载的方式。

再来看看ClassLoad内的加载逻辑方法:findFile($class) 

// class map lookup
if (isset($this->classMap[$class])) {
    //'Person' => __DIR__ . '/../..' . '/src/Person.php'
    //classMap可能的格式如上,比如查找Person类,会判断是否包含,若存在则直接返回文件路径(包含文件名)
    return $this->classMap[$class];
}

//判断apcu是否有使用
if (null !== $this->apcuPrefix) {
    //获取缓存数据
    $file = apcu_fetch($this->apcuPrefix.$class, $hit);
    //成功获取路径名
    if ($hit) {
        return $file;
    }
}

//查找类文件 后缀为.php
$file = $this->findFileWithExtension($class, '.php');
//TODO 后面展开

//前面未找到apcu缓存且apcu已启动,则写入对应文件路径名
if (null !== $this->apcuPrefix) {
    apcu_add($this->apcuPrefix.$class, $file);
}

//文件加载失败,设置missingClasses全局变量,表示文件不存在
if (false === $file) {
    // Remember that this class does not exist.
    $this->missingClasses[$class] = true;
}

//返回加载结果
return $file;

入口只用判断返回的数据类型,若为false,则未查找成功;否则进行include操作。这里也有点小讲究:

根据psr自动加载的规范,自动加载器(autoloader)的实现一定不可抛出异常、一定不可触发任一级别的错误信息以及不应该有返回值,所以使用include,保证不会报错。

 

findFileWithExtension()内实现了psr-0和psr-4具体的加载逻辑,源码逻辑上注释我觉得是很好的学习方式;

private function findFileWithExtension($class, $ext){
    // PSR-4 lookup
    //将\替换成 系统分隔符
    $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext;
    //例:class = one\name\Person
    $first = $class[0]; //first => o
    /*
    $prefixLengthsPsr4 = array (
        'o' =>
            array (
                'one\\name\\' => 9
            )
    );
    * */
    if (isset($this->prefixLengthsPsr4[$first])) {  //查找键名为o的数组
        //1、传入字符串 判断是否存在\字符
        //2、舍去最后一个\后面及\的内容
        //3、重新在后面添加一个\
        //4、判断prefixDirsPsr4是否存在3生成的字符串键
        //5、prefixDirsPsr4对应的值加上分隔符+类名,判断是否存在,存在则返回完整文件路径
        //6、重要!保证查找前 文件路径的分隔符全部用 DIRECTORY_SEPARATOR 替换,做到不同系统的兼容性
        $subPath = $class; //one\name\Person
        //判断是受存在\字符,获取\最后一次出现的位置,下标
        while (false !== $lastPos = strrpos($subPath, '\\')) {
            //切割获取最后一个\前面部分内容,也就是去掉类名,完整的命名空间
            $subPath = substr($subPath, 0, $lastPos);   //one\name
            $search = $subPath.'\\';    //one\name\
            //通过prefixLengthsPsr4数组的处理,获取有效的命名空间信息
            /*
            $prefixDirsPsr4 = array (
                'one\\name\\' =>
                    array (
                        0 => __DIR__ . '/..' . '/src'
                    )
            );
            */
            if (isset($this->prefixDirsPsr4[$search])) {
                //拼接分隔符 + 类名 + 后缀 ==>  /Person.php
                $pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1);
                //拼接映射路径 + 类名 + 后缀 ==> __DIR__ . '/..' . '/src' . '/Perso.php'
                foreach ($this->prefixDirsPsr4[$search] as $dir) {
                   //判断文件是否存件
                   if (file_exists($file = $dir . $pathEnd)) {
                       return $file;
                   }
                }
             }
        }
   }
   // PSR-4 fallback dirs
   //$useStaticLoader === false fallbackDirsPsr4可能会被赋值
   foreach ($this->fallbackDirsPsr4 as $dir) {
       //查找fallbackDirsPsr4内的包含目录
       if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) {
           return $file;
       }
   }
   // PSR-0 lookup
   if (false !== $pos = strrpos($class, '\\')) {
       //logicalPathPsr4 = two\\name\\Person.php
       // namespaced class name
       //logicalPathPsr0 = two\\name\\Person.php
       //拼接好的字符串为:two\\name\\Person.php
       logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1)
                . strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR);
    } else {
       // PEAR-like class name
       //将_替换成分隔符
       $logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext;
    }
    /*
    $prefixesPsr0 = array (
       't' =>
            array (
                'two\\name\\' =>
                array (
                    0 => __DIR__ . '/..' . '/src',
                ),
            ),
        );
    * */
    if (isset($this->prefixesPsr0[$first])) {   //判断是否存在t键名
       //轮询t键对应的值
       foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) {
           //prefix ==> two\\name\\
           //dirs ==> 0
           if (0 === strpos($class, $prefix)) {
               //一个命令空间可以有多个目录,轮询查找是否存在该文件
               foreach ($dirs as $dir) {
                   //dir ==> __DIR__.\\..\\src
                   //logicalPathPsr0 ==> two\\name\\Person.php
                   //这个和psr-4的区别很明显
                   //logicalPathPsr0 保留two\\name\\
                   //logicalPathPsr4 不保留one\\name\\
                   //判断 __DIR__.\\..\\src\ + two\\name\\Person.php 文件是否存在
                   if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
                       return $file;
                    }
               }
            }
        }
    }
    // PSR-0 fallback dirs
    //$useStaticLoader === false fallbackDirsPsr0可能会被赋值
    foreach ($this->fallbackDirsPsr0 as $dir) {
        if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
            return $file;
        }
    }
}

 

总结来说

psr-4定义:"one\\name\\": "./vendor/src/"

psr-0定义:"two\\name\\": "./vendor/src/"

psr-4查找的路径为:./vendor/src/xx.php

psr-0查找的路径为:./vendor/src/two/name/xx.php

当然细节还有很多,这时候回去看看文档,也就看得懂了。

 

最后附上简单的运行代码和结果

require 'vendor/autoload.php';

//psr-0
$person0 = new \two\name\Person();
echo $person0->execute()."
"; //psr-4 $person4 = new \one\name\Person(); echo $person4->execute()."
"; //classmap $person = new Person(); echo $person->execute()."
"; //files echo Utils::execute();

你可能感兴趣的:(PHP)