1. include & require
我们知道一个 A.php
文件若想引入 B.php
文件里的类,就需要通过 include / require
的方式将 B.php
引入。
这种方式对小项目来说没啥问题,但对大型项目来说,通常会包含很多公共文件,比如:Foo/Bar/Dog.php
,按照传统方式我们在每个所需的地方将这个文件引入即可,但这样会造成如下问题:
- 每个地方都要引入
Foo/Bar/Dog.php
,操作实在繁琐 - 代码量增多
- 重复粘贴容易出现残漏情况
那有没有办法解决这个问题呢?有!__autoload
就是用来解放 include / require 的。
2. __autoload
__autoload
是 php 5 以后新增的一个魔法函数,此函数在使用 new xxx
时自动触发,并传递一个 $class
的参数,这个参数就是 new xxx
中的 xxx
部分, 下面是它的用法
function __autoload($class) {
# $class = Foo\Bar\Dog
require_once $class_name . '.php';
}
$dog = new Foo\Bar\Dog();
$dog->say();
这个函数帮我们减少了许多的 include/require,但由于 __autoload
只能使用一次,假设我们不止有 Foo/Bar
这个目录,还有 Coo/Too
、Aoo/Boo
等共用目录这可怎么办? 有没有办法能让多次加载呢?有! sql_autoload_register
就是来解决这个问题的。
3. sql_autoload_register
sql_autoload_register
专门用来定义多个 __autoload
的函数,它的用法如下:
function my_autoload_1($class) {
require_once $class_name . '.php';
}
function my_autoload_2($class) {
require_once $class_name . '.php';
}
function my_autoload_3($class) {
require_once $class_name . '.php';
}
sql_autoload_register('my_autoload_1');
sql_autoload_register('my_autoload_2');
sql_autoload_register('my_autoload_3');
$dog1 = new Foo\Bar\Dog();
$dog1->say();
$dog2 = new Coo\Too\Dog();
$dog2->say();
$dog3 = new Foo\Bar\Dog();
$dog3->say();
现在多个自动加载的问题解决了,由于sql_autoload_register
既能代替 __autoload
也能实现多个 __autoload
,所以 __autoload
自然也就被 PHP 官方淘汰了。
然后这就完了吗?并没有~
每个人都可以用 sql_autoload_register
定义自己的自动加载器,而每个人的写法又是不同的,若第三方插件/框架的作者们都实现自家的自动加载器,当我们使用这些插件/框架时就得熟悉它们的引入语法,对开发者的学习成本增加了许多,后来就有了一群志同道合的人联合起来要搞一个自动加载器的规范,而这个规范就叫做 PSR-0
,全称是 PHP Standard Recommend
,大家需要统一按照这种规范来写出自己的自动加载器才算合格。
4. PSR-0
PSR-0 的规范这里我就不细说,有意者可以参考官方文档 PSR-0
这里我们重点关注实现了 PSR-0 自动加载器后写法是怎么样的?或者说,哪些比较流行的框架帮我们写好了一个 PSR-0 规范的自动加载器? 要怎么使用?这里我们就以 composer 为例子,假设我们的项目结构如下:
src
Foo
Bar
Dog.php
Coo
Too
Dog.php
test.php
接着参考 composer 文档需要在 composer.json
里进行映射配置
{
"name": "cookcyq",
"autoload": {
"psr-0": {
"Foo\\Bar": "src/",
"Coo\\Too": "src/"
}
}
}
配置后需要执行: composer durmp-auto -o
它会自动在 vendor/composer/
下生成 autoload.php
文件,我们引入这个文件就可以使用愉快的使用自动加载器了。
// test.php
require_once "vendor/composer/autoload.php";
// 提示:
// PSR-0 规范里支持 _ 下划线语法,它最终会被替换成 / ,所以下面是等价的。
$dog1 = new Foo_Bar_Dog();
$dog2 = new Foo\Bar\Dog();
$dog3 = new Coo\Too_Dog();
$dog4 = new Coo\Too\Dog();
有的小伙伴可能疑惑了,为什么会支持下划线 _ 这种形式呢?这是为了起到独立作用域作用,避免有重复名字冲突,因为 namespace
在那时还没出现呢。
嗯,到了 PSR-0 就结束了吗?然鹅并没有,这不 namespace
在不久后就出现了。
所以这才有了后来的 PSR-4 新的规范。
5. PSR-4
我认为 PSR-4 完全就是因为有了 namespace
这玩意才诞生出的新规范。
如果你还不知道什么是 namespace
可以参考我前面写过的 PHP & 理解 Namespace (命名空间)
PSR-4 与 PSR-0 有什么不同呢?
- PSR-4 不支持 _ 下划线这种写法,因为已经有了
namespace
- 在
composer.json
中 key 的结尾必须要带上\\
,如下
{
"name": "cookcyq",
"autoload": {
"psr-0": {
"Foo\\Bar": "src/",
"Coo\\Too": "src/"
},
"psr-4": {
"Foo\\Bar\\": "src/",
"Coo\\Too\\": "src/"
}
}
}
看到这里,相信你已经懂得 autoload 这个自动加载的概念以及如何在 composer 中使用它们的自动加载了,对于时间匆忙的同学也可以不用往下看,我认为这已经够用了。
如果时间充裕的话可以接着往下读。
6. 为什么 composer 不直接拥抱 PSR-4 还要兼容 PSR-0 ?
答案很明显,目前有些古老且有用的插件作者采用的还是 PSR-0
规范,
其中有些作者用的是 _ 下划线语法,所以 composer 不能一刀切。
7. composer 的 PSR-0 和 PSR-4 实现方式有啥不同?
对于新手(包括我)来说,常常找到的 PSR-0 与 PSR-4 的解释很令人疑惑。
图中的PSR-0 映射关系我能看懂,但 PSR-4 我是一脸懵逼。假设 Bar.php
文件就放到 src/Acme/Foo/Bar.php
里面,但 PSR-4 的 Acme\Foo => /src/Bar.php
的 /src/Bar.php
的这种映射关系肯定找不到 Bar.php
文件啊?于是本着好奇心便各种搜索,结果还是令人失望,大部分要么都是搬官方的例子要么都是复制别人的过来然后也不说明为什么会这样的关系,至少对我来说,这种解释是行不通的。目前我安慰自己的方式是:底层会自动帮我们找到完整的路径进行引入,然后这个映射关系不是指上面的引入文件关系,这样我心里才舒服些,于是我就在想,与其这么找,倒不如去看看源码到底是怎么帮我们引入最终的文件的,于是就有了接下来的源码分析,放心,我这里仅仅摘取最关键部分,因为其它的我也看不懂。
8. 分析 composer PSR-0 & PSR-4 实现原理
这是本案例的完整目录结构
假设 composer.json
采用 PSR-4 ,内容如下
{
"name": "cookcyq",
"license": "n",
"require": {},
"autoload": {
"psr-4": {
"Foo\\Bar\\": "vendor/foo/bar/src"
}
}
}
使用 comopser durmp-auto -o
生成以下文件:
我们重点关注里面的 ClassLoader.php
这个文件,里面包含了 PSR-0 和 PSR-4 的几个核心关键实现自动加载的属性和方法
关键属性:
class ClassLoader {
// PSR-4 关键属性
private $prefixLengthsPsr4 = array();
private $prefixDirsPsr4 = array();
// PSR-0 关键属性
private $prefixesPsr0 = array();
private $fallbackDirsPsr0 = array();
关键方法1 add / addPsr4()
:将 composer.json
里的 "psr-0": {... } 和 psr-4": {... }
内容添加关键属性里面,源码如下
public function add($prefix, $paths, $prepend = false) { ...代码与 addPsr4 差不多 }
public function addPsr4($prefix, $paths, $prepend = false)
{
if (!$prefix) {
// ...
} elseif (!isset($this->prefixDirsPsr4[$prefix])) {
$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;
} elseif ($prepend) {
// Prepend directories for an already registered namespace.
$this->prefixDirsPsr4[$prefix] = array_merge(
(array) $paths,
$this->prefixDirsPsr4[$prefix]
);
} else {
// Append directories for an already registered namespace.
$this->prefixDirsPsr4[$prefix] = array_merge(
$this->prefixDirsPsr4[$prefix],
(array) $paths
);
}
}
你可以不用看上面的代码,只需关心最终存储结构类似为:
// PSR-4==========================
$prefixLengthsPsr4 = [
"F" => [
"Foo\\Bar\\" => 6,
]
]
$prefixDirsPsr4 = [
"Foo\\Bar\\" => [
"vendor/foo/bar/src"
]
]
// PSR-0==========================
$prefixesPsr0 = [
"F" => [
"Foo\\Bar\\" => [
"vendor/foo/bar/src"
]
]
]
关键方法2 findFileWithExtension()
: 查找 PSR-0 和 PSR-4 的完整文件路径就是在这里完成的,当找到后就将其 includeFile 引入即可,整个 autoload 基本流程就是这样,关键源码如下:
private function findFileWithExtension($class, $ext)
{
$logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext;
$first = $class[0];
// ============================ PSR-4 查询 =============================================
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;
}
}
}
}
}
// ============================ PSR-0 查询 =============================================
// 支持下划线的条件语句
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;
}
}
}
}
}
return false;
}
上面的代码我们只需要找到最关键的两层循环:
PSR-4 采用: while + foreach
PSR-0 采用: foreach + foeach
因为 PSR-4 不需要 _ 下划线,以及结尾必须要带上 \\
,势必要用另外一种方式来实现找到文件路径结构,才有了这两个属性: $prefixLengthsPsr4 / $prefixDirsPsr4,然后再结合 whre +foreach 来寻找完整路径,而 PSR-0 只定义了 $prefixesPsr0 属性,所以采用了 foreach + foeach 来寻找完整路径,最终这两种方式都成功找出完整路径。
9. 好了,做个总结吧
- PSR-0 支持 _ 下划线,PSR-4 不支持
- composer.json PSR-0 后面不用加
\\
,PSR-4 后面必须加\\
剩下的在 composer.json 配置用法是完全一致的。只是查找完整文件路径方式采用不同的循环策略。
参考文献:
https://*.com/questions/24868586/what-are-the-differences-between-psr-0-and-psr-4#:~:text=The%20summary%20is%20that%20PSR,part%20following%20the%20anchor%20point.
https://my.oschina.net/sallency/blog/893518