public/index.php
:
// Register The Auto Loader require __DIR__.'/../vendor/autoload.php';
vendor/composer/autoload_real.php'
:
autoload.php
不负责具体功能逻辑,只做了两件事:初始化自动加载类、注册自动加载类。
autoload_real.php
中的类名为 ComposerAutoloaderInit...
这可能是为防止与用户自定义类名跟这个类重复冲突,加上了哈希值。
其实还有一个做法我们更加熟悉,那就是不直接定义类名,而是定义一个命名空间。这里为什么不定义一个命名空间呢?个人理解:命名空间一般都是为了复用,而这个类只需要运行一次即可,以后也不会用得到,用哈希值更加合适。
autoload_real.php
autoload.php
主要调用了 getLoader()
:
public static function getLoader() { // 单例模式,自动加载类只能有一个 1 if (null !== self::$loader) { return self::$loader; } // 获得自动加载核心类对象 2 spl_autoload_register(array('ComposerAutoloaderInit76e88f0b305cd64c7c84b90b278c31db', 'loadClassLoader'), true, true); self::$loader = $loader = new \Composer\Autoload\ClassLoader(); spl_autoload_unregister(array('ComposerAutoloaderInit76e88f0b305cd64c7c84b90b278c31db', 'loadClassLoader')); // 初始化自动加载核心类对象 3 $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); } } // 注册自动加载核心类对象 4 $loader->register(true); // 自动加载全局函数 5 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 // autoload_static.php @generated by Composer namespace Composer\Autoload; // hash 防止冲突 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
,找到:
'P' => array (...)
这个数组,然后就会遍历这个数组来和 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
,那么就要使用核心类的接口进行初始化。
/* PSR0 取出命名空间的第一个字母作为索引,一个索引对应多个顶级命名空间,一个顶级命名空间对应多个目录路径,具体形式可以查看上面的 autoload_static 的 $prefixesPsr0。 如果没有顶级命名空间,就只存储一个路径名,以便在后面尝试加载。 */ $map = require __DIR__ . '/autoload_namespaces.php'; foreach ($map as $namespace => $path) { $loader->set($namespace, $path); } /* PSR4 如果没有顶级命名空间,就直接保存目录。 如果有命名空间的话,要保证顶级命名空间最后是 \,然后分别保存 ( 前缀 -> 顶级命名空间,顶级命名空间 -> 顶级命名空间长度 ) ( 顶级命名空间 -> 目录 ) 这两个映射数组。具体形式可以查看上面我们讲的 autoload_static 的 prefixLengthsPsr4、$prefixDirsPsr4 。 */ $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
这一段。
/** * Registers this instance as an autoloader. * * @param bool $prepend Whether to prepend the autoloader or not */ 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()
里面的函数堆栈,运行其中的每个函数,直到找到命名空间对应的文件。
/** * Loads the given class or interface. * * @param string $class The name of the class * @return bool|null True if loaded, null otherwise */ public function loadClass($class) { if ($file = $this->findFile($class)) { includeFile($file); // include $file; Prevents access to $this/self from included files. return true; } } /** * Finds the path to the file where the class is defined. * * @param string $class The name of the class * * @return string|false The path if found, false otherwise */ public function findFile($class) { // class map lookup if (isset($this->classMap[$class])) { return $this->classMap[$class]; } // classMapAuthoritative 关闭搜索未在类映射中注册的类的 prefix and fallback directories。- 不清楚干啥的 暂没研究 if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) { return false; } // 如果启用扩展名,则使用 APCu 前缀来缓存已找到/未找到的类。 - 不清楚干啥的 暂没研究 if (null !== $this->apcuPrefix) { $file = apcu_fetch($this->apcuPrefix.$class, $hit); if ($hit) { return $file; } } $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; }
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
标准映射目录时,步骤如下:
// $class: phpDocumentor\Reflection\example // PSR-4 lookup $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext; // $logicalPathPsr4: phpDocumentor/Reflection/example.php(hh)` $first = $class[0]; // $first: p if (isset($this->prefixLengthsPsr4[$first])) { /* 'p' => array ( 'phpDocumentor\\Reflection\\' => 25, ), */ $subPath = $class; // $subPath: phpDocumentor\Reflection\example while (false !== $lastPos = strrpos($subPath, '\\')) { // $lastPos 13 $subPath = substr($subPath, 0, $lastPos); $search = $subPath.'\\'; if (isset($this->prefixDirsPsr4[$search])) { // search phpDocumentor\\Reflection\\ // $lastPos 25 /* 'phpDocumentor\\Reflection\\' => array ( 0 => __DIR__ . '/..' . '/phpdocumentor/reflection-common/src', 1 => __DIR__ . '/..' . '/phpdocumentor/reflection-docblock/src', 2 => __DIR__ . '/..' . '/phpdocumentor/type-resolver/src', ), */ $pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1); // $pathEnd /example.php(hh) foreach ($this->prefixDirsPsr4[$search] as $dir) { // 遍历 3 个 if (file_exists($file = $dir . $pathEnd)) { // $file __DIR__ . '/..' . /phpdocumentor/type-resolver/src/example.php(hh)` return $file; } } } } }
PSR0
如果 PSR4
标准加载失败,则要进行 PSR0
标准加载。对于 phpDocumentor\Reflection\example
,当尝试利用 PSR0
标准映射目录时,步骤如下:
// $class: phpDocumentor\Reflection\example // 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; } // $logicalPathPsr0: phpDocumentor/Reflection/example.php(hh)` if (isset($this->prefixesPsr0[$first])) { foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) { /* 'P' => array ( 'Prophecy\\' => array ( 0 => __DIR__ . '/..' . '/phpspec/prophecy/src', ), 'Parsedown' => array ( 0 => __DIR__ . '/..' . '/erusev/parsedown', ), ), */ if (0 === strpos($class, $prefix)) { foreach ($dirs as $dir) { if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { // $file __DIR__ . '/..' . '/phpspec/prophecy/src' . phpDocumentor/Reflection/example.php(hh) return $file; } } } } }
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- java反射原理, 注解原理
- Webpack 原理(二):加载原理
- Docker原理之 - CGroup实现原理
- 【Vue原理】响应式原理 - 白话版
- Docker实现原理之 - OverlayFS实现原理
- UAV MOF工作原理之Agent注入机制原理
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
The Art and Science of CSS
Jonathan Snooks、Steve Smith、Jina Bolton、Cameron Adams、David Johnson / SitePoint / March 9, 2007 / $39.95
Want to take your CSS designs to the next level? will show you how to create dozens of CSS-based Website components. You'll discover how to: # Format calendars, menus and table of contents usin......一起来看看 《The Art and Science of CSS》 这本书的介绍吧!