PHP自动加载下——PSR4

1.先来介绍一下PSR规范

PHP-FIG,它的网站是:www.php-fig.org。就是这个联盟组织发明和创造了PSR规范,其中自动加载涉及其中两个规范,一个是PSR0,一个是PSR4, PSR0规范已经过时了,官方有提示,现在主要是用PSR4规范定义自动加载标准。

2.PRS4简介

这个 PSR 描述的是通过文件路径自动载入类的指南;它作为对 PSR-0 的补充;根据这个 指导如何规范存放文件来自动载入;
术语「类」是一个泛称;它包含类,接口,traits 以及其他类似的结构;

完全限定类名应该类似如下范例:

()*
完全限定类名必须有一个顶级命名空间(Vendor Name);
完全限定类名可以有多个子命名空间;
完全限定类名应该有一个终止类名;
下划线在完全限定类名中是没有特殊含义的;
字母在完全限定类名中可以是任何大小写的组合;
所有类名必须以大小写敏感的方式引用;

当从完全限定类名载入文件时:

在完全限定类名中,连续的一个或几个子命名空间构成的命名空间前缀(不包括顶级命名空间的分隔符),至少对应着至少一个基础目录。
在「命名空间前缀」后的连续子命名空间名称对应一个「基础目录」下的子目录,其中的命名 空间分隔符表示目录分隔符。子目录名称必须和子命名空间名大小写匹配;
终止类名对应一个以 .php 结尾的文件。文件名必须和终止类名大小写匹配;
自动载入器的实现不可抛出任何异常,不可引发任何等级的错误;也不应返回值;

完全限定类名 命名空间前缀 基础路径 完全路径
\Acme\Log\Writer\File_Writer Acme\Log\Write ./acme-log-writer/lib/ ./acme-log-writer/lib/File_Writer.php
\Aura\Web\Response\Status Aura\Web /path/to/aura-web/src/ /path/to/aura-web/src/Response/Status.php
\Symfony\Core\Request Symfony\Core ./vendor/Symfony/Core/ ./vendor/Symfony/Core/Request.php
\Zend\Acl Zend /usr/includes/Zend/ /usr/includes/Zend/Acl.php

大家注意看第二列和第四列,命名空间前缀对应基础路径,命名空间前缀之后的子命名空间必须对应代码目录(类名必须是PHP文件)

3.优化自动加载方法

上一节中封装自动加载的方法比较简单,无法自动加载带命名空间的类

spl_autoload_register(function ($class) {

    // 命名空间前缀
    $prefix = 'Foo\\Bar\\';

    // 命名空间前缀对应的基础目录
    $base_dir = __DIR__ . '/src/';

    // 检查new的类是否有命名空间前缀
    $len = strlen($prefix);
    if (strncmp($prefix, $class, $len) !== 0) {
        return;
    }

    // 获取去掉命名空间前缀后的类名
    $relative_class = substr($class, $len);

    // 将命名空间的中的分隔符替换为目录分隔符,再加上基础目录和.php后缀,最终拼接成
    // 文件路径
    $file = $base_dir . str_replace('\\', '/', $relative_class) . '.php';

    // 如果文件存在则require
    if (file_exists($file)) {
        require $file;
    }
});

但是上面的方法只能适用固定的命名空间前缀,不能通用。

4、再次优化通用自动加载方法

<?php
namespace Example;

/**
 * 下面这个例子实现了一个命名空间前缀对应多个基础目录
 *
 * 现在我们的目录结构是下面这样:
 *
 *     /demo/autoload/
 *          controller/
 *             DemoController.php              # Foo\Bar\DemoController
 *             Admin/
 *                 AdminController.php         # Foo\Bar\Admin\AdminController
 *          model/
 *             DemoModel.php                   # Foo\Bar\DemoModel
 *             Admin/
 *                 AdminModel.php              # Foo\Bar\Admin\AdminModel
 *
 * Foo\Bar分别对应基础路径 /demo/autoload/controller 和 /demo/autoload/model
 */
class Psr4AutoloaderClass
{
    /**
     * 一个数组,key为命名空间前缀,值为基础路径
     *
     * @var array
     */
    protected $prefixes = array();

    /**
     * 封装自动加载函数
     *
     * @return void
     */
    public function register()
    {
        spl_autoload_register(array($this, 'loadClass'));
    }

    /**
     *
     * 添加一个基础路径对应一个命名空间前缀
     *
     * @param string $prefix 命名空间前缀.
     * @param string $base_dir 命名空间类文件的基础路径
     * @param bool true为往数组头部添加元素,false为往数组尾部添加元素
     * @return void
     */
    public function addNamespace($prefix, $base_dir, $prepend = false)
    {
        // 去掉左边的\
        $prefix = trim($prefix, '\\') . '\\';

        // 规范基础路径
        $base_dir = rtrim($base_dir, DIRECTORY_SEPARATOR) . '/';

        // 初始化数组
        if (isset($this->prefixes[$prefix]) === false) {
            $this->prefixes[$prefix] = array();
        }

        // 将命名空间前缀和基础路径存入数组
        if ($prepend) {
            array_unshift($this->prefixes[$prefix], $base_dir);
        } else {
            array_push($this->prefixes[$prefix], $base_dir);
        }
    }

    /**
     * 真正包含文件方法,将给到类名文件包含进来
     *
     * @param string $class 全限定类名(包含命名空间).
     * @return 成功将返回文件路径,失败则返回false
     */
    public function loadClass($class)
    {
        $prefix = $class;
        //查找$prefix最后一个\的位置,看看最后一个\之前的字符串是否在$this->prefixes中
        //如果不存在则继续查询上一个\的位置,获取上一个\之前的字符串是否在$this->prefixes中
        //如果循环结束还是没有找到则返回false
        while (false !== $pos = strrpos($prefix, '\\')) {
            $prefix = substr($class, 0, $pos + 1);

            $relative_class = substr($class, $pos + 1);

            $mapped_file = $this->loadMappedFile($prefix, $relative_class);
            if ($mapped_file) {
                return $mapped_file;
            }

            //去掉右边的\
            $prefix = rtrim($prefix, '\\');
        }

        return false;
    }

    /**
     * 如果参数中的$prefix在$this->prefixes中存在,那么将循环$this->prefixes[$prefix]里的value(基础路径)
     * 之后拼接文件路径,如果文件存在将文件包含进来
     *
     * @param string $prefix 命名空间前缀.
     * @param string $relative_class 真正的类名(不包含命名空间路径的类名).
     * @return mixed 包含成功返回文件路径,否则返回false
     */
    protected function loadMappedFile($prefix, $relative_class)
    {
        // 检查数组中是否有$prefix这个key
        if (isset($this->prefixes[$prefix]) === false) {
            return false;
        }

        // 将数组中所有的基础路径中的文件包含进来
        foreach ($this->prefixes[$prefix] as $base_dir) {

            // 拼接文件绝对路径
            $file = $base_dir
                . str_replace('\\', '/', $relative_class)
                . '.php';

            // 如果文件存在则包含进来
            if ($this->requireFile($file)) {
                // 返回文件路径
                return $file;
            }
        }

        // 没有找到文件
        return false;
    }

    /**
     *如果文件存在则包含进来.
     *
     * @param string $file 文件路径.
     * @return bool
     */
    protected function requireFile($file)
    {
        if (file_exists($file)) {
            require $file;
            return true;
        }
        return false;
    }
}
posted @ 2017-09-14 18:32  scofi  阅读(1143)  评论(0编辑  收藏  举报