目錄
前言
大綱
一、PHP 自動載入功能
PHP 自動載入功能的由來
PHP 自動載入函數__autoload()
__autoload() 函数存在的问题
SPL Autoload
二、PSR 规范
PSR4 标准
1)一个完整的类名需具有以下结构:
2)根据完整的类名载入相应的文件
3) 例子
Composer自动加载过程
Composer 做了哪些事情
执行 composer require 时发生了什么
Composer 源码分析
启动
Composer自动加载文件
autoload_real 引导类
第一部分——单例
第二部分——构造ClassLoader核心类
第三部分 —— 初始化核心类对象
autoload_static 静态初始化 ( PHP >= 5.6 )
classMap(命名空间映射)
PSR4 标准顶级命名空间映射数组:
ClassLoader 接口初始化( PHP < 5.6 )
PSR4 标准的映射
命名空间映射
第四部分 —— 注册
全局函数的自动加载
静态初始化:
普通初始化
加载全局函数
第五部分 —— 运行
最后小结
首頁 開發工具 composer 解析composer的自動載入原理

解析composer的自動載入原理

Apr 21, 2021 am 11:41 AM
composer php 原始碼分析

下面由composer教學欄位給大家深入解析 composer 的自動載入原理,希望對需要的朋友有幫助!

前言

PHP 自5.3的版本後,已經重煥新生,命名空間、性狀(trait)、閉包、介面、PSR 規範、以及composer 的出現已經讓PHP 變成了一門現代化的腳本語言。 PHP 的生態系統也一直在演進,而 composer 的出現更是徹底的改變了以往建構 PHP 應用的方式,我們可以根據 PHP 的應用需求混合搭配最合適的 PHP 元件。當然這也得益於 PSR 規範的提出。


大綱

  • PHP 自動載入功能
  • #PSR 規格
  • comoposer 的自動載入程序
  • #composer 原始碼分析

一、PHP 自動載入功能

PHP 自動載入功能的由來

在PHP 開發過程中,如果希望從外部引入一個Class ,通常會使用includerequire 方法,去把定義這個Class 的檔案包含進來。這個在小規模開發的時候,沒什麼大問題。但在大型的開發專案中,使用這種方式會帶來一些隱含的問題:如果一個PHP 檔案需要使用很多其它類,那麼就需要很多的require/include 語句,這樣有可能會造成遺漏包含進不必要的類別檔案。如果大量的文件都需要使用其它的類,那麼要保證每個文件都包含正確的類文件肯定是一個噩夢, 況且 require或 incloud 的性能代價很大。

PHP5 為這個問題提供了一個解決方案,這就是 類別的自動載入(autoload)機制autoload機制 可以使得PHP 程式有可能在使用類別時才自動包含類別文件,而不是一開始就將所有的類別文件include進來,這種機制也稱為Lazy loading (惰性載入)

  • 總結起來,自動載入功能帶來了幾處優點:

    1. #使用類別之前不需要include / require
    2. 使用類別的時候才會include / require 文件,實作了lazy loading ,避免了include / require 多餘檔案。
    3. 無需考慮引入 類別的實際磁碟位址 ,實現了邏輯和實體檔案的分離。

PHP 自動載入函數__autoload()

  • 從PHP5 開始,當我們在使用一個類別時,如果發現這個類別沒有加載,就會自動執行__autoload() 函數,這個函數是我們在程式中自訂的,在這個函數中我們可以載入需要使用的類別。以下是個簡單的範例:

    <?php
    
    function __autoload($classname) {
            require_once ($classname . ".class.php");
    }
    登入後複製
  • 在我們這個簡單的範例中,我們直接將類別名稱加上副檔名.class.php 構成了類別文件名,然後使用require_once 將其載入。

    從這個例子中,我們可以看出 __autoload 至少要做三件事:

    1. 根据类名确定类文件名;
    2. 确定类文件所在的磁盘路径;
    3. 将类从磁盘文件中加载到系统中。
  • 第三步最简单,只需要使用 include / require 即可。要实现第一步,第二步的功能,必须在开发时约定类名与磁盘文件的映射方法,只有这样我们才能根据类名找到它对应的磁盘文件。
  • 当有大量的类文件要包含的时候,我们只要确定相应的规则,然后在 __autoload() 函数中,将类名与实际的磁盘文件对应起来,就可以实现 lazy loading 的效果
  • 如果想详细的了解关于 autoload 自动加载的过程,可以查看手册资料:PHP autoload函数说明

__autoload() 函数存在的问题

  • 如果在一个系统的实现中,如果需要使用很多其它的类库,这些类库可能是由不同的开发人员编写的, 其类名与实际的磁盘文件的映射规则不尽相同。这时如果要实现类库文件的自动加载,就必须 在 __autoload() 函数中将所有的映射规则全部实现,这样的话 __autoload() 函数有可能会非常复杂,甚至无法实现。最后可能会导致 __autoload() 函数十分臃肿,这时即便能够实现,也会给将来的维护和系统效率带来很大的负面影响。
  • 那么问题出现在哪里呢?问题出现在 __autoload() 是全局函数只能定义一次 ,不够灵活,所以所有的类名与文件名对应的逻辑规则都要在一个函数里面实现,造成这个函数的臃肿。那么如何来解决这个问题呢?答案就是使用一个 __autoload调用堆栈 ,不同的映射关系写到不同的 __autoload函数 中去,然后统一注册统一管理,这个就是 PHP5 引入的 SPL Autoload

