1.先来介绍一下PSR规范
PHP-FIG,它的网站是:www.php-fig.org。就是这个联盟组织发明和创造了PSR规范,其中自动加载涉及其中两个规范,一个是PSR0,一个是PSR4, PSR0规范已经过时了,官方有提示,现在主要是用PSR4规范定义自动加载标准。
2.PRS4简介
这个 PSR 描述的是通过文件路径自动载入类的指南;它作为对 PSR-0 的补充;根据这个 指导如何规范存放文件来自动载入;
术语「类」是一个泛称;它包含类,接口,traits 以及其他类似的结构;
完全限定类名应该类似如下范例:
( )*
完全限定类名必须有一个顶级命名空间(Vendor Name);
完全限定类名可以有多个子命名空间;
完全限定类名应该有一个终止类名;
下划线在完全限定类名中是没有特殊含义的;
字母在完全限定类名中可以是任何大小写的组合;
所有类名必须以大小写敏感的方式引用;
当从完全限定类名载入文件时:
在完全限定类名中,连续的一个或几个子命名空间构成的命名空间前缀(不包括顶级命名空间的分隔符),至少对应着至少一个基础目录。
在「命名空间前缀」后的连续子命名空间名称对应一个「基础目录」下的子目录,其中的命名 空间分隔符表示目录分隔符。子目录名称必须和子命名空间名大小写匹配;
终止类名对应一个以 .php 结尾的文件。文件名必须和终止类名大小写匹配;
自动载入器的实现不可抛出任何异常,不可引发任何等级的错误;也不应返回值;
完全限定类名 | 命名空间前缀 | 基础路径 | 完全路径 |
---|---|---|---|
AcmeLogWriterFile_Writer | AcmeLogWrite | ./acme-log-writer/lib/ | ./acme-log-writer/lib/File_Writer.php |
AuraWebResponseStatus | AuraWeb | /path/to/aura-web/src/ | /path/to/aura-web/src/Response/Status.php |
SymfonyCoreRequest | SymfonyCore | ./vendor/Symfony/Core/ | ./vendor/Symfony/Core/Request.php |
endAcl | 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 # FooBarDemoController
* Admin/
* AdminController.php # FooBarAdminAdminController
* model/
* DemoModel.php # FooBarDemoModel
* Admin/
* AdminModel.php # FooBarAdminAdminModel
*
* FooBar分别对应基础路径 /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;
}
}