以下以 Monolog 库为例,来分析 composer 的自动加载过程。

文件结构

composer.json

{
    "name": "yang/composerdemo",
    "description": "just a demo",
    "type": "project",
    "require": {
        "monolog/monolog": "^2.1"
    }
}

index.php

<?php
require 'vendor/autoload.php';

$log=new Monolog\Logger('my_logger');
var_dump($log);

过程分析

autoload.php

<?php

// autoload.php @generated by Composer

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

return ComposerAutoloaderInit083a2e14f8ef3f2d5fd56431cab35edd::getLoader();

autoload.php 文件非常简单,它加载了 autoload_real.php 这个文件,然后调用了里面的 ComposerAutoloaderInit083a2e14f8ef3f2d5fd56431cab35edd::getLoader() 方法。

autoload_real.php

<?php

// autoload_real.php @generated by Composer

class ComposerAutoloaderInit083a2e14f8ef3f2d5fd56431cab35edd
{
    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('ComposerAutoloaderInit083a2e14f8ef3f2d5fd56431cab35edd', 'loadClassLoader'), true, true);
        self::$loader = $loader = new \Composer\Autoload\ClassLoader();
        spl_autoload_unregister(array('ComposerAutoloaderInit083a2e14f8ef3f2d5fd56431cab35edd', 'loadClassLoader'));

        $useStaticLoader = PHP_VERSION_ID >= 50600 && !defined('HHVM_VERSION') && (!function_exists('zend_loader_file_encoded') || !zend_loader_file_encoded());
        if ($useStaticLoader) {
            require_once __DIR__ . '/autoload_static.php';

            call_user_func(\Composer\Autoload\ComposerStaticInit083a2e14f8ef3f2d5fd56431cab35edd::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);
            }
        }

        $loader->register(true);

        if ($useStaticLoader) {
            $includeFiles = Composer\Autoload\ComposerStaticInit083a2e14f8ef3f2d5fd56431cab35edd::$files;
        } else {
            $includeFiles = require __DIR__ . '/autoload_files.php';
        }
        foreach ($includeFiles as $fileIdentifier => $file) {
            composerRequire083a2e14f8ef3f2d5fd56431cab35edd($fileIdentifier, $file);
        }

        return $loader;
    }
}

function composerRequire083a2e14f8ef3f2d5fd56431cab35edd($fileIdentifier, $file)
{
    if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) {
        require $file;

        $GLOBALS['__composer_autoload_files'][$fileIdentifier] = true;
    }
}

这个文件包含了一个类:ComposerAutoloaderInit083a2e14f8ef3f2d5fd56431cab35edd 和一个函数 composerRequire083a2e14f8ef3f2d5fd56431cab35edd($fileIdentifier, $file)

类中有一个私有变量 $loader

getLoader 分析

 if (null !== self::$loader) {
            return self::$loader;
  }

这一个是单例模式的代码。确保系统中只有一个loader

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

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

这几行代码实现了ClassLoader 这个类的自动加载,并且把这个类赋值给 $loader 这个变量。getLoader 返回的就是这个变量。

   $useStaticLoader = PHP_VERSION_ID >= 50600 && !defined('HHVM_VERSION') && (!function_exists('zend_loader_file_encoded') || !zend_loader_file_encoded());
        if ($useStaticLoader) {
            require_once __DIR__ . '/autoload_static.php';

            call_user_func(\Composer\Autoload\ComposerStaticInit083a2e14f8ef3f2d5fd56431cab35edd::getInitializer($loader));
        }

在php版本大于5.6,并且不是 HHVM 环境,并且没有用 ZendLoader 模块的时候,加载 autoload_static.php 这个文件,并且调用里面的 getInitializer 方法。

不是上述环境的话,执行以下代码:

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);
            }
        }

先看一下上面涉及的四个文件是做什么的:

autoload_static.php

<?php

// autoload_static.php @generated by Composer

namespace Composer\Autoload;

class ComposerStaticInitab0099410c243afa0a9667f1535990c8
{
    public static $prefixLengthsPsr4 = array (
        'P' => 
        array (
            'Psr\\Log\\' => 8,
        ),
        'M' => 
        array (
            'Monolog\\' => 8,
        ),
    );

