本文是《自制php框架》之自动加载篇,笔者参照tp5框架的自动加载相关源码,写了几个p1~p4四个demo(放在我的github了),基本体现了从0到成型框架的自动加载的编写过程。文章篇幅很长,如果你属于以下情况,建议看下:
用过php框架,但不懂为何:只要use app\model\User
(没有include或require
)就能直接用User类。
理解php是通过spl_autoload_register
实现自动加载的,但心血来潮看了一下某个框架的源码,不理解大型一点的框架的自动机制代码为何能写那么长,是为了解决什么问题。
想理解composer的自动加载如何实现。
spl_autoload_register
注册(即指定)的,可以注册多个
spl_autoload_register(callback, $prepend)
,设置第二个参数为true,即可排到队首。__autoload(){}
写处理代码,但明显不能应对上面说的有多个自动加载处理函数的情况。use
,然后访问。根据上面的背景知识,最直观能想到的,就是P1这种实现。也是网上大多数博文都有写到的。
先看看整体目录结果,后面的p2~p4目录结构相同。
p1
├── app ## 应用目录,开发者主要在这层写
│ ├── controller
│ │ └── User.php
│ └── model
│ └── User.php
├── fool ## 框架类库目录
│ └── Loader.php ## 自动加载类
├── index.php ## 入口文件
└── vendor ## 第三方包目录
└── pack1
└── A.php ## 第三个包pack1下的A文件,里面有A类
首先在入口文件/index.php
定义根目录路径和调用自动加载类
define("DS", DIRECTORY_SEPARATOR); // linux下是/,windows是/或\
define("EXT", '.php');
define("ROOT_PATH", __DIR__ . DS); // 项目根目录
// 注册自动加载机制
require ROOT_PATH . 'fool/Loader.php';
\fool\Loader::register();
然后在/fool/Loader.php
写具体的加载处理:
autoload($class)
的参数$class
是自动传入的,其值就是想访问但找不到的类的完整类名(即包含命名空间部分的)。打印一下就知道了~findFile()
,命名空间格式是namespace app\controller;
,linux下文件目录路径是app/contoller
,要找到文件,将传入命名空间的\
转成DS
,再拼上.php
,就是了。
namespace fool;
class Loader
{
/*
* 注册自动加载处理函数
* @return void
* */
public static function register()
{
spl_autoload_register("fool\\Loader::autoload", true, true);
}
/*
* 自动加载处理函数
* @param string $class 类名
* @return bool
* */
public static function autoload($class)
{
if ($file = self::findFile($class)) {
return require $file;
}
return false;
}
/*
* 查找文件
* @param string $class 类名
* @return bool|string
* */
private static function findFile($class)
{
return ROOT_PATH . strtr($class, '\\', '/') . EXT;
}
}
测试:
/app/controller/User.php
声明User类,当然是要声明命名空间的。然后通过use方式,访问定义在app/model/User.php
的User
类。
namespace app\controller;
use app\model\User as UserModel;
class User
{
public function index()
{
echo "this is controller User function index
\n";
$model = new UserModel();
$model->getList();
}
}
/app/model/User.php
:
namespace app\model;
class User
{
public function getList()
{
echo "this is model User function getList
\n";
}
}
/index.php
访问// new与目录对应的命名空间,成功
use app\controller\User;
$user = new User();
$user->index();
php index.php
,或通过web方式访问index.php。看到打印如下:表明访问成功this is controller User function index
this is model User function getList
似乎,自动加载就这么简单的实现了,上面写的最终实现效果也实现了。但,你也许有2个疑惑:
写了一堆代码,与最原始方式:用常量定义根目录,访问写在其它文件的类前require ROOT_PATH . 文件路径
,再访问。似乎区别不大啊,并没简化多少工作量,访问前还是要use
,甚至多出一步,在类头声明命名空间,这样做的意义在哪?
在类头声明命名空间是为了避免类重名,即使通过原始方式,也应该要在类头声明命名空间,访问前也要use。
即原始方式实际也是:require -> use -> 访问
而自动加载是:use -> 访问
use 是必需的,否则,程序怎么可能知道你要访问的是哪个类。
嗯,自动加载的好处我体会到了(省去了require步骤),那么,为什么那些框架的自动加载处理代码有那么长呢,为了解决什么问题?tp5.0自动加载源码。
其中一个要解决的问题是,也就是P1方式的致命缺陷,如果文件路径与声明的命名空间不对应。当使用第三方包时~看例子吧。
把第三方包放在/vendor
目录下,/vendor/pack1
为一个第三方包,在/vendor/pack1/A.php
写,第三方包的命名空间不可能是vendow/pack1
的,别人用了存放第三方包的目录名未必是vendor
,因此第三方包基本都是这种格式。
namespace pack1;
class A
{
public function work()
{
echo "this is a extra package pack1, class A function work was called
\n";
}
}
访问试试,在/index.php
// new目录与命名空间不对应的 (项目目录/vendor/pack1/A.php, 命名空间pack1\A) 失败
use pack1\A;
$p1 = new A();
$p1->work();
报错找不到
PHP Warning: require(/data/autoload/p1/pack1/A.php): failed to open stream: No such file or directory in /data/autoload/p1/fool/Loader.php on line 24
如何解决命名空间与所在文件路径不对应情况?
use pack1\A
要引入/vendor/pack1/A.php
,很容易想到,只要在自动加载处理函数函数里,将pack1
替换成/vendor/pack1
即可,即需要加多两个步骤:
pack1
与/vendor/pack1
的映射关系存到变量里。上面的代码不变,改的只有/fool/Loader.php
。
注册命名空间
$prefixDirsPsr4
变量,存命名空间与目录的映射关系。addPsr4()
方法,将映射关系存储到$prefixDirsPsr4
变量。addNamespace()
方法, 目的有
private static $prefixDirsPsr4 = [];
public static function addNamespace($namespace, $path = '')
{
if (is_array($namespace)) {
foreach ($namespace as $prefix => $paths) {
self::addPsr4($prefix, $paths, true);
}
} else {
self::addPsr4($namespace, $path, true);
}
}
private static function addPsr4($prefix, $paths, $prepend = false)
{
if (!isset(self::$prefixDirsPsr4[$prefix])) {
// 注册新的命名空间
self::$prefixDirsPsr4[$prefix] = (array) $paths;
} else {
// 为已有命名空间添加对应目录
self::$prefixDirsPsr4[$prefix] = $prepend ?
array_merge((array) $paths, self::$prefixDirsPsr4[$prefix]) :
array_merge(self::$prefixDirsPsr4[$prefix], (array) $paths);
}
}
注册框架必须的命名空间:
public static function register()
{
spl_autoload_register("fool\\Loader::autoload", true, true);
// 添加命名空间 对应目录
self::addNamespace([
'app' => APP_PATH,
'fool' => FOOL_PATH,
]);
}
修改找文件方式:思路是识别出传入命名空间里首次出现的/
的索引,将前面替换成对应文件路径,再拼接后面。
private static function findFile($class)
{
// 先直接 命名空间 转成 路径
$logicalPathPsr4 = strtr($class, '\\', DS) . EXT;
// 根据(命名空间 与 目录)映射 替换前缀
$len = strpos($logicalPathPsr4, '/');
$cPrefix = substr($logicalPathPsr4, 0, $len);
$follow = substr($logicalPathPsr4, $len+1);
foreach (self::$prefixDirsPsr4 as $prefix => $dirs) {
if ($prefix == $cPrefix) {
foreach ($dirs as $dir) {
if (is_file($file = $dir . $follow)) {
return $file;
}
}
}
}
return false;
}
与P1同样调用的代码,在/index.php
// new与目录对应的命名空间,成功
use app\controller\User;
$user = new User();
$user->index();
运行,成功
this is controller User function index
this is model User function getList
注册额外的命名空间,在/index.php
。这就是为什么addNamespace()
方法了要设为public。
// 设置命名空间pack1 对应 目录 vendow/pack1
\fool\Loader::addNamespace('pack1', ROOT_PATH . 'vendor/pack1'. DS);
new命名空间与文件所在路径不对应的类,在/index.php
// new目录与命名空间不对应的 (项目目录/vendor/pack1/A.php, 命名空间pack1\A) 也成功
use pack1\A;
$p1 = new A();
$p1->work();
运行,成功
this is controller User function index
this is model User function getList
this is a extra package pack1, class A function work was called
p3实现的效果与p2基本相同,p3基本就是tp5那套,而tp5那套很像composer那套。
用了官方的规范:对于映射关系中命名空间结尾是否要加\
,目录结尾是否要加/
,想必很乱,按tp5的来,这里统一:
\
:如fool\
,app\
/
:如/data/autoload/p3/fool
其它代码不变,修改/fool/Loader.php
添加$prefixLengthsPsr4
:变量的结构是,按注册的命名空间的首字母划分,存了其长度
Array
(
[a] => Array
(
[app\] => 4
[aaaaa\] => 6
)
[f] => Array
(
[fool\] => 5
)
)
addPsr4()
和findFile()
方法也改成对应那套
private static $prefixLengthsPsr4 = [];
private static function addPsr4($prefix, $paths, $prepend = false)
{
if (!isset(self::$prefixDirsPsr4[$prefix])) {
// 注册新的命名空间
self::$prefixDirsPsr4[$prefix] = (array) $paths;
// 记录前缀长度
$length = strlen($prefix);
if ('\\' !== $prefix[$length - 1]) {
// PSR-4规范,非类的命名空间应该以\结尾
echo 'A non-empty PSR-4 prefix must end with a namespace separator.';
}
self::$prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
} else {
// 为已有命名空间添加对应目录
self::$prefixDirsPsr4[$prefix] = $prepend ?
array_merge((array) $paths, self::$prefixDirsPsr4[$prefix]) :
array_merge(self::$prefixDirsPsr4[$prefix], (array) $paths);
}
}
private static function findFile($class)
{
// 先直接 命名空间 转成 路径
$logicalPathPsr4 = strtr($class, '\\', DS) . EXT;
// 根据(命名空间 与 目录)映射 替换前缀
$first = $class[0];
if (isset(self::$prefixLengthsPsr4[$first])) {
foreach (self::$prefixLengthsPsr4[$first] as $prefix => $length) {
if (0 === strpos($class, $prefix)) {
foreach (self::$prefixDirsPsr4[$prefix] as $dir) {
if (is_file($file = $dir . DS . substr($logicalPathPsr4, $length))) {
return $file;
}
}
}
}
}
return false;
}
笔者也还没能理解这样写好处在哪,猜想是注册时,即新增数据时,添加索引,查找时,根据索引找效率会更高,(自动加载确实应该考虑效率)。但findFile时始终要遍历去找,与P2相比,貌似并不能减少循环次数。
上面P2,P3基本没问题了,只要给类定义好命名空间,不管类所在文件放在哪,只要将命名空间注册,用前use下,就能用了。
但还能优化,从效率方面考虑。
如,当框架成型,每次请求运行,都必须要找自定义异常处理类,日志类等,框架目录结果基本是固定的,但现在的方式对这种命名空间与目录确定的关系,还是要遍历去找, 是否觉得这部分遍历是多余的?同理,当项目开发完,大部分文件只需小改,文件路径不会大改,是否可以省去遍历查找。
答案是肯定的:只需记录完整命名空间 => 文件路径。下面第一点就是相应处理。下面的第二点是可简单理解为设置默认目录,如果都找不到,就去默认目录里找。第三点:我目前还没用过,看tp5有就也加上去了。
下面是P4的所有优化:
在/fool/Loader.php
添加$classMap
变量:存储完整命名空间 与 文件路径 的映射关系,结构是:
Array
(
[TestClassMap] => /data/autoload/p4/classMap/TestClassMap.php
)
添加public的注册类名映射方法addClassMap()
findFile()
时,优先通过$classMap
判断。
private static $classMap = [];
public static function addClassMap($class, $map = '')
{
if (is_array($class)) {
self::$classMap = array_merge(self::$classMap, $class);
} else {
self::$classMap[$class] = $map;
}
}
private static function findFile($class)
{
// 类库映射 具体指定的,优先级最高
if (isset(self::$classMap[$class])) {
return self::$classMap[$class];
}
// 遍历prefixDirsPsr4找...
// 找不到记录一下映射为false, 并返回false
return self::$classMap[$class] = false;
}
在/classMap/TestClassMap.php
,加
class TestClassMap
{
public function work()
{
echo "this is classMap class A function work
\n";
}
}
在index.php
注册类名映射,并运行之
// 注册类库映射
Loader::addClassMap('TestClassMap', ROOT_PATH . 'classMap/TestClassMap.php');
$tcp = new TestClassMap();
$tcp->work();
// 输出
this is classMap class A function work
在/fool/Loader.php
添加,$fallbackDirsPsr4
结构:
Array
(
[0] => /data/autoload/p4/extend
)
private static $fallbackDirsPsr4 = [];
public static function register()
{
// ...
// 自动加载extend目录
self::$fallbackDirsPsr4[] = rtrim(EXTEND, DS);
}
private static function findFile($class)
{
// 类库映射 查找,优先级最高
// ...
// 根据prefixDirsPsr4找
// ...
// 指定目录找不到 从PSR-4回退目录(也可理解为默认目录)找
foreach (self::$fallbackDirsPsr4 as $dir) {
if (is_file($file = $dir . DS . $logicalPathPsr4)) {
return $file;
}
}
// 找不到记录一下映射为false, 并返回false
// ...
}
添加/extend/e1/A.php
,写上
namespace e1;
class A
{
public function work()
{
echo "this is extend e1 class A function work
\n";
}
}
在/index.php
// new扩展目录的类
use e1\A as eA;
$e = new eA();
$e->work();
运行,输出
this is extend e1 class A function work
在/fool/Loader.php
,添加$namespaceAlias
,结构:
Array
(
[model] => app\model
)
private static $namespaceAlias = [];
public static function addNamespaceAlias($namespace, $original = '')
{
if (is_array($namespace)) {
self::$namespaceAlias = array_merge(self::$namespaceAlias, $namespace);
} else {
self::$namespaceAlias[$namespace] = $original;
}
}
public static function autoload($class)
{
// 检测命名空间别名,若匹配,则返回(并不加载)后面再进来findFile后加载
if (!empty(self::$namespaceAlias)) {
$length = strpos($class, '\\');
$prefix = substr($class, 0, $length);
if (isset(self::$namespaceAlias[$prefix])) {
$original = self::$namespaceAlias[$prefix] . substr($class, $length);
if (class_exists($original)) {
return class_alias($original, $class, false);
}
}
}
if ($file = self::findFile($class)) {
return require $file;
}
return false;
}
在/app/model/Goods.php
加
namespace app\model;
class Goods
{
public function getList()
{
echo "this is model Goods function getList
\n";
}
}
在/index.php
,加
// 添加别名
Loader::addNamespaceAlias('model', 'app\model');
use model\Goods; // 使用了别名找类
$goods = new Goods();
$goods->getList();
运行之,输出
this is model Goods function getList
添加composer的自动加载处理
添加PSR0规范的处理
P4基本就能用了,添加composer处理和PSR-0处理以后有时间再写了。简单看了composer的Loader.php
,大概也是这回事。而PSR-0,感觉写了相关处理也用不上。
额外写一句:php的自动加载花了2天多来理解,写demo并总结,以后看其它语言的自动加载,理解应该不难了。
通过Loader类的属性来总结下吧,实在无力写了。
重要的:
prefixDirsPsr4
classMap
次要的:
prefixLengthsPsr4
fallbackDirsPsr4
namespaceAlias
github源码~