SPL Autoload

  • SPL是 Standard PHP Library(标准PHP库)的缩写。它是 PHP5 引入的一个扩展标准库,包括 spl autoload 相关的函数以及各种数据结构和迭代器的接口或类。spl autoload 相关的函数具体可见 php中spl_autoload
<?php

// __autoload 函数
//
// function __autoload($class) {
//     include &#39;classes/&#39; . $class . &#39;.class.php&#39;;
// }


function my_autoloader($class) {
    include &#39;classes/&#39; . $class . &#39;.class.php&#39;;
}

spl_autoload_register(&#39;my_autoloader&#39;);


// 定义的 autoload 函数在 class 里

// 静态方法
class MyClass {
  public static function autoload($className) {
    // ...
  }
}

spl_autoload_register(array(&#39;MyClass&#39;, &#39;autoload&#39;));

// 非静态方法
class MyClass {
  public function autoload($className) {
    // ...
  }
}

$instance = new MyClass();
spl_autoload_register(array($instance, &#39;autoload&#39;));
登入後複製

spl_autoload_register() 就是我们上面所说的__autoload调用堆栈,我们可以向这个函数注册多个我们自己的 autoload() 函数,当 PHP 找不到类名时,PHP就会调用这个堆栈,然后去调用自定义的 autoload() 函数,实现自动加载功能。如果我们不向这个函数输入任何参数,那么就会默认注册 spl_autoload() 函数。


二、PSR 规范

与自动加载相关的规范是 PSR4,在说 PSR4 之前先介绍一下 PSR 标准。PSR 标准的发明和推出组织是:PHP-FIG,它的网站是:www.php-fig.org。由几位开源框架的开发者成立于 2009 年,从那开始也选取了很多其他成员进来,虽然不是 “官方” 组织,但也代表了社区中不小的一块。组织的目的在于:以最低程度的限制,来统一各个项目的编码规范,避免各家自行发展的风格阻碍了程序员开发的困扰,于是大伙发明和总结了 PSR,PSR 是 PHP Standards Recommendation 的缩写,截止到目前为止,总共有 14 套 PSR 规范,其中有 7 套PSR规范已通过表决并推出使用,分别是:

PSR-0 自动加载标准(已废弃,一些旧的第三方库还有在使用)

PSR-1 基础编码标准

PSR-2 编码风格向导

PSR-3 日志接口

PSR-4 自动加载的增强版,替换掉了 PSR-0

PSR-6 缓存接口规范

PSR-7 HTTP 消息接口规范

具体详细的规范标准可以查看PHP 标准规范

PSR4 标准

2013 年底,PHP-FIG 推出了第 5 个规范——PSR-4。

PSR-4 规范了如何指定文件路径从而自动加载类定义,同时规范了自动加载文件的位置。

1)一个完整的类名需具有以下结构:

\<命名空间>\<子命名空间>\<类名></pre> <ul> <li>完整的类名<strong>必须</strong>要有一个顶级命名空间,被称为 "vendor namespace";</li> <li>完整的类名<strong>可以</strong>有一个或多个子命名空间;</li> <li>完整的类名<strong>必须</strong>有一个最终的类名;</li> <li>完整的类名中<strong>任意一部分</strong>中的下滑线都是没有特殊含义的;</li> <li>完整的类名<strong>可以</strong>由任意大小写字母组成;</li> <li>所有类名都<strong>必须</strong>是大小写敏感的。</li> </ul> <h4 id="根据完整的类名载入相应的文件">2)根据完整的类名载入相应的文件</h4> <ul> <li>完整的类名中,去掉最前面的命名空间分隔符,前面连续的一个或多个命名空间和子命名空间,作为「命名空间前缀」,其必须与至少一个「文件基目录」相对应;</li> <li>紧接命名空间前缀后的子命名空间 必须 与相应的「文件基目录」相匹配,其中的命名空间分隔符将作为目录分隔符。</li> <li>末尾的类名<strong>必须</strong>与对应的以 .php 为后缀的文件同名。</li> <li>自动加载器(autoloader)的实现<strong>一定不可</strong>抛出异常、<strong>一定不可</strong>触发任一级别的错误信息以及<strong>不应该</strong>有返回值。</li> </ul> <h4 id="例子">3) 例子</h4> <p>PSR-4风格</p> <blockquote>类名:ZendAbc    <br>命名空间前缀:Zend     <br>文件基目录:/usr/includes/Zend/    <br>文件路径:/usr/includes/Zend/Abc.php</blockquote> <blockquote>类名:SymfonyCoreRequest  <br>命名空间前缀:SymfonyCore      <br>文件基目录:./vendor/Symfony/Core/   <br>文件路径:./vendor/Symfony/Core/Request.php</blockquote> <p>目录结构</p> <div class="code" style="position:relative; padding:0px; margin:0px;"><pre class="brush:php;toolbar:false">-vendor/ | -vendor_name/ | | -package_name/ | | | -src/ | | | | -ClassName.php       # Vendor_Name\Package_Name\ClassName | | | -tests/ | | | | -ClassNameTest.php   # Vendor_Name\Package_Name\ClassNameTest</pre><div class="contentsignin">登入後複製</div></div> <h1 id="Composer自动加载过程">Composer自动加载过程</h1> <h4 id="Composer-做了哪些事情">Composer 做了哪些事情</h4> <ul> <li>你有一个项目依赖于若干个库。</li> <li>其中一些库依赖于其他库。</li> <li>你声明你所依赖的东西。</li> <li>Composer 会找出哪个版本的包需要安装,并安装它们(将它们下载到你的项目中)。</li> </ul> <p>例如,你正在创建一个项目,需要做一些单元测试。你决定使用 <code>phpunit 。为了将它添加到你的项目中,你所需要做的就是在 composer.json 文件里描述项目的依赖关系。

 {
   "require": {
     "phpunit/phpunit":"~6.0",
   }
 }
登入後複製

然后在 composer require 之后我们只要在项目里面直接 use phpunit 的类即可使用。

执行 composer require 时发生了什么

  • composer 会找到符合 PR4 规范的第三方库的源
  • 将其加载到 vendor 目录下
  • 初始化顶级域名的映射并写入到指定的文件里

(如:'PHPUnit\\Framework\\Assert' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/Assert.php'

  • 写好一个 autoload 函数,并且注册到 spl_autoload_register()里

题外话:现在很多框架都已经帮我们写好了顶级域名映射了,我们只需要在框架里面新建文件,在新建的文件中写好命名空间,就可以在任何地方 use 我们的命名空间了。


Composer 源码分析

下面我们通过对源码的分析来看看 composer 是如何实现 PSR4标准 的自动加载功能。

很多框架在初始化的时候都会引入 composer 来协助自动加载的,以 Laravel 为例,它入口文件 index.php 第一句就是利用 composer 来实现自动加载功能。

启动

<?php
  define(&#39;LARAVEL_START&#39;, microtime(true));

  require __DIR__ . &#39;/../vendor/autoload.php&#39;;
登入後複製

去 vendor 目录下的 autoload.php

<?php
  require_once __DIR__ . &#39;/composer&#39; . &#39;/autoload_real.php&#39;;

  return ComposerAutoloaderInit7b790917ce8899df9af8ed53631a1c29::getLoader();
登入後複製

这里就是 Composer 真正开始的地方了

Composer自动加载文件

首先,我们先大致了解一下Composer自动加载所用到的源文件。

  1. autoload_real.php: 自动加载功能的引导类。

    • composer 加载类的初始化(顶级命名空间与文件路径映射初始化)和注册(spl_autoload_register())。
  2. ClassLoader.php : composer 加载类。

    • composer 自动加载功能的核心类。
  3. autoload_static.php : 顶级命名空间初始化类,

    • 用于给核心类初始化顶级命名空间。
  4. autoload_classmap.php : 自动加载的最简单形式,

    • 有完整的命名空间和文件目录的映射;
  5. autoload_files.php : 用于加载全局函数的文件,

    • 存放各个全局函数所在的文件路径名;
  6. autoload_namespaces.php : 符合 PSR0 标准的自动加载文件,

    • 存放着顶级命名空间与文件的映射;
  7. autoload_psr4.php : 符合 PSR4 标准的自动加载文件,

    • 存放着顶级命名空间与文件的映射;

autoload_real 引导类


在 vendor 目录下的 autoload.php 文件中我们可以看出,程序主要调用了引导类的静态方法 getLoader() ,我们接着看看这个函数。

<?php
    public static function getLoader()
    {
      if (null !== self::$loader) {
          return self::$loader;
      }

      spl_autoload_register(
        array(&#39;ComposerAutoloaderInit7b790917ce8899df9af8ed53631a1c29&#39;, &#39;loadClassLoader&#39;), true, true
      );

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

      spl_autoload_unregister(
        array(&#39;ComposerAutoloaderInit7b790917ce8899df9af8ed53631a1c29&#39;, &#39;loadClassLoader&#39;)
      );

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

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

          call_user_func(
          \Composer\Autoload\ComposerStaticInit7b790917ce8899df9af8ed53631a1c29::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\ComposerStaticInit7b790917ce8899df9af8ed53631a1c29::$files;
      } else {
          $includeFiles = require __DIR__ . '/autoload_files.php';
      }

      foreach ($includeFiles as $fileIdentifier => $file) {
          composerRequire7b790917ce8899df9af8ed53631a1c29($fileIdentifier, $file);
      }

      return $loader;
    }
登入後複製

我把自动加载引导类分为 5 个部分。

第一部分——单例

第一部分很简单,就是个最经典的单例模式,自动加载类只能有一个。

<?php
  if (null !== self::$loader) {
      return self::$loader;
  }
登入後複製

第二部分——构造ClassLoader核心类

第二部分 new 一个自动加载的核心类对象。

<?php
  /***********************获得自动加载核心类对象********************/
  spl_autoload_register(
    array(&#39;ComposerAutoloaderInit7b790917ce8899df9af8ed53631a1c29&#39;, &#39;loadClassLoader&#39;), true, true
  );

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

  spl_autoload_unregister(
    array(&#39;ComposerAutoloaderInit7b790917ce8899df9af8ed53631a1c29&#39;, &#39;loadClassLoader&#39;)
  );
登入後複製

loadClassLoader()函数:

<?php
public static function loadClassLoader($class)
{
    if (&#39;Composer\Autoload\ClassLoader&#39; === $class) {
        require __DIR__ . &#39;/ClassLoader.php&#39;;
    }
}
登入後複製

从程序里面我们可以看出,composer 先向 PHP 自动加载机制注册了一个函数,这个函数 require 了 ClassLoader 文件。成功 new 出该文件中核心类 ClassLoader() 后,又销毁了该函数。

第三部分 —— 初始化核心类对象

<?php
  /***********************初始化自动加载核心类对象********************/
  $useStaticLoader = PHP_VERSION_ID >= 50600 && !defined('HHVM_VERSION');
  if ($useStaticLoader) {
     require_once __DIR__ . '/autoload_static.php';

     call_user_func(
       \Composer\Autoload\ComposerStaticInit7b790917ce8899df9af8ed53631a1c29::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);
      }
    }
登入後複製

这一部分就是对自动加载类的初始化,主要是给自动加载核心类初始化顶级命名空间映射。

初始化的方法有两种:

  1. 使用 autoload_static 进行静态初始化;
  2. 调用核心类接口初始化。
登入後複製

autoload_static 静态初始化 ( PHP >= 5.6 )

静态初始化只支持 PHP5.6 以上版本并且不支持 HHVM 虚拟机。我们深入 autoload_static.php 这个文件发现这个文件定义了一个用于静态初始化的类,名字叫 ComposerStaticInit7b790917ce8899df9af8ed53631a1c29,仍然为了避免冲突而加了 hash 值。这个类很简单:

<?php
  class ComposerStaticInit7b790917ce8899df9af8ed53631a1c29{
     public static $files = array(...);
     public static $prefixLengthsPsr4 = array(...);
     public static $prefixDirsPsr4 = array(...);
     public static $prefixesPsr0 = array(...);
     public static $classMap = array (...);

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

          $loader->prefixDirsPsr4
                          = ComposerStaticInit7b790917ce8899df9af8ed53631a1c29::$prefixDirsPsr4;

          $loader->prefixesPsr0
                          = ComposerStaticInit7b790917ce8899df9af8ed53631a1c29::$prefixesPsr0;

          $loader->classMap
                          = ComposerStaticInit7b790917ce8899df9af8ed53631a1c29::$classMap;

      }, null, ClassLoader::class);
  }
登入後複製

这个静态初始化类的核心就是 getInitializer() 函数,它将自己类中的顶级命名空间映射给了 ClassLoader 类。值得注意的是这个函数返回的是一个匿名函数,为什么呢?原因就是 ClassLoader类 中的 prefixLengthsPsr4prefixDirsPsr4等等变量都是 private的。利用匿名函数的绑定功能就可以将这些 private 变量赋给 ClassLoader 类 里的成员变量。

关于匿名函数的绑定功能。

接下来就是命名空间初始化的关键了。

classMap(命名空间映射)

<?php
  public static $classMap = array (
      &#39;App\\Console\\Kernel&#39;
              => __DIR__ . '/../..' . '/app/Console/Kernel.php',

      'App\\Exceptions\\Handler'
              => __DIR__ . '/../..' . '/app/Exceptions/Handler.php',

      'App\\Http\\Controllers\\Auth\\ForgotPasswordController'
              => __DIR__ . '/../..' . '/app/Http/Controllers/Auth/ForgotPasswordController.php',

      'App\\Http\\Controllers\\Auth\\LoginController'
              => __DIR__ . '/../..' . '/app/Http/Controllers/Auth/LoginController.php',

      'App\\Http\\Controllers\\Auth\\RegisterController'
              => __DIR__ . '/../..' . '/app/Http/Controllers/Auth/RegisterController.php',
  ...)
登入後複製

直接命名空间全名与目录的映射,简单粗暴,也导致这个数组相当的大。

PSR4 标准顶级命名空间映射数组:

<?php
  public static $prefixLengthsPsr4 = array(
      &#39;p&#39; => array (
        'phpDocumentor\\Reflection\\' => 25,
    ),
      'S' => array (
        'Symfony\\Polyfill\\Mbstring\\' => 26,
        'Symfony\\Component\\Yaml\\' => 23,
        'Symfony\\Component\\VarDumper\\' => 28,
        ...
    ),
  ...);

  public static $prefixDirsPsr4 = array (
      'phpDocumentor\\Reflection\\' => array (
        0 => __DIR__ . '/..' . '/phpdocumentor/reflection-common/src',
        1 => __DIR__ . '/..' . '/phpdocumentor/type-resolver/src',
        2 => __DIR__ . '/..' . '/phpdocumentor/reflection-docblock/src',
    ),
       'Symfony\\Polyfill\\Mbstring\\' => array (
        0 => __DIR__ . '/..' . '/symfony/polyfill-mbstring',
    ),
      'Symfony\\Component\\Yaml\\' => array (
        0 => __DIR__ . '/..' . '/symfony/yaml',
    ),
  ...)
登入後複製

PSR4 标准顶级命名空间映射用了两个数组,第一个是用命名空间第一个字母作为前缀索引,然后是 顶级命名空间,但是最终并不是文件路径,而是 顶级命名空间的长度。为什么呢?

因为 PSR4 标准是用顶级命名空间目录替换顶级命名空间,所以获得顶级命名空间的长度很重要。

具体说明这些数组的作用:

假如我们找 Symfony\Polyfill\Mbstring\example 这个命名空间,通过前缀索引和字符串匹配我们得到了

<?php
    &#39;Symfony\\Polyfill\\Mbstring\\&#39; => 26,
登入後複製

这条记录,键是顶级命名空间,值是命名空间的长度。拿到顶级命名空间后去 $prefixDirsPsr4数组 获取它的映射目录数组:(注意映射目录可能不止一条)

<?php
  &#39;Symfony\\Polyfill\\Mbstring\\&#39; => array (
              0 => __DIR__ . '/..' . '/symfony/polyfill-mbstring',
          )
登入後複製

然后我们就可以将命名空间 Symfony\\Polyfill\\Mbstring\\example 前26个字符替换成目录 __DIR__ . '/..' . '/symfony/polyfill-mbstring ,我们就得到了__DIR__ . '/..' . '/symfony/polyfill-mbstring/example.php,先验证磁盘上这个文件是否存在,如果不存在接着遍历。如果遍历后没有找到,则加载失败。

ClassLoader 接口初始化( PHP < 5.6 )


如果PHP版本低于 5.6 或者使用 HHVM 虚拟机环境,那么就要使用核心类的接口进行初始化。

<?php
    // PSR0 标准
    $map = require __DIR__ . &#39;/autoload_namespaces.php&#39;;
    foreach ($map as $namespace => $path) {
       $loader->set($namespace, $path);
    }

    // PSR4 标准
    $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);
    }<h4 id="PSR-标准的映射">PSR4 标准的映射</h4>
</h3>
<p>autoload_psr4.php 的顶级命名空间映射</p>
<pre class="brush:php;toolbar:false"><?php
    return array(
    &#39;XdgBaseDir\\&#39;
        => array($vendorDir . '/dnoegel/php-xdg-base-dir/src'),

    'Webmozart\\Assert\\'
        => array($vendorDir . '/webmozart/assert/src'),

    'TijsVerkoyen\\CssToInlineStyles\\'
        => array($vendorDir . '/tijsverkoyen/css-to-inline-styles/src'),

    'Tests\\'
        => array($baseDir . '/tests'),

    'Symfony\\Polyfill\\Mbstring\\'
        => array($vendorDir . '/symfony/polyfill-mbstring'),
    ...
    )
登入後複製

PSR4 标准的初始化接口:

<?php
    public function setPsr4($prefix, $paths)
    {
        if (!$prefix) {
            $this->fallbackDirsPsr4 = (array) $paths;
        } else {
            $length = strlen($prefix);
            if ('\\' !== $prefix[$length - 1]) {
                throw new \InvalidArgumentException(
                  "A non-empty PSR-4 prefix must end with a namespace separator."
                );
            }
            $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
            $this->prefixDirsPsr4[$prefix] = (array) $paths;
        }
    }
登入後複製

总结下上面的顶级命名空间映射过程:

( 前缀 -> 顶级命名空间,顶级命名空间 -> 顶级命名空间长度 )
( 顶级命名空间 -> 目录 )
登入後複製

这两个映射数组。具体形式也可以查看下面的 autoload_static 的 $prefixLengthsPsr4 、 $prefixDirsPsr4 。

命名空间映射

autoload_classmap:

<?php
public static $classMap = array (
    &#39;App\\Console\\Kernel&#39;
        => __DIR__ . '/../..' . '/app/Console/Kernel.php',

    'App\\Exceptions\\Handler'
        => __DIR__ . '/../..' . '/app/Exceptions/Handler.php',
    ...
)
登入後複製

addClassMap:

<?php
    public function addClassMap(array $classMap)
    {
        if ($this->classMap) {
            $this->classMap = array_merge($this->classMap, $classMap);
        } else {
            $this->classMap = $classMap;
        }
    }
登入後複製

自动加载核心类 ClassLoader 的静态初始化到这里就完成了!

其实说是5部分,真正重要的就两部分——初始化与注册。初始化负责顶层命名空间的目录映射,注册负责实现顶层以下的命名空间映射规则。

第四部分 —— 注册


讲完了 Composer 自动加载功能的启动与初始化,经过启动与初始化,自动加载核心类对象已经获得了顶级命名空间与相应目录的映射,也就是说,如果有命名空间 'App\Console\Kernel,我们已经可以找到它对应的类文件所在位置。那么,它是什么时候被触发去找的呢?

这就是 composer 自动加载的核心了,我们先回顾一下自动加载引导类:

 public static function getLoader()
 {
    /***************************经典单例模式********************/
    if (null !== self::$loader) {
        return self::$loader;
    }
    
    /***********************获得自动加载核心类对象********************/
    spl_autoload_register(array('ComposerAutoloaderInit
    7b790917ce8899df9af8ed53631a1c29', 'loadClassLoader'), true, true);
    
    self::$loader = $loader = new \Composer\Autoload\ClassLoader();
    
    spl_autoload_unregister(array('ComposerAutoloaderInit
    7b790917ce8899df9af8ed53631a1c29', 'loadClassLoader'));

    /***********************初始化自动加载核心类对象********************/
    $useStaticLoader = PHP_VERSION_ID >= 50600 && 
    !defined('HHVM_VERSION');
    
    if ($useStaticLoader) {
        require_once __DIR__ . '/autoload_static.php';

        call_user_func(\Composer\Autoload\ComposerStaticInit
        7b790917ce8899df9af8ed53631a1c29::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\ComposerStaticInit
        7b790917ce8899df9af8ed53631a1c29::$files;
    } else {
        $includeFiles = require __DIR__ . '/autoload_files.php';
    }
    
    foreach ($includeFiles as $fileIdentifier => $file) {
        composerRequire
        7b790917ce8899df9af8ed53631a1c29($fileIdentifier, $file);
    }

    return $loader;
}
登入後複製

现在我们开始引导类的第四部分:注册自动加载核心类对象。我们来看看核心类的 register() 函数:

public function register($prepend = false)
{
    spl_autoload_register(array($this, 'loadClass'), true, $prepend);
}
登入後複製

其实奥秘都在自动加载核心类 ClassLoader 的 loadClass() 函数上:

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

            return true;
        }
    }
登入後複製

这个函数负责按照 PSR 标准将顶层命名空间以下的内容转为对应的目录,也就是上面所说的将  'App\Console\Kernel 中' Console\Kernel 这一段转为目录,至于怎么转的在下面 “运行”的部分讲。核心类 ClassLoader 将 loadClass() 函数注册到PHP SPL中的 spl_autoload_register() 里面去。这样,每当PHP遇到一个不认识的命名空间的时候,PHP会自动调用注册到 spl_autoload_register 里面的 loadClass() 函数,然后找到命名空间对应的文件。

全局函数的自动加载

Composer 不止可以自动加载命名空间,还可以加载全局函数。怎么实现的呢?把全局函数写到特定的文件里面去,在程序运行前挨个 require就行了。这个就是 composer 自动加载的第五步,加载全局函数。

if ($useStaticLoader) {
    $includeFiles = Composer\Autoload\ComposerStaticInit7b790917ce8899df9af8ed53631a1c29::$files;
} else {
    $includeFiles = require __DIR__ . '/autoload_files.php';
}
foreach ($includeFiles as $fileIdentifier => $file) {
    composerRequire7b790917ce8899df9af8ed53631a1c29($fileIdentifier, $file);
}
登入後複製

跟核心类的初始化一样,全局函数自动加载也分为两种:静态初始化和普通初始化,静态加载只支持PHP5.6以上并且不支持HHVM。

静态初始化:

ComposerStaticInit7b790917ce8899df9af8ed53631a1c29::$files:

public static $files = array (
'0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => __DIR__ . '/..' . '/symfony/polyfill-mbstring/bootstrap.php',
'667aeda72477189d0494fecd327c3641' => __DIR__ . '/..' . '/symfony/var-dumper/Resources/functions/dump.php',
...
);
登入後複製

普通初始化

autoload_files:

$vendorDir = dirname(dirname(__FILE__));
$baseDir = dirname($vendorDir);
    
return array(
'0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => $vendorDir . '/symfony/polyfill-mbstring/bootstrap.php',
'667aeda72477189d0494fecd327c3641' => $vendorDir . '/symfony/var-dumper/Resources/functions/dump.php',
   ....
);
登入後複製

其实跟静态初始化区别不大。

加载全局函数

class ComposerAutoloaderInit7b790917ce8899df9af8ed53631a1c29{
  public static function getLoader(){
      ...
      foreach ($includeFiles as $fileIdentifier => $file) {
        composerRequire7b790917ce8899df9af8ed53631a1c29($fileIdentifier, $file);
      }
      ...
  }
}

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

        $GLOBALS['__composer_autoload_files'][$fileIdentifier] = true;
    }
}
登入後複製

第五部分 —— 运行

到这里,终于来到了核心的核心—— composer 自动加载的真相,命名空间如何通过 composer 转为对应目录文件的奥秘就在这一章。
前面说过,ClassLoader 的 register() 函数将 loadClass() 函数注册到 PHP 的 SPL 函数堆栈中,每当 PHP 遇到不认识的命名空间时就会调用函数堆栈的每个函数,直到加载命名空间成功。所以 loadClass() 函数就是自动加载的关键了。

看下 loadClass() 函数:

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

        return true;
    }
}

