PHP自动加载机制
在没有使用自动加载的代码中,我们总是能看到一堆的include/require,同时你还需要考虑加载文件的顺序,如果先加载的文件中使用了后加载文件中的类或函数,那程序肯定不能正常运行。这时,自动加载机制就能够派上用场了,使用自动加载的好处是不用手动去include或require文件,也不用考虑加载文件的先后顺序,由程序去决定什么时候加载哪些文件。
实现自动加载
先来看一段没有使用自动加载的代码
require 'inc/animals/Dog.php';
$object = new Dog();
$object->setName('阿汪');
echo $object->say();
在没有自动加载时,我们需要将Dog.php手动require进来,才能使用其中的Dog类。接下来,将require注释掉并编写如下代码
spl_autoload_register(function ($className) {
echo $className;
});
$object = new Dog();
$object->setName('阿汪');
echo $object->say();
再次执行代码,在抛出致命错误之前输出了Dog,不难发现,这就是我们需要的类名,接着我们来实现一下这个自动加载。
spl_autoload_register(function ($className) {
$file = 'inc/animals/' . $className. '.php';
if (is_file($file)) {
require $file;
}
});
这样,就简单地利用spl_autoload_register函数实现了一个文件自动加载功能。
命名空间与自动加载
很多人搞不清楚命名空间与自动加载的关系。命名空间只是允许在不同命名空间下存在相同的类名或函数名,在使用了命名空间后,类全限定名就变为“命名空间\类名”,某些自动加载机制利用了命名空间的这个特性,将命名空间名称对应为目录名称,再配合spl_autoload_register函数实现文件自动加载。
PSR自动加载规范
PSR(传送门)提出的自动加载规范有两种:PSR-0、PSR-4。
PSR-0
PSR-0自动加载规范要求命名空间与目录一一对应,如
\Doctrine\Common\IsolatedClassLoader => /path/to/project/lib/vendor/Doctrine/Common/IsolatedClassLoader.php
或者使用下划线分割最后一层目录,如
\namespace\package\Class_Name => /path/to/project/lib/vendor/namespace/package/Class/Name.php
PSR官网关于PSR-0的介绍中提供了一个示例,这里我写一个更加直白的代码,方便大家理解:
spl_autoload_register(function ($className) {
// 去掉左边的反斜线,统一字符串格式
$className = ltrim($className, '\\');
// 替换反斜线和下划线为目录分隔符
$fileName = str_replace(['\\', '_'], DIRECTORY_SEPARATOR, $className);
// 拼接文件名
$fileName .= '.php';
// 加载文件
require $fileName;
});
PSR官网示例
function autoload($className)
{
$className = ltrim($className, '\\');
$fileName = '';
$namespace = '';
if ($lastNsPos = strrpos($className, '\\')) {
$namespace = substr($className, 0, $lastNsPos);
$className = substr($className, $lastNsPos + 1);
$fileName = str_replace('\\', DIRECTORY_SEPARATOR, $namespace) . DIRECTORY_SEPARATOR;
}
$fileName .= str_replace('_', DIRECTORY_SEPARATOR, $className) . '.php';
require $fileName;
}
spl_autoload_register('autoload');
官网的示例有单独拆分命名空间和类名的步骤,然后再拼接文件名,分得更加细致。
PSR-4
PSR-4自动加载规范,在指定了前缀与对应的目录后,允许命名空间与目录不对应,并且允许同前缀的命名空间在不同目录下,如
类全限定名:\Aura\Web\Response\Status
命名空间前缀:Aura\Web
映射路径:./path/to/aura-web/src/
最终加载的类文件:./path/to/aura-web/src/Response/Status.php
类全限定名:\Acme\Log\Writer\File_Writer
命名空间前缀:Acme\Log\Writer
映射路径:./acme-log-writer/lib/
最终加载的类文件:./acme-log-writer/lib/File_Writer.php
PSR官网提供两中方式的实现,闭包实现和类实现。闭包实现我个人认为不太实用,如果目录结构太复杂,就需要写很多个类似的闭包函数,所以这里重点说一下类实现。大致思路如下
由后向前依反斜线所在位置每次截取一次,将类全限定名拆分出命名空间前缀和类名/类限定名,如“application\extend\JWT”,第一次拆分为“application\extend”、“JWT”,第二次拆分为“application”、“extend\JWT”,再根据前缀和类名/类限定名去查找对应的路径并加载文件。
贴上我简化过的代码
class Loader
{
/**
* 前缀数组
* @var array
*/
private $prefixes = [];
/**
* 将函数注册到自动加载函数队列
*/
public function register()
{
spl_autoload_register([$this, 'loadClass']);
}
/**
* 命名空间前缀映射
* @param $prefix
* @param $dir
*/
public function addNamespace($prefix, $dir)
{
// 规范前缀格式
$prefix = trim($prefix, '\\');
// 规范路径格式
$dir = rtrim($dir, '/');
if (isset($this->prefixes[$prefix])) {
$this->prefixes[$prefix][] = $dir;
} else {
$this->prefixes[$prefix] = [$dir];
}
}
/**
* 根据类全限定名加载文件
* @param $className
* @return bool
*/
private function loadClass($className)
{
$prefix = $className;
// 逐层取出前缀和类名/类限定名去尝试加载文件
while (($pos = strrpos($prefix, '\\')) !== false) {
// 从开始位置截取字符串到最后一个反斜线作为前缀
$prefix = substr($prefix, 0, $pos);
// 从最后一个反斜线位置开始截取到类全限定名末尾作为类名/类限定名
$name = substr($className, $pos + 1);
if ($this->loadFile($prefix, $name)) {
return true;
}
// 删除最后一个反斜线,进入下一次循环
$prefix = rtrim($prefix, '\\');
}
return false;
}
/**
* 根据前缀和类名/类限定名加载文件
* @param $prefix
* @param $className
* @return bool
*/
private function loadFile($prefix, $className)
{
// 如果前缀被指定,遍历前缀映射的所有路径,尝试加载文件
if (isset($this->prefixes[$prefix])) {
foreach ($this->prefixes[$prefix] as $key => $value) {
$fileName = $value . '/' . $className . '.php';
if (is_file($fileName)) {
require $fileName;
return true;
}
$fileName = null;
}
}
return false;
}
}
添加映射并调用register方法注册自动加载方法
require 'Loader.php';
$loader = new Loader();
$loader->addNamespace('inc\\top\\', './inc/animals/');
$loader->register();
$object = new \inc\top\Dog();
$object->setName('阿汪');
echo $object->say();
上面示例调用Dog类加载的文件为./inc/animals/Dog.php。不难发现,PSR-4规范完整地包含了PSR-0规范,也可以说是对PSR-0自动加载规范的补充。
使用Composer实现自动加载
在项目的根目录创建composer.json,这里以PSR-4为例(传送门),内容如下
{
"autoload": {
"psr-4": {
"inc\\top\\": "inc/animals/"
}
}
}
创建完成后,在根目录执行命令
composer install
命令执行结束后,composer在根目录创建了vendor目录(如果没有指定vendor目录的位置,那么就是在根目录),打开vendor目录,有一个autoload.php文件,在我们的代码中加载这个文件
require 'vendor/autoload.php';
$object = new \inc\top\Dog();
$object->setName('阿汪');
echo $object->say();
再次执行代码,没有报错。
注:1、PSR-0自动加载规范现被标记为已弃用,取而代之的是PSR-4自动加载规范。2、以上示例为了更易于理解,代码均有所简化。