第4章 探究 Composer 加载原理

4.1 Composer 介绍与应用

在这里插入图片描述

1. 什么是 Composer ?

Composer 是一个依赖的包管理工具, 会在每个项目的基础上进行依赖包的管理,在你项目中的某个目录 ( 例如 vendor ) 进行安装。默认情况下它不会全局安装任何东西。

如果没有包管理器会怎样?

当开发中需要一个组件包时,这时候你就需要下载引入它,如果使用的组件包又需要另外一个包的话,你又不得不继续下载包,而且它们之间依赖的环境条件你还需要自己去进行解决。但如果有了包管理器,开发需要使用的包或者包与包之间的依赖关系,都完全可以交给包管理器来统一管理之间的依赖关系。

2. 如何使用?

Linux 下载安装 Composer

$ curl -s https://getcomposer.org/installer | php  
$ sudo mv composer.phar /usr/local/bin/composer  

Windows 下载安装

Wondows 平台上,我们只需要下载 Composer-Setup.exe 后,一步步安装即可。需要注意的是,你需要开启 openssl 配置,我们打开 php 目录下的 php.ini,将 extension=php_openssl.dll 前面的分号去掉就可以了。

安装好后,在更改镜像为阿里云 Composer 全量镜像

composer config -g repo.packagist composer https://mirrors.aliyun.com/composer/
取消配置:composer config -g --unset repos.packagist

4.2 PHP 自动加载原理

1. 为啥需要自动加载

例如,现在有 A.php和 B.php,如果现在我A需要用到 B.php 里面的方法,就需要把 B.php 加载到 A.php 里面来,这样我 A 才能使用到方法。

何来自动加载?就是当一个 PHP 文件实例化一个类文件,但实例化的文件本身不存在,就需要通过自动加载的方式把不存在的文件给引入到当前类库中,然后才能进行方法调用的执行。相当于有一个备选的方案来帮助引入 PHP 类文件。

2. PHP 要怎么实现自动加载呢?

首先 PHP 在实例化一个不存在的类库时,就会触发到 spl_autoload_register 注册的闭包方法,在这个函数的方法体里面进行方法引入。那为啥能准备的引入文件呢 ?

因为当实例化的文件不存在时,这个类的名字会通过参数传到闭包函数中,闭包函数的方法体在根据参数直接来引入相关类,这样就可以实例化到相关的文件,然后就可以进行正常的操作。

spl_autoload_register(function($className){
  if (is_file('./lib/' .$className.'.php')) {
    require './lib/' . $className.'.php';
  }
});

其实在 spl_autoload_register 之前,还有一个 __autoload 的魔术方法来进行类库的自动引入,只不过该方法在一个类文件中只能引用一次,所以当多个文件需要引入时,就不太方便。故次才有了spl_autoload_register 的方法。

4.3 解析 Composer 加载过程

Composer 的加载是通过 PSR 的编码规范来进行自动加载的。 PSR-4 规范了如何指定文件路径从而自动加载类的定义。

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

\<命名空间>\<子命名空间>\<类名>
  • 完整的类名必须要有一个顶级命名空间,被称为 “vendor namespace”。
  • 完整的类名可以有一个或多个子命名空间。
  • 完整的类名必须有一个最终的类名。
  • 完整的类名中任意一部分中的下滑线都是没有特殊含义的。
  • 完整的类名可以由任意大小写字母组成。
  • 所有类名都必须是大小写敏感的。

示例 :

PSR-4 风格

  • 类名:Nahida
  • 命名空间前缀:Zend
  • 文件基目录:/usr/includes/Zend/
  • 文件路径:/usr/includes/Zend/Nahida.php

类名:Request

  • 命名空间前缀:SymfonyCore
  • 文件基目录:./vendor/Symfony/Core/
  • 文件路径:./vendor/Symfony/Core/Request.php

目录结构 :