    public static $prefixDirsPsr4 = array (
        'Psr\\Log\\' => 
        array (
            0 => __DIR__ . '/..' . '/psr/log/Psr/Log',
        ),
        'Monolog\\' => 
        array (
            0 => __DIR__ . '/..' . '/monolog/monolog/src/Monolog',
        ),
    );

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

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

本项目只涉及到psr-4 模式的加载,所以有两个涉及psr-4 的变量,实际这里还会有 filespsr-0classMap 模式的变量。格式如下:

  public static $files = array (
        '03f28fe5dff09936e2b16239db4b2894' => __DIR__ . '/../..' . '/test.php',
    );
 public static $prefixesPsr0 = array (
        'a' => 
        array (
            'app2\\' => 
            array (
                0 => __DIR__ . '/../..' . '/psr1',
            ),
        ),
    );

    public static $classMap = array (
        'AA\\A' => __DIR__ . '/../..' . '/classmap/Price.php',
        'BB\\B' => __DIR__ . '/../..' . '/classmap/Price.php',
        'Brand' => __DIR__ . '/../..' . '/classmap/sub/Brand.php',
        'Config' => __DIR__ . '/../..' . '/config.php',
        'Goods' => __DIR__ . '/../..' . '/classmap/Goods.php',
        'GoodsUnit' => __DIR__ . '/../..' . '/classmap/GoodsUnit.php',
        'No' => __DIR__ . '/../..' . '/classmap/noclass.php',
    );

最后那个getInitializer 方法作用是把 psr-4的命名空间长度、路径,psr0的路径,classmap设置的路径绑定到loader里面。

autoload_psr4.php

<?php

// autoload_psr4.php @generated by Composer

$vendorDir = dirname(dirname(__FILE__));
$baseDir = dirname($vendorDir);

return array(
    'Psr\\Log\\' => array($vendorDir . '/psr/log/Psr/Log'),
    'Monolog\\' => array($vendorDir . '/monolog/monolog/src/Monolog'),
);

这个文件定义的是psr-4模式命名空间对应的物理路径。其实就是 autoload_static.php 中的 $prefixDirsPsr4 变量。

总结起来看,两种环境最终设置了 ClassLoader 中的 这些变量:

    // PSR-4
    private $prefixLengthsPsr4 = array(); //命名空间长度
    private $prefixDirsPsr4 = array(); //命名空间对应的物理路径
    // PSR-0
    private $prefixesPsr0 = array(); //psr0命名空间对应的物理路径
    private $classMap = array(); //设置的classmap 对应的物理路径

接下来就执行了这行代码

  $loader->register(true);

也就是

ClassLoader.php

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

从这里可以看出,composer的自动加载最终调用的是这里的 loadClass

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

            return true;
        }
    }

loadClass 执行的是根据类名,找到这个类对应的文件,然后加载这个文件。

  public function findFile($class)
    {
        // class map lookup
        if (isset($this->classMap[$class])) {
            return $this->classMap[$class]; //classmap中有这个类的话,直接返回classmap对应的结果
        }
        if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) {
            return false; // 已经明确添加到类缺失数组的,直接返回失败
        }
        if (null !== $this->apcuPrefix) {
            $file = apcu_fetch($this->apcuPrefix.$class, $hit);
            if ($hit) {
                return $file; //apcu缓存里面有的,直接从缓存返回
            }
        }
         //上述步骤没找到,就执行下面的步骤,也就是按psr4,psr0模式里面找
        $file = $this->findFileWithExtension($class, '.php');

        // Search for Hack files if we are running on HHVM
        if (false === $file && defined('HHVM_VERSION')) {
            $file = $this->findFileWithExtension($class, '.hh');
        }

        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;
    }
   private function findFileWithExtension($class, $ext)
    {
        // PSR-4 lookup
        $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext;
       // Monolog\Logger.php     Monolog\Logger                       .php  
        $first = $class[0];
       // M               
        if (isset($this->prefixLengthsPsr4[$first])) {
        //  ['Monolog\\'=>8]
            $subPath = $class;
            //Monolog\Logger
            while (false !== $lastPos = strrpos($subPath, '\\')) {
                // $lastPos=7
                $subPath = substr($subPath, 0, $lastPos);
                // $subPath=
                $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;
            }
        }

        // PSR-0 lookup
        if (false !== $pos = strrpos($class, '\\')) {
            // namespaced class name
            $logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1)
                . strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR);
        } else {
            // PEAR-like class name
            $logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext;
        }

        if (isset($this->prefixesPsr0[$first])) {
            foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) {
                if (0 === strpos($class, $prefix)) {
                    foreach ($dirs as $dir) {
                        if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
                            return $file;
                        }
                    }
                }
            }
        }

        // PSR-0 fallback dirs
        foreach ($this->fallbackDirsPsr0 as $dir) {
            if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
                return $file;
            }
        }

        // PSR-0 include paths.
        if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) {
            return $file;
        }

        return false;
    }

