从零开始编写一个PHP框架 系列的《自动加载模块》
项目地址:terse
前言
本来在纠结,第一步是从哪个模块开始的。后来决定了先编写一些基础类,这样能够串联整个框架。
关于自动加载,网上有很多的版本,万变不离其宗,其根本还是使用的是 spl_register_autoload
。
需求分析
- 需要实现类的自动加载
- 需要给长路径进行别名设置
自动加载
在 php.net 中,对于 spl_autoload_register 的描述如下。
bool spl_autoload_register ([ callable
$autoload_function
[, bool$throw
= true [, bool$prepend
= false ]]] )
在这里,我准备将它封装成一个方法,预留一个参数用来设置第三个参数。具体实现如下。
register();
主要实现文件的引入还是在 register
里调用的 load
方法里。
在实现过程中,也提供了根目录配置和扩展名配置。
_baseDir = $baseDir;
}
/**
* 设置扩展名
*
* @param string $ext .php
*/
public function setExt($ext)
{
$this->_ext = (string)$ext;
}
...
/**
* 自动加载函数
*
* @param string $class 命名空间
* @return bool
*/
public function load($class)
{
// 不同平台支持的正反斜杠不同,这里做一个处理
$classFormat = strtr($class, '\\', DIRECTORY_SEPARATOR);
// 无别名,拼接路径
$file = sprintf("%s%s%s%s", $this->_baseDir, DIRECTORY_SEPARATOR, $classFormat, $this->_ext);
if (is_file($file)) {
include $file;
return true;
}
return false;
}
}
别名设置
如果作为一个基础的自动加载类,上述这些已经足够。但有时候,我们目录结构比较深,但是我们不希望命名太长,或者,希望最外层小写的目录也能够在使用命名空间时用驼峰的方式去使用。那我们就需要引入别名了。
app => App
abc/tools => Tools
这里别名的算法,是参考的 Composer
的,在其基础上,做了一些减法。
计算过程如下:
1、设置别名和真实路径
2、判断真实路径最后是否有分隔符,如若没有,自主加上
2、判断别名最后是否有分隔符,如若没有,自主加上
3、记录别名和路径的关系
4、记录别名首字母+别名和别名长度的关系
这里之所以要解析首字母,是因为我们拿到命名空间的时候,并不知道别名的长度,所以这里需要先去除一部分别名映射,防止全部匹配,提升性能。
具体实现如下:
/**
* 设置别名
*
* @param $prefix 别名
* @param $path 真实路径
*/
public function setAlias($prefix, $path)
{
if (!$prefix) {
throw new \Exception('Error Alias', -__LINE__);
}
$path = rtrim($path, DIRECTORY_SEPARATOR);
$prefix = rtrim($prefix, '\\');
$path .= DIRECTORY_SEPARATOR;
$prefix .= '\\';
$this->_alias[$prefix] = $path;
$this->_aliasLength[$prefix{0}][$prefix] = strlen($prefix);
}
别名解析
设置有了,下面就是解析了。解析过程如下:
1、取命名空间的首字母
2、匹配相应的别名映射
3、遍历匹配别名
4、替换真实路径
5、返回文件路径
具体实现如下:
/**
* 根据别名获取路径
*
* @param string $class 命名空间
* @return mixed string | bool
*/
private function getAliasFile($class)
{
$classFormat = strtr($class, '\\', DIRECTORY_SEPARATOR);
$first = $class{0};
// 判断别名的首字母存不存在
if (!isset($this->_aliasLength[$first])) {
return false;
}
// 判断别名,拼接路径
foreach ($this->_aliasLength[$first] as $prefix => $length) {
if (0 !== strpos($class, $prefix)) {
continue;
}
$dir = $this->_alias[$prefix] . substr($classFormat, $length);
$file = sprintf("%s%s%s%s", $this->_baseDir, DIRECTORY_SEPARATOR, $dir, $this->_ext);
if (is_file($file)) {
return $file;
}
}
return false;
}
完整代码
*/
class AutoLoad
{
/**
* 别名和真实路径的映射
*
* @var array
*/
private $_alias = [];
/**
* 别名首字母+别名和别名长度的映射
*
* @var array
*/
private $_aliasLength = [];
/**
* 根目录路径
*
* @var array
*/
private $_baseDir = [];
/**
* 扩展类型
*
* @var string
*/
private $_ext = '.php';
function __construct($baseDir)
{
$this->_baseDir = rtrim((string)$baseDir, DIRECTORY_SEPARATOR);
}
/**
* 设置别名
*
* @param $prefix 别名
* @param $path 真实路径
*/
public function setAlias($prefix, $path)
{
if (!$prefix) {
throw new \Exception('Error Alias', -__LINE__);
}
$path = rtrim($path, DIRECTORY_SEPARATOR);
$prefix = rtrim($prefix, '\\');
$path .= DIRECTORY_SEPARATOR;
$prefix .= '\\';
$this->_alias[$prefix] = $path;
$this->_aliasLength[$prefix{0}][$prefix] = strlen($prefix);
}
/**
* 设置扩展名
*
* @param string $ext .php
*/
public function setExt($ext)
{
$this->_ext = (string)$ext;
}
/**
* 注册
*
* @param $prepend 如果是true,spl_autoload_register()会添加函数到队列之首,而不是队列尾部。
*/
public function register($prepend = false)
{
spl_autoload_register([$this, 'load'], 0, $prepend);
}
/**
* 自动加载函数
*
* @param string $class 命名空间
* @return bool
*/
public function load($class)
{
$classFormat = strtr($class, '\\', DIRECTORY_SEPARATOR);
// 根据别名获取路径
$file = $this->getAliasFile($class);
if ($file !== false) {
require $file;
return true;
}
// 无别名,拼接路径
$file = sprintf("%s%s%s%s", $this->_baseDir, DIRECTORY_SEPARATOR, $classFormat, $this->_ext);
if (is_file($file)) {
require $file;
return true;
}
return false;
}
/**
* 根据别名获取路径
*
* @param string $class 命名空间
* @return mixed string | bool
*/
private function getAliasFile($class)
{
$classFormat = strtr($class, '\\', DIRECTORY_SEPARATOR);
$first = $class{0};
// 判断别名的首字母存不存在
if (!isset($this->_aliasLength[$first])) {
return false;
}
// 判断别名,拼接路径
foreach ($this->_aliasLength[$first] as $prefix => $length) {
if (0 !== strpos($class, $prefix)) {
continue;
}
$dir = $this->_alias[$prefix] . substr($classFormat, $length);
$file = sprintf("%s%s%s%s", $this->_baseDir, DIRECTORY_SEPARATOR, $dir, $this->_ext);
if (is_file($file)) {
return $file;
}
}
return false;
}
}
结语
自动加载类的核心就是 spl_autoload_register
函数,不过我们可能在某些方面有更深层次的需求,去扩展,完善。