-vendor/
| -vendor_name/
| | -package_name/
| | | -src/
| | | | -ClassName.php       # Vendor_Name\Package_Name\ClassName
| | | -tests/
| | | | -ClassNameTest.php   # Vendor_Name\Package_Name\ClassNameTest

class的类文件需要存放到 vendor 当中去,不然就会引入失败。

按照规范定义了类库后。Composer 会做如下操作 :

  • 你有一个项目依赖于若干个库。
  • 其中一些库依赖于其他库。
  • 你声明你所依赖的东西。
  • Composer 会找出那个版本的包需要安装,并安装它们(将它们下载到你的项目中)

比如:项目需要使用 phpunit 组件

{
   "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( ) 里

4.4 探索 Composer 源码

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

1. 启动框架

<?php
  define('LARAVEL_START', microtime(true));
  require __DIR__ . '/../vendor/autoload.php';

去找 vendor 目录下的 autoload.php

<?php
  require_once __DIR__ . '/composer' . '/autoload_real.php';
  return ComposerAutoloaderInit2c981d0f72838b8ba6448f6427ca961d::getLoader();

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 标准的自动加载文件
    • 存放着顶级命名空间与文件的映射。

2. autoload_real 引导类

在 vendor 目录下的 autoload.php 文件中我们可以看出,程序主要调用了引导类的静态方法 getLoader() 来实现类库的加载和引用。

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

    /***********初始化自动加载核心类对象****************/
    $useStaticLoader = PHP_VERSION_ID >= 50600 && 
    !defined('HHVM_VERSION');
    
    if ($useStaticLoader) {
		require_once __DIR__ . '/autoload_static.php';
		call_user_func(\Composer\Autoload\ComposerStaticInit2c981d0f72838b8ba6448f6427ca961d::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\ComposerStaticInit2c981d0f72838b8ba6448f6427ca961d::$files;
	} else {
		$includeFiles = require __DIR__ . '/autoload_files.php';
	}
    
	foreach ($includeFiles as $fileIdentifier => $file) {
		composerRequire2c981d0f72838b8ba6448f6427ca961d($fileIdentifier, $file);
	}

    return $loader;
} 
4.4.1 单例

单例模式,保证自动加载类只能有一个。

<?php
if (null !== self::$loader) {
    return self::$loader;
}
4.4.2 构造ClassLoader核心类

第二部分 new 一个自动加载的核心类对象。当类不存在就直接通过spl_autoload_register注册到当前对象。注册函数体为loadClassLoader

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

  self::$loader = $loader = new \Composer\Autoload\ClassLoader();
//注册成功并获取到对象后,直接进行销毁
  spl_autoload_unregister(
array('ComposerAutoloaderInit2c981d0f72838b8ba6448f6427ca961d', 'loadClassLoader'));

loadClassLoader()函数 :

<?php
 //自动注册回调方法
public static function loadClassLoader($class)
{
    if ('Composer\Autoload\ClassLoader' === $class) {
		require __DIR__ . '/ClassLoader.php';
    }
}

在程序中,当 composer 先向 PHP 自动加载机制注册了一个函数,这个函数 requireClassLoader 文件。成功 new 出该文件中核心类 ClassLoader() 后,又销毁了该函数。减少内存空间的使用。

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

     call_user_func(   \Composer\Autoload\ComposerStaticInit2c981d0f72838b8ba6448f6427ca961d::getInitializer($loader));
  } else {
      //导入composer定义的命名空间规则
      $map = require __DIR__ . '/autoload_namespaces.php';
      foreach ($map as $namespace => $path) {
 			$loader->set($namespace, $path);
      }
//引入psr4的加载规范,并设置到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);
      }
    }

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

初始化的方法有两种,根据系统环境来进行注册方式的选择 :

  1. 使用 autoload_static 进行静态初始化;
  2. 包含命名空间、psr4、组件类、File等核心类来做到自动加载类库的引入,然后在来进行初始化
4.4.4 autoload_static 静态初始化 ( PHP >= 5.6 )