findFileWithExtension($class, $ext) 分析

//传入参数 
$class="Monolog\Logger";
$ext=".php";
$logicalPathPsr4="Monolog\Logger.php";
$first="M";
$this->prefixLengthsPsr4[$first]=8;
$subPath="Monolog\Logger";
//while循环
$lastPos=7;   // 最后一个斜线的位置,这个是分割命名空间和类的

$subPath=substr($subPath, 0, $lastPos)="Monolog";
$search="Monolog\";
$pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1);
$pathEnd="\Logger.php";
$dir=$this->prefixDirsPsr4[$search];
$dir="C:\code\composer\vendor\composer/../monolog/monolog/src/Monolog";
$file=$dir.$pathEnd;
$file="C:\code\composer\vendor\composer/../monolog/monolog/src/Monolog\Logger.php";
//找到这个文件了
//如果这一步没找到,就到fallback目录里面找。
//fallback目录拼接上$logicalPathPsr4="Monolog\Logger.php" 看有没有这个文件
//有的话就返回,还是没有的话再按psr-0的方式找
$pos = strrpos($class, '\\');
$pos=7  //这是找最后一个斜线
 $logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1)
                . strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR);
$logicalPathPsr4="Monolog\Logger.php";
$logicalPathPsr0="Monolog\Logger.php";
$this->prefixesPsr0[$first] as $prefix => $dirs
// 把命名空间首字母相同的,命名空间和物理路径的映射关系循环一下
  if (isset($this->prefixesPsr0[$first])) {
            foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) {
                if (0 === strpos($class, $prefix)) {
                    foreach ($dirs as $dir) {
                        if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
                            return $file;
                        }
                    }
                }
            }
        }

$file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0
//$dir 是命名空间对应的物理路径
$logicalPathPsr0=substr($logicalPathPsr4, 0, $pos + 1)
                . strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR);
//最后一个斜线前和最后一个斜线后的部分拼接起来  也就是namespace  和 class拼接,
//并且class里面有下横线的话,下横线要转换成目录分隔符
//最后的文件是命名空间对应的目录和命名空间类拼接起来,
//和psr4比较起来,前面多拼接了一次物理路径并且class里面的_转换了一下

//这次还是没找到的话,就到psr0的fallback目录里面找,还是把物理路径和整体拼接
// PSR-0 fallback dirs
        foreach ($this->fallbackDirsPsr0 as $dir) {
            if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
                return $file;
            }
        }
        
function includeFile($file)
{
    include $file;
}

classmap,psr4,psr0 之后

    $loader->register(true);

        if ($useStaticLoader) {
            $includeFiles = Composer\Autoload\ComposerStaticInit083a2e14f8ef3f2d5fd56431cab35edd::$files;
        } else {
            $includeFiles = require __DIR__ . '/autoload_files.php';
        }
        foreach ($includeFiles as $fileIdentifier => $file) {
            composerRequire083a2e14f8ef3f2d5fd56431cab35edd($fileIdentifier, $file);
        }

autoload里面直接设置的files加载进来。

总结

文件查找顺序: classmap,psr4,psr0,files

前面三个都是按需加载的,files在生成loader的最后加载进来,如果里面有可执行的语句会直接执行。


本文由 yang 创作,采用 知识共享署名 3.0,可自由转载、引用,但需署名作者且注明文章出处。

楼主残忍的关闭了评论