PHP自动加载
此篇文章主要介绍加载的php类的方式,以及自动加载的几种模式;
题外话:大家空了可以多看看别人写的框架源码;自然能学到很多高级的东西;
PHPER几个发展阶段
初级阶段
大家在学刚开始学习php的时候常用的加载方式大致如下(大致说下目录结构):
project 项目根目录
├─controller 控制器层
├─model 模型层
├─view 视图层
├─config 配置目录
│ ├─config.php 常量定义
│ ├─require_file.php 加载文件
│ └─... 其他配置
|... 其他目录
通常启动时需要加载的文件都放在require_file.php中,使用加载函数都是include
、require
、include_once
以及require_once
;require_file.php就变成下面的样子;
这么做的坏处是什么,就是需要的,不需要的都一股脑加载了。当然你可以说,我只加载核心的就可以了,其他的地方需要的时候再去引入加载就行了。但是你不觉得麻烦吗?时间长了,自己在什么地方引入了什么文件,加载了什么文件都不知道了。
如果你从来没有想过要改变这种方式,那么接下来内容,就可以不看了。
中级阶段
此时可能你已经认识到了这种方式的弊端了,并且想要改变什么,这时候你会谷歌php的懒加载,自动加载方面的内容;此时,你可能会接触两个魔术函数__autoload()
(官方已不推荐使用该函数)与spl_autoload_register()
;这时候你可能在spl_autoload_register()
函数内写大量的规则去实现自动加载;可是你还是觉得不优雅,此时,你如果还想改变。那么就会进入到下个阶段。
这个阶段你可能大概会写一些,关于自动加载函数,下面这样的:
//第一种方式
function __autoload($classname) {
if ($classname === 'xxx.php'){
$filename = "./". $classname .".php";
include_once($filename);
} else if ($classname === 'yyy.php'){
$filename = "./other_library/". $classname .".php";
include_once($filename);
} else if ($classname === 'zzz.php'){
$filename = "./my_library/". $classname .".php";
include_once($filename);
}
// blah
}
//第二种方式
spl_autoload_register(function($className){
$filePath = __DIR__.DIRECTORY_SEPARATOR."...".DIRECTORY_SEPARATOR;
//完整类名
$completeClassName = $filePath.$className;
if(class_exist($filePath.$className)){ //或者file_exist()
include_once($completeClassName.".php");
}else{
throw new Exception("can't find this class(".$class.")!");
}
})
//第三种方式
spl_autoload_register('my_library_loader');
spl_autoload_register('other_library_loader');
spl_autoload_register('basic_loader');
function my_library_loader($classname) {
$filename = "./my_library/". $classname .".php";
include_once($filename);
}
function other_library_loader($classname) {
$filename = "./other_library/". $classname .".php";
include_once($filename);
}
function basic_loader($classname) {
$filename = "./". $classname .".php";
include_once($filename);
}
在这个阶段,你可能还有个过程就是了解了命名空间与PSR-4;此时又会产生比以上稍微高级点的方式,我只是写下,我的代码:
//第一个文件 base.php
//自动注册类
SDK_ROOT_PATH,
];
public static function auto_load($className)
{
//如果class别名不存在;则使用PSR-4尝试查找文件
$nameSpaceArr = explode("\\",$className);
//获取根命名空间
$rootNameSpace = array_shift($nameSpaceArr);
if(array_key_exists($rootNameSpace,self::$nameSpace)){
$classFile = self::$nameSpace[$rootNameSpace].implode(DS,$nameSpaceArr).PHP_EXT;
if(file_exists($classFile)){
return __include_once_file($classFile);
}
}
return false;
}
public static function register($autoload = null){
// 注册自动加载
spl_autoload_register(!is_null($autoload)?:"DevicePlatform\\library\\Loader::auto_load",true,true);
}
}
/**
* 引入文件
* @param string $filePath
* @return mixed
*/
function __include_once_file($filePath){
return (include_once($filePath));
}
/**
* 加载文件
* @param string $filePath
* @return mixed
*/
function _require_once_file($filePath){
return (require_once($filePath));
}
?>
高级阶段
达到上面的中级阶段,其实你已经离成功不远了。其实自动加载的原理都是根据:查找文件或者类存不存在,然后加载文件。然而,我们最难跨越的就是究竟如何写出来才是真的。理论再多也不及自己实践一把;
此时,你可能拼命了解PSR-0
、PSR-4
以及classmap
这三种懒加载标准以及方便的工具:composer
;
此时你写出来的代码应该和tp5的Loader.php
差不多了,此处就不放全部代码了;这个代码就比较长了,放了就不能说什么内容了就放两个最核心的方法,有兴趣的可以去看看。这个类基本包含了所有的了;
/**
* 自动加载
* @access public
* @param string $class 类名
* @return bool
*/
public static function autoload($class)
{
// 检测命名空间别名
if (!empty(self::$namespaceAlias)) {
$namespace = dirname($class);
if (isset(self::$namespaceAlias[$namespace])) {
$original = self::$namespaceAlias[$namespace] . '\\' . basename($class);
if (class_exists($original)) {
return class_alias($original, $class, false);
}
}
}
if ($file = self::findFile($class)) {
// 非 Win 环境不严格区分大小写
if (!IS_WIN || pathinfo($file, PATHINFO_FILENAME) == pathinfo(realpath($file), PATHINFO_FILENAME)) {
__include_file($file);
return true;
}
}
return false;
}
/**
* 注册自动加载机制
* @access public
* @param callable $autoload 自动加载处理方法
* @return void
*/
public static function register($autoload = null)
{
// 注册系统自动加载
spl_autoload_register($autoload ?: 'think\\Loader::autoload', true, true);
// Composer 自动加载支持
if (is_dir(VENDOR_PATH . 'composer')) {
if (PHP_VERSION_ID >= 50600 && is_file(VENDOR_PATH . 'composer' . DS . 'autoload_static.php')) {
require VENDOR_PATH . 'composer' . DS . 'autoload_static.php';
$declaredClass = get_declared_classes();
$composerClass = array_pop($declaredClass);
self::$prefixLengthsPsr4 = $composerClass::$prefixLengthsPsr4;
self::$prefixDirsPsr4 = property_exists($composerClass, 'prefixDirsPsr4') ? $composerClass::$prefixDirsPsr4 : [];
self::$prefixesPsr0 = property_exists($composerClass, 'prefixesPsr0') ? $composerClass::$prefixesPsr0 : [];
self::$map = property_exists($composerClass, 'classMap') ? $composerClass::$classMap : [];
} else {
self::registerComposerLoader();
}
}
// 注册命名空间定义
self::addNamespace([
'think' => LIB_PATH . 'think' . DS,
'behavior' => LIB_PATH . 'behavior' . DS,
'traits' => LIB_PATH . 'traits' . DS,
]);
// 加载类库映射文件
if (is_file(RUNTIME_PATH . 'classmap' . EXT)) {
self::addClassMap(__include_file(RUNTIME_PATH . 'classmap' . EXT));
}
self::loadComposerAutoloadFiles();
// 自动加载 extend 目录
self::$fallbackDirsPsr4[] = rtrim(EXTEND_PATH, DS);
}
关于懒加载
其实只要我们遵循了PSR-4的相关规范,我们可以直接使用composer中的相关懒加载逻辑。毕竟别人已经帮我们完全实现了懒加载的目的;只需我们在项目的启动时,引入composer自己的autoload.php
即可;并且根据命名空间规范使用即可;其实包括thinkphp5框架,在关于懒加载这块,也是集成了composer自身的autoload.php
相关规则。只是我们在开发SDK时,如何让其他开发人员更好的使用SDK,可以将相关懒加载代码写入进去;使其他开发人员(未使用composer工具的开发人员),通过引入一个SDK文件,就可完成整个SDK的运行;
懒加载四种模式
此处就说说懒加载的四种模式:PSR-0、psr-4、classmap、files;这四种模式需要结合下composer的自动加载;
命名空间
命名空间意义
PHP 命名空间(namespace)是在PHP 5.3中加入的,如果你学过C#和Java,那命名空间就不算什么新事物。 不过在PHP当中还是有着相当重要的意义。
PHP 命名空间可以解决以下两类问题:
- 用户编写的代码与PHP内部的类/函数/常量或第三方类/函数/常量之间的名字冲突。
- 为很长的标识符名称(通常是为了缓解第一类问题而定义的)创建一个别名(或简短)的名称,提高源代码的可读性。[1]
命名空间使用
PHP 命名空间中的类名可以通过三种方式引用:
非限定名称,或不包含前缀的类名称,例如
$a=new foo()
; 或foo::staticmethod()
;。如果当前命名空间是currentnamespace
,foo
将被解析为currentnamespace\foo
。如果使用foo
的代码是全局的,不包含在任何命名空间中的代码,则foo
会被解析为foo
。 警告:如果命名空间中的函数或常量未定义,则该非限定的函数名称或常量名称会被解析为全局函数名称或常量名称。限定名称,或包含前缀的名称,例如
$a = new subnamespace\foo()
; 或subnamespace\foo::staticmethod()
;。如果当前的命名空间是currentnamespace
,则foo
会被解析为currentnamespace\subnamespace\foo
。如果使用foo
的代码是全局的,不包含在任何命名空间中的代码,foo
会被解析为subnamespace\foo
。完全限定名称,或包含了全局前缀操作符的名称,例如,
$a = new \currentnamespace\foo()
; 或\currentnamespace\foo::staticmethod()
;。在这种情况下,foo
总是被解析为代码中的文字名(literal name)currentnamespace\foo
。
psr-0 标准 autoload_namespaces
懒加载,将目标目录作为基目录再进行命名空间和路径的映射后继续向后加载;
//composer.json文件
{
"autoload": {
"psr-0": {
"Psr0\\Lib\\": "psr0/lib/src/"
}
}
}
psr-4 标准 autoload_psr4
懒加载,将目标目录直接映射为命名空间对应的目录继续向后加载;
//composer.json文件
{
"autoload": {
// php 的 psr-4 规范的自动载入,是将目标目录直接影射为命名空间的
"psr-4": {
"Psr4\\Lib\\": "psr4/lib/src/",
"App\\Controllers\\": "app/controllers/",
"App\\Models\\": "app/models/"
}
}
}
classmap 模式 autoload_classmap
懒加载,扫描目录下的所有类文件,支持递归扫描, 生成对应的类名=>路径的映射,当载入需要的类时直接取出路径,速度最快
//composer.json文件
{
"autoload": {
// php 的 psr-4 规范的自动载入,是将目标目录直接影射为命名空间的
"psr-4": {
"Psr4\\Lib\\": "psr4/lib/src/",
"App\\Controllers\\": "app/controllers/",
"App\\Models\\": "app/models/"
}
}
}
files 模式
自动载入的文件,主要用来载入一些没办法懒加载的公共函数
//composer.json文件
// 扫描目录下的所有文件生成 hash => 路径的映射 运行时实时加载
// 主要用来载入工具函数
{
"autoload": {
// php 的 psr-4 规范的自动载入,是将目标目录直接影射为命名空间的
"files": [
"ext/common/functions.php",
"ext/system/functions.php"
]
}
}
完整composer.json文件加载
编辑composer.json
文件如下,
{
"autoload": {
"psr-0": {
"Psr0\\Lib\\": "psr0/lib/src/"
},
"psr-4": {
"Psr4\\Lib\\": "psr4/lib/src/",
"App\\Controllers\\": "app/controllers/",
"App\\Models\\": "app/models/"
},
"classmap": [
"classmap/lib/src/"
],
"files": [
"ext/common/functions.php",
"ext/system/functions.php"
]
}
}
刷新 autoload 规则
使用composer dump-autoload
重新加载生成对应的文件映射结构即可。
psr-0
在vendor/composer/autoload_namespaces.php
文件中可以看到:
array($baseDir . '/psr0/lib/src'),
);
psr-4
在vendor/composer/autoload_psr4.php
文件中可以看到:
array($baseDir . '/psr4/lib/src'),
'App\\Models\\' => array($baseDir . '/app/models'),
'App\\Controllers\\' => array($baseDir . '/app/controllers'),
);
其实 psr-0/psr-4 的自动载入都将命名空间映射为相应的目录。只不过 psr-0 是映射到目标地址作为基目录再解析命名空间,而 psr-4 是直接映射。
classmap
因为它本身就没遵循命名空间和路径映射的半点规范...随便写一个好了,他的模式其实就是扫描指定目录下的所有文件,把类采集出来,做一个映射表,这个类在这个文件里,完了。
files
在运行时就直接载入的一些函数文件(非类文件)
PSR-0与PSR-4的区别
PSR是Proposing a Standards Recommendation(提出标准建议)的缩写,是由PHP Framework Interoperability Group(PHP通用性框架小组,简称PHP-FIG)发起的,通过他们命名就可以看出,这是个主要是针对框架通用性而做努力的开放性小组,他们的在Github上有自己的仓库地址,目前只有一个被接受的标准,那就是PSR-0标准,标准定义了PHP自动加载的命名规范和文件路径规范。 针对PSR-0标准主要提到了以下几点:
- 一个完全合格的命名空间和类名必须有以下的结构“<提供者名称>(<命名空间>)*<类名>”
- 每个命名空间必须有顶级的命名空间(“提供者”)
- 每个命名空间可以有任意多个子命名空间
- 每个命名空间在被从文件系统加载时必须被转换为“操作系统路径分隔符”(DIRECTORY_SEPARATOR )
- 每个“”字符在“类名”中被转换为DIRECTORY_SEPARATOR 。“”符号在命名空间中没有这个含义
- 符合命名标准的命名空间和类名必须以“.php”结尾来加载文件
- 提供商名称,命名空间,类名可以由大小写字母组成,其中命名空间和类名是大小写敏感的以保证多系统兼容性
- 如果文件不存在需要返回false
而PSR-4与PSR-0的区别:
在composer中定义的NS,psr4必须以\结尾否则会抛出异常,psr0则不要求
psr0里面最后一个\之后的类名中,如果有下划线,则会转换成路径分隔符,如Name_Space_Test会转换成Name\Space\Test.php。在psr4中下划线不存在实际意义
psr0有更深的目录结构
比如定义了NS为 Foo\Bar=>vendor\foo\bar\src, use Foo\Bar\Tool\Request调用NS。
如果以psr0方式加载,实际的目录为vendor\foo\bar\src\Foo\Bar\Tool\Request.php
如果以psr4方式加载,实际目录为vendor\foo\bar\src\Tool\Request.php
参考博客
- PSR-4
- PSR-4 kancloud 中文版
- 命名空间 php手册
- 自动加载与命名空间
- composer 自动载入 autoload 的使用详解 psr0/psr4/classmap/files
- PSR-0与PSR-4区别
-
引用于菜鸟教程中《PHP 命名空间(namespace)》文章 ↩