静态初始化只支持 PHP 5.6 以上版本并且不支持 HHVM 虚拟机。满足就进行静态方式的初始化。autoload_static.php 这个文件会定义了一个用于静态初始化的类,名字叫 ComposerStaticInit2c981d0f72838b8ba6448f6427ca961d,为了避免冲突而加了 hash 值。

<?php
  class ComposerStaticInit2c981d0f72838b8ba6448f6427ca961d{
     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 类 里的成员变量。

匿名函数的绑定功能

4.4.5 classMap(命名空间映射)
<?php
  public static $classMap = array (
    'App\\Console\\Kernel' => __DIR__ . '/../..' . '/app/Console/Kernel.php',
	'App\\Exceptions\\Handler' => __DIR__ . '/../..' . '/app/Exceptions/Handler.php',
	'App\\Http\\Controllers\\Controller' => __DIR__ . '/../..' . '/app/Http/Controllers/Controller.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',
  ...)

直接让命名空间的全名与目录映射,也会导致这个数组比较大。

4.4.6 PSR4 标准顶级命名空间映射数组
<?php
public static $prefixLengthsPsr4 = array(
      'p' => array (
		'YuanShen\\Nahida\\' => 15,
       ),
      'S' => array (
		'Symfony\\Polyfill\\Mbstring\\' => 26,
		'Symfony\\Component\\Yaml\\' => 23,
		'Symfony\\Component\\VarDumper\\' => 28,
		...
    ),
  ...
);

public static $prefixDirsPsr4 = array (
      'YuanShen\\Nahida\\' => array (
			0 => __DIR__ . '/..' . '/YuanShen/Nahida-common/src',
			1 => __DIR__ . '/..' . '/YuanShen/type-resolver/src',
			2 => __DIR__ . '/..' . '/YuanShen/Nahida-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
    'Symfony\\Polyfill\\Mbstring\\' => 26,

这条记录,键是顶级命名空间,值是命名空间的长度。拿到顶级命名空间后去 $prefixDirsPsr4 数组 获取它的映射目录数组 :

注意: 映射目录可能不止一条 。

<?php
  'Symfony\\Polyfill\\Mbstring\\' => array (
      0 => __DIR__ . '/..' . '/symfony/polyfill-mbstring',
  )

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

4.4.7 ClassLoader 接口初始化( PHP < 5.6 )

如果 PHP 版本低于 5.6 或者使用 HHVM 虚拟机环境,就要使用核心类的各个方法来进行命令空间、核心类等来进行初始化。不在使用静态调用方式。

<?php
    // PSR0 标准
    $map = require __DIR__ . '/autoload_namespaces.php';
    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);
    }
4.4.8 PSR4 标准的映射

autoload_psr4.php 的顶级命名空间映射。

<?php
return array(
    'XdgBaseDir\\' => 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。

4.4.9 命名空间映射

autoload_classmap :

<?php
public static $classMap = array (
    'App\\Console\\Kernel'
=> __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 部分,真正重要的就两部分——初始化与注册。初始化负责顶层命名空间的目录映射,注册负责实现顶层以下的命名空间映射规则。

4.4.10 注册

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

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

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

    /***********初始化自动加载核心类对象********************/
    $useStaticLoader = PHP_VERSION_ID >= 50600 && 
    !defined('HHVM_VERSION');
    
    if ($useStaticLoader) {
		require_once __DIR__ . '/autoload_static.php';
		call_user_func(\Composer\Autoload\ComposerStaticInit2c981d0f72838b8ba6448f6427ca961d::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\ComposerStaticInit2c981d0f72838b8ba6448f6427ca961d::$files;
    } else {
		$includeFiles = require __DIR__ . '/autoload_files.php';
    }
    
    foreach ($includeFiles as $fileIdentifier => $file) {
		composerRequire2c981d0f72838b8ba6448f6427ca961d($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\KernelConsole\Kernel 这一段转为目录,至于怎么转,会在下面 “运行” 的部分讲。核心类 ClassLoader 将 loadClass( ) 函数注册到 PHP SPL 中的 spl_autoload_register( ) 里面去。这样,每当 PHP 遇到一个不认识的命名空间的时候,PHP 会自动调用注册到 spl_autoload_register 里面的 loadClass() 函数,然后找到命名空间对应的文件。

4.4.11 全局函数的自动加载

Composer 不止可以自动加载命名空间,还可以加载全局函数。怎么实现的呢 ?

把全局函数写到特定的文件里面去,在程序运行前挨个 require就行了。这个就是 composer 自动加载的第五步,加载全局函数。

if ($useStaticLoader) {
    $includeFiles = Composer\Autoload\ComposerStaticInit7b790917ce8899df9af8ed53631a1c29::$files;
} else {
    $includeFiles = require __DIR__ . '/autoload_files.php';
}

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

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

4.4.12 静态初始化
ComposerStaticInit2c981d0f72838b8ba6448f6427ca961d::$files

public static $files = array (
'0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => __DIR__ . '/..' . '/symfony/polyfill-mbstring/bootstrap.php',
'667aeda72477189d0494fecd327c3641' => __DIR__ . '/..' . '/symfony/var-dumper/Resources/functions/dump.php',
...
);
4.4.13 普通初始化

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',
   ....
);

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

4.4.14 加载全局函数
class composerRequire2c981d0f72838b8ba6448f6427ca961d{
  public static function getLoader(){
      ...
      foreach ($includeFiles as $fileIdentifier => $file) {      		
          composerRequire7b790917ce8899df9af8ed53631a1c29($fileIdentifier, $file);
      }
      ...
  }
}
//根据获取到全局函数的数组来引入相关文件
function composerRequire2c981d0f72838b8ba6448f6427ca961d($fileIdentifier, $file)
 {
    if (empty(\$GLOBALS['__composer_autoload_files'][\$fileIdentifier])) {
		require $file;

		$GLOBALS['__composer_autoload_files'][$fileIdentifier] = true;
    }
}
4.4.15 运行

前面说过,ClassLoaderregister() 函数将 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;
    }
}
4.4.16 总结

原理流程 :

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

  • 将 \ 转为文件分隔符 /,加上后缀 php,变成 $logicalPathPsr4, 即 YuanShen//Nahida//Element.php
  • 利用命名空间第一个字母s作为前缀索引搜索 prefixLengthsPsr4 数组,查到下面这个数组 :
's' => array (
	'YuanShen\\Nahida\\' => 15,
	'YuanShen\\Fake\\' => 13,
 )
  • 遍历这个数组,得到两个顶层命名空间 YuanShen\Nahida\ 和 YuanShen\Fake\
  • 在这个数组中查找 YuanShen\Nahida\Element,找出 YuanShen\Nahida\ 这个顶层命名空间并且长度为 15
  • 在 prefixDirsPsr4 映射数组中得到 YuanShen\Nahida\ 的目录映射为 :
'YuanShen\\Nahida\\' => array (
    0 => __DIR__ . '/..' . '/YuanShen/Nahida-common/src',
    1 => __DIR__ . '/..' . '/YuanShen/type-resolver/src',
    2 => __DIR__ . '/..' . '/YuanShen/Nahida-docblock/src',
),
  • 遍历这个映射数组,得到三个目录映射

  • 查看 “目录+文件分隔符 //+substr($logicalPathPsr4, $length)” 文件是否存在,存在即返回。这里就是
    '__DIR__/../YuanShen/Nahida-common/src + substr(YuanShen/Nahida/Element.php,15)'

  • 如果失败,则利用 fallbackDirsPsr4 数组里面的目录继续判断是否存在文件


如果自己在使用第三方包的过程中,自己定义的根命名空间和第三方包的命名空间一样,则会以数组的形式存储,如 :

'Fairy\\' => array($baseDir . '/app', $vendorDir . '/cshaptx4869/work/src/Fairy'),

但注意不要有文件名冲突,不然只能识别一处,所以最好就是根命名空间不要同名。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

纳西妲爱编程

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值