在没有使用自动加载的代码中,我们总是能看到一堆的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、以上示例为了更易于理解,代码均有所简化。

标签: php, 自动加载

添加新评论