启动 文章以 Laravel 学习。入口文件 public/index.php
:
require __DIR__ .'/../vendor/autoload.php' ;
autoload.php
不负责具体功能逻辑,只做了两件事:初始化自动加载类、注册自动加载类。
autoload_real.php
中的类名为 ComposerAutoloaderInit...
这可能是为防止与用户自定义类名跟这个类重复冲突,加上了哈希值。
其实还有一个做法我们更加熟悉,是定义一个命名空间。这里为什么不定义一个命名空间呢?一种理解:命名空间一般都是为了复用,而这个类只需要运行一次即可,以后也不会用得到,用哈希值更加合适。
autoload_real.php autoload.php
主要调用了 getLoader()
:
public static function getLoader ( ) { if (null !== self ::$loader ) { return self ::$loader ; } spl_autoload_register (array ('ComposerAutoloaderInit76e88f0b305cd64c7c84b90b278c31db' , 'loadClassLoader' ), true , true ); self ::$loader = $loader = new \Composer\Autoload\ClassLoader (); spl_autoload_unregister (array ('ComposerAutoloaderInit76e88f0b305cd64c7c84b90b278c31db' , '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\ComposerStaticInit76e88f0b305cd64c7c84b90b278c31db ::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\ComposerStaticInit76e88f0b305cd64c7c84b90b278c31db ::$files ; } else { $includeFiles = require __DIR__ . '/autoload_files.php' ; } foreach ($includeFiles as $fileIdentifier => $file ) { composerRequire76e88f0b305cd64c7c84b90b278c31db ($fileIdentifier , $file ); } return $loader ; }
单例模式 1 if (null !== self ::$loader ) { return self ::$loader ; }
构造 ClassLoader 核心类 2 spl_autoload_register (array ('ComposerAutoloaderInit76e88f0b305cd64c7c84b90b278c31db' , 'loadClassLoader' ), true , true );self ::$loader = $loader = new \Composer\Autoload\ClassLoader ();spl_autoload_unregister (array ('ComposerAutoloaderInit76e88f0b305cd64c7c84b90b278c31db' , 'loadClassLoader' ));
public static function loadClassLoader ($class ) { if ('Composer\Autoload\ClassLoader' === $class ) { require __DIR__ . '/ClassLoader.php' ; } }
composer
先向 PHP
自动加载机制注册了一个函数,这个函数 require
了 ClassLoader
文件。成功 new
出该文件中核心类 ClassLoader()
后,又销毁了该函数。
为什么不直接 require
?原因是:怕有的用户也定义了个 \Composer\Autoload\ClassLoader
命名空间,导致自动加载错误文件。
那为什么不跟引导类一样用个哈希值呢?原因是:这个类是可以复用的,框架允许用户使用这个类。
初始化核心类对象 3 对自动加载类的初始化,主要是给自动加载核心类初始化顶级命名空间映射。初始化的方法有两种:
使用 autoload_static
进行静态初始化 调用核心类接口初始化 autoload_static 静态初始化 静态初始化只支持 PHP 5.6
以上版本、不支持 HHVM
虚拟机、不存在 Zend-encoded file
。
autoload_static.php
<?php namespace Composer \Autoload ;class ComposerStaticInit76e88f0b305cd64c7c84b90b278c31db { public static $files = array (...); public static $prefixLengthsPsr4 = array (...); public static $prefixDirsPsr4 = array (...); public static $fallbackDirsPsr4 = array (...); public static $prefixesPsr0 = array (...); public static $classMap = array array (...); public static function getInitializer (ClassLoader $loader ) { return \Closure ::bind (function () use ($loader ) { $loader ->prefixLengthsPsr4 = ComposerStaticInit76e88f0b305cd64c7c84b90b278c31db ::$prefixLengthsPsr4 ; $loader ->prefixDirsPsr4 = ComposerStaticInit76e88f0b305cd64c7c84b90b278c31db ::$prefixDirsPsr4 ; $loader ->fallbackDirsPsr4 = ComposerStaticInit76e88f0b305cd64c7c84b90b278c31db ::$fallbackDirsPsr4 ; $loader ->prefixesPsr0 = ComposerStaticInit76e88f0b305cd64c7c84b90b278c31db ::$prefixesPsr0 ; $loader ->classMap = ComposerStaticInit76e88f0b305cd64c7c84b90b278c31db ::$classMap ; }, null , ClassLoader ::class ); } }
这个静态初始化类的核心就是 getInitializer()
函数,它将自己类中的顶级命名空间映射给了 ClassLoader 类。
值得注意的是这个函数返回的是一个匿名函数,为什么呢?原因就是 ClassLoader
中的 prefixLengthsPsr4
、prefixDirsPsr4
等等方法都是 private
的。普通的函数没办法给类的 private
成员变量赋值。利用匿名函数的绑定功能就可以将把匿名函数转为 ClassLoader
类的成员函数。
关于匿名函数的 绑定功能 。
接下来就是 顶级命名空间 初始化的关键了。
classMap public static $classMap = array ( 'App\\Api\\Middleware\\DeviceRecord' => __DIR__ . '/../..' . '/app/Api/Middleware/DeviceRecord.php' , 'App\\Api\\Middleware\\HeaderCheck' => __DIR__ . '/../..' . '/app/Api/Middleware/HeaderCheck.php' , ... )
直接命名空间全名与目录的映射,没有顶级命名空间。简单粗暴,也导致这个数组相当的大。
PSR0 顶级命名空间映射 public static $prefixesPsr0 = array ( 'P' => array ( 'Prophecy\\' => array ( 0 => __DIR__ . '/..' . '/phpspec/prophecy/src' , ), 'Parsedown' => array ( 0 => __DIR__ . '/..' . '/erusev/parsedown' , ), ), ... );
为了快速找到顶级命名空间,这里使用命名空间第一个字母作为前缀索引。这个映射的用法比较明显,假如我们有 Parsedown/example
这样的命名空间,首先通过首字母 P
,找到:
这个数组,然后就会遍历这个数组来和 Parsedown/example
比较,发现第一个 Prophecy
不符合,第二个 Parsedown
符合,然后得到了映射目录(映射目录可能不止一个):
0 => __DIR__ . '/..' . '/erusev/parsedown' ,
接着遍历这个数组,尝试 __DIR__ . '/..' . '/erusev/parsedown/Parsedown/example.php'
是否存在,如果不存在接着遍历数组(这个例子数组只有一个元素),如果数组遍历完都没有,就会加载失败。
PSR4 标准顶级命名空间映射 public static $prefixLengthsPsr4 = array ( 'p' => array ( 'phpDocumentor\\Reflection\\' => 25 , ), 'Z' => array ( 'Zend\\Diactoros\\' => 15 , ), ... ); public static $prefixDirsPsr4 = array ( 'phpDocumentor\\Reflection\\' => array ( 0 => __DIR__ . '/..' . '/phpdocumentor/reflection-common/src' , 1 => __DIR__ . '/..' . '/phpdocumentor/reflection-docblock/src' , 2 => __DIR__ . '/..' . '/phpdocumentor/type-resolver/src' , ), 'Zend\\Diactoros\\' => array ( 0 => __DIR__ . '/..' . '/zendframework/zend-diactoros/src' , ), ... );
PSR4
标准 顶级命名空间
映射用了两个数组,第一个和 PSR0
一样用命名空间第一个字母作为前缀索引,然后是 顶级命名空间
,但是最终并不是文件路径,而是 顶级命名空间
的长度。为什么呢?因为 PSR4
的文件目录更加灵活,更加简洁。
PSR0
中 顶级命名空间
目录 直接加 到命名空间前面就可以得到路径:
↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓ Parsedown/example => __DIR__ . '/..' . '/erusev/parsedown/Parsedown/example.php ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑
而 PSR4
却是用顶级命名空间目录 替换 顶级命名空间,所以获得顶级命名空间的 长度 很重要:
↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓ Parsedown/example => __DIR__ . '/..' . '/erusev/parsedown/example.php ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑
举例:假如我们找 Symfony\\Polyfill\\Mbstring\\example
这个类,和 PSR0
一样通过前缀索引和字符串匹配我们得到了:
'Symfony\\Polyfill\\Mbstring\\' => 26,
这条记录,键是顶级命名空间,值是命名空间的长度。拿到顶级命名空间后去 $prefixDirsPsr4
获取它的映射目录数组(注意映射目录可能不止一条):
'Symfony\\Polyfill\\Mbstring\\' =>array ( 0 => __DIR__ . '/..' . '/symfony/polyfill-mbstring' , ),
将 Symfony\\Polyfill\\Mbstring\\example
前 26 个字母替换为 __DIR__ . '/..' . '/symfony/polyfill-mbstring
也就是:
__DIR__ . '/..' . '/symfony/polyfill-mbstring/example.php
先验证磁盘上这个文件是否存在,如果不存在接着遍历。如果遍历后没有找到,则加载失败。
自动加载核心类 ClassLoader
的静态初始化完成!
其实还有 $fallbackDirsPsr4
,暂未研究
调用接口初始化 如果 PHP
版本低于 5.6
或者使用 HHVM
虚拟机环境或者存在 zend_loader_file_encoded
,那么就要使用核心类的接口进行初始化。
$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 ); }
注册核心类对象 4 Composer 自动加载功能的启动与初始化,经过启动与初始化,自动加载核心类对象已经获得了顶级命名空间与相应目录的映射,换句话说,如果有命名空间 App\Console\Kernel
,我们已经知道了 App\
对应的目录,接下来我们就要解决下面的就是 \Console\Kernel
这一段。
public function register ($prepend = false ) { spl_autoload_register (array ($this , 'loadClass' ), true , $prepend ); }
一行代码实现自动加载。核心在 ClassLoader
的 loadClass()
函数上,这个函数负责按照 PSR
标准将顶层命名空间以下的内容转为对应的目录,也就是上面所说的将 App\Console\Kernel
中 Console\Kernel
这一段转为目录。
自动加载全局函数 5 Composer
不止可以自动加载命名空间,还可以加载全局函数。就是把全局函数写到特定的文件里面去,在程序运行前挨个 require
就行了。
if ($useStaticLoader ) { $includeFiles = Composer\Autoload\ComposerStaticInit76e88f0b305cd64c7c84b90b278c31db ::$files ; } else { $includeFiles = require __DIR__ . '/autoload_files.php' ; } foreach ($includeFiles as $fileIdentifier => $file ) { composerRequire76e88f0b305cd64c7c84b90b278c31db ($fileIdentifier , $file ); }
function composerRequire76e88f0b305cd64c7c84b90b278c31db ($fileIdentifier , $file ) { if (empty ($GLOBALS ['__composer_autoload_files' ][$fileIdentifier ])) { require $file ; $GLOBALS ['__composer_autoload_files' ][$fileIdentifier ] = true ; } }
问题 1 为什么不直接 require
$includeFiles
里面的每个文件名,而要用类外面的函数 composerRequire...
?
避免和用户定义函数冲突 防止有人在全局函数所在的文件写 $this
或者 self
假如 $includeFiles
有个 app/helper.php
文件,这个 helper.php
文件的函数外有一行代码: $this->foo()
,如果引导类在 getLoader()
函数直接 require($file)
,那么引导类就会运行这句代码,调用自己的 foo()
函数,这显然是错的。
事实上 helper.php
就不应该出现 $this
或 self
这样的代码,这样写一般都是用户写错了的,一旦这样的事情发生:
第一种情况:引导类恰好有 foo()
函数,那么就会莫名其妙执行了引导类的 foo()
。 第二种情况:引导类没有 foo()
函数,但是却甩出来引导类没有 foo()
方法这样的错误提示,用户不知道自己哪里错了。把 require
语句放到 引导类的外面 ,遇到 $this
或者 self
,程序就会告诉用户根本没有类, $this
或 self
无效,错误信息更加明朗。 问题 2 为什么要用 hash
作为 $fileIdentifier
?
这个变量是用来控制全局函数只被 require
一次的,那为什么不用 require_once
呢?事实上 require_once
比 require
效率低很多,使用全局变量 $GLOBALS
这样控制加载会更快。猜测另一个原因应该是 require_once
对相对路径的支持并不理想,所以 composer
尽量少用 require_once
。
运行 ClassLoader
将 loadClass()
函数注册到 PHP SPL
中的 spl_autoload_register()
里面去。这样,每当 PHP 遇到一个不认识的命名空间的时候,PHP 会自动调用注册到 spl_autoload_register()
里面的函数堆栈,运行其中的每个函数,直到找到命名空间对应的文件。
public function loadClass ($class ) { if ($file = $this ->findFile ($class )) { includeFile ($file ); return true ; } } public function findFile ($class ) { if (isset ($this ->classMap[$class ])) { return $this ->classMap[$class ]; } if ($this ->classMapAuthoritative || isset ($this ->missingClasses[$class ])) { return false ; } if (null !== $this ->apcuPrefix) { $file = apcu_fetch ($this ->apcuPrefix.$class , $hit ); if ($hit ) { return $file ; } } $file = $this ->findFileWithExtension ($class , '.php' ); if (false === $file && defined ('HHVM_VERSION' )) { $file = $this ->findFileWithExtension ($class , '.hh' ); } if (null !== $this ->apcuPrefix) { apcu_add ($this ->apcuPrefix.$class , $file ); } if (false === $file ) { $this ->missingClasses[$class ] = true ; } return $file ; }
loadClass()
主要调用 findFile()
函数。findFile()
在解析命名空间的时候主要分为两部分:
classMap
直接看命名空间是否在映射数组findFileWithExtension()
包含了 PSR0
、PSR4
如果我们在代码中写 'phpDocumentor\Reflection\example
,PHP 会通过 SPL 调用 loadClass
-> findFile
-> findFileWithExtension
。
首先默认用 .php
后缀名调用 findFileWithExtension
函数里,利用 PSR4
标准尝试解析目录文件,如果文件不存在则继续用 PSR0
标准解析 如果解析出来的目录文件仍然不存在,但是环境是 HHVM
虚拟机,继续用后缀名 .hh
再次调用 findFileWithExtension
函数,如果不存在,说明此命名空间无法加载,放到 classMap
中设为 false
,以便以后更快地加载 PSR4 对于 phpDocumentor\Reflection\example
,当尝试利用 PSR4
标准映射目录时,步骤如下:
$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 ; } } } } }
PSR0 如果 PSR4
标准加载失败,则要进行 PSR0
标准加载。对于 phpDocumentor\Reflection\example
,当尝试利用 PSR0
标准映射目录时,步骤如下:
if (false !== $pos = strrpos ($class , '\\' )) { $logicalPathPsr0 = substr ($logicalPathPsr4 , 0 , $pos + 1 ) . strtr (substr ($logicalPathPsr4 , $pos + 1 ), '_' , DIRECTORY_SEPARATOR); } else { $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 ; } } } } }
Q&A 个人一些疑问:
防止用户自定义与 ClassLoader 命名空间冲突 spl_autoload_register (array ('ComposerAutoloaderInit76e88f0b305cd64c7c84b90b278c31db' , 'loadClassLoader' ), true , true );self ::$loader = $loader = new \Composer\Autoload\ClassLoader ();spl_autoload_unregister (array ('ComposerAutoloaderInit76e88f0b305cd64c7c84b90b278c31db' , 'loadClassLoader' ));
为什么这样可以解决:与用户也定义了个 \Composer\Autoload\ClassLoader
命名空间,导致自动加载错误文件。
与第四个参数 $prepend
true
有关吗?
composer StaticLoader 有什么优势 composer
在加载类和加载全局方法时,都有两种方式。
$useStaticLoader = PHP_VERSION_ID >= 50600 && !defined('HHVM_VERSION') && (!function_exists('zend_loader_file_encoded') || !zend_loader_file_encoded());
以 $useStaticLoader
的值进行选择,为什么一定分两种,静态方法是有什么优势吗?
References – EOF –