public function findFile($class)
{
    // work around for PHP 5.3.0 - 5.3.2 https://bugs.php.net/50731
    if ('\\' == $class[0]) {
        $class = substr($class, 1);
    }

    // class map lookup
    if (isset($this->classMap[$class])) {
        return $this->classMap[$class];
    }
    if ($this->classMapAuthoritative) {
        return false;
    }

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

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

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

    return $file;
}
登入後複製

我们看到 loadClass() ,主要调用 findFile() 函数。findFile() 在解析命名空间的时候主要分为两部分:classMap 和 findFileWithExtension() 函数。classMap 很简单,直接看命名空间是否在映射数组中即可。麻烦的是 findFileWithExtension() 函数,这个函数包含了 PSR0 和 PSR4 标准的实现。还有个值得我们注意的是查找路径成功后 includeFile() 仍然是外面的函数,并不是 ClassLoader 的成员函数,原理跟上面一样,防止有用户写 $this 或 self。还有就是如果命名空间是以\开头的,要去掉\然后再匹配。

看下 findFileWithExtension 函数:

private function findFileWithExtension($class, $ext)
{
    // PSR-4 lookup
    $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext;
    
    $first = $class[0];
    if (isset($this->prefixLengthsPsr4[$first])) {
        foreach ($this->prefixLengthsPsr4[$first] as $prefix => $length) {
            if (0 === strpos($class, $prefix)) {
                foreach ($this->prefixDirsPsr4[$prefix] as $dir) {
                    if (file_exists($file = $dir . DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $length))) {
                        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;
    }
}
登入後複製

最后小结

我们通过举例来说下上面代码的流程:

如果我们在代码中写下 new phpDocumentor\Reflection\Element(),PHP 会通过 SPL_autoload_register 调用 loadClass -> findFile -> findFileWithExtension。步骤如下:

  • 将 \ 转为文件分隔符/,加上后缀php,变成 $logicalPathPsr4, 即 phpDocumentor/Reflection//Element.php;
  • 利用命名空间第一个字母p作为前缀索引搜索 prefixLengthsPsr4 数组,查到下面这个数组:
        p' => 
            array (
                'phpDocumentor\\Reflection\\' => 25,
                'phpDocumentor\\Fake\\' => 19,
          )
登入後複製
  • 遍历这个数组,得到两个顶层命名空间 phpDocumentor\Reflection\ 和 phpDocumentor\Fake\
  • 在这个数组中查找 phpDocumentor\Reflection\Element,找出 phpDocumentor\Reflection\ 这个顶层命名空间并且长度为25。
  • 在prefixDirsPsr4 映射数组中得到phpDocumentor\Reflection\ 的目录映射为:
    'phpDocumentor\\Reflection\\' => 
        array (
            0 => __DIR__ . '/..' . '/phpdocumentor/reflection-common/src',
            1 => __DIR__ . '/..' . '/phpdocumentor/type-resolver/src',
            2 => __DIR__ . '/..' . '/phpdocumentor/reflection-docblock/src',
        ),
登入後複製
  • 遍历这个映射数组,得到三个目录映射;
  • 查看 “目录+文件分隔符//+substr($logicalPathPsr4, $length)”文件是否存在,存在即返回。这里就是
    '__DIR__/../phpdocumentor/reflection-common/src + substr(phpDocumentor/Reflection/Element.php,25)'
  • 如果失败,则利用 fallbackDirsPsr4 数组里面的目录继续判断是否存在文件

以上就是 composer 自动加载的原理解析!

以上是解析composer的自動載入原理的詳細內容。更多資訊請關注PHP中文網其他相關文章!

本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn

熱AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover

AI Clothes Remover

用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool

Undress AI Tool

免費脫衣圖片

Clothoff.io

Clothoff.io

AI脫衣器

Video Face Swap

Video Face Swap

使用我們完全免費的人工智慧換臉工具,輕鬆在任何影片中換臉!

熱工具

記事本++7.3.1

記事本++7.3.1

好用且免費的程式碼編輯器

SublimeText3漢化版

SublimeText3漢化版

中文版,非常好用

禪工作室 13.0.1

禪工作室 13.0.1

強大的PHP整合開發環境

Dreamweaver CS6

Dreamweaver CS6

視覺化網頁開發工具

SublimeText3 Mac版

SublimeText3 Mac版

神級程式碼編輯軟體(SublimeText3)

laravel入門實例 laravel入門實例 Apr 18, 2025 pm 12:45 PM

Laravel 是一款 PHP 框架,用於輕鬆構建 Web 應用程序。它提供一系列強大的功能,包括:安裝: 使用 Composer 全局安裝 Laravel CLI,並在項目目錄中創建應用程序。路由: 在 routes/web.php 中定義 URL 和處理函數之間的關係。視圖: 在 resources/views 中創建視圖以呈現應用程序的界面。數據庫集成: 提供與 MySQL 等數據庫的開箱即用集成,並使用遷移來創建和修改表。模型和控制器: 模型表示數據庫實體,控制器處理 HTTP 請求。

使用 Composer 解決推薦系統的困境:andres-montanez/recommendations-bundle 的實踐 使用 Composer 解決推薦系統的困境:andres-montanez/recommendations-bundle 的實踐 Apr 18, 2025 am 11:48 AM

在開發一個電商網站時,我遇到了一個棘手的問題:如何為用戶提供個性化的商品推薦。最初,我嘗試了一些簡單的推薦算法,但效果並不理想,用戶的滿意度也因此受到影響。為了提升推薦系統的精度和效率,我決定採用更專業的解決方案。最終,我通過Composer安裝了andres-montanez/recommendations-bundle,這不僅解決了我的問題,還大大提升了推薦系統的性能。可以通過一下地址學習composer:學習地址

解決 Craft CMS 中的緩存問題:使用 wiejeben/craft-laravel-mix 插件 解決 Craft CMS 中的緩存問題:使用 wiejeben/craft-laravel-mix 插件 Apr 18, 2025 am 09:24 AM

在使用CraftCMS開發網站時,常常會遇到資源文件緩存的問題,特別是當你頻繁更新CSS和JavaScript文件時,舊版本的文件可能仍然被瀏覽器緩存,導致用戶無法及時看到最新的更改。這個問題不僅影響用戶體驗,還會增加開發和調試的難度。最近,我在項目中遇到了類似的困擾,經過一番探索,我找到了wiejeben/craft-laravel-mix這個插件,它完美地解決了我的緩存問題。

laravel框架安裝方法 laravel框架安裝方法 Apr 18, 2025 pm 12:54 PM

文章摘要:本文提供了詳細分步說明,指導讀者如何輕鬆安裝 Laravel 框架。 Laravel 是一個功能強大的 PHP 框架,它 упростил 和加快了 web 應用程序的開發過程。本教程涵蓋了從系統要求到配置數據庫和設置路由等各個方面的安裝過程。通過遵循這些步驟,讀者可以快速高效地為他們的 Laravel 項目打下堅實的基礎。

如何使用 Composer 簡化郵件營銷:DUWA.io 的應用實踐 如何使用 Composer 簡化郵件營銷:DUWA.io 的應用實踐 Apr 18, 2025 am 11:27 AM

在進行郵件營銷活動時,我遇到了一個棘手的問題:如何高效地創建並發送HTML格式的郵件。傳統的方法是手動編寫代碼並使用SMTP服務器發送郵件,但這不僅耗時,而且容易出錯。在嘗試了多種解決方案後,我發現了DUWA.io,這是一個簡單易用的RESTAPI,能夠幫助我快速創建和發送HTML郵件。為了進一步簡化開發流程,我決定使用Composer來安裝和管理DUWA.io的PHP庫——captaindoe/duwa。

繼續使用PHP:耐力的原因 繼續使用PHP:耐力的原因 Apr 19, 2025 am 12:23 AM

PHP仍然流行的原因是其易用性、靈活性和強大的生態系統。 1)易用性和簡單語法使其成為初學者的首選。 2)與web開發緊密結合,處理HTTP請求和數據庫交互出色。 3)龐大的生態系統提供了豐富的工具和庫。 4)活躍的社區和開源性質使其適應新需求和技術趨勢。

laravel怎麼查看版本號 laravel查看版本號方法 laravel怎麼查看版本號 laravel查看版本號方法 Apr 18, 2025 pm 01:00 PM

Laravel框架內置了多種方法來方便地查看其版本號,滿足開發者的不同需求。本文將探討這些方法,包括使用Composer命令行工具、訪問.env文件或通過PHP代碼獲取版本信息。這些方法對於維護和管理Laravel應用程序的版本控制至關重要。

使用DICR/YII2-Google將Google API集成在YII2中 使用DICR/YII2-Google將Google API集成在YII2中 Apr 18, 2025 am 11:54 AM

vProcesserazrabotkiveb被固定,мнелостольностьстьс粹餾標д都LeavallySumballanceFriablanceFaumDoptoMatification,Čtookazalovnetakprosto,kakaožidal.posenesko

See all articles