我们调用一个类方法时,一定要先把这个类所在的php文件include(require)进来,然后才能调用。
我们先来看初始化文件init.php:
<?php
/**
* 统一初始化
*/
// 定义项目路径
defined('API_ROOT') || define('API_ROOT', dirname(__FILE__) . '/..');
// 引入composer
require_once API_ROOT . '/vendor/autoload.php';
// 时区设置
date_default_timezone_set('Asia/Shanghai');
// 引入DI服务
include API_ROOT . '/config/di.php';
di.php文件执行基本类的注入操作,在这里面会new相关的基础类。在new之前肯定要先include类所在的php文件,所以在di.php之前先包含了autoload.php作自动加载。autoload.php主体代码如下:
public static function getLoader()
{
call_user_func(
\Composer\Autoload\ComposerStaticInit6835957bc29912b8a4cd2dd758af2e80::getInitializer($loader)
);
$loader->register(true);
return $loader;
}
第一步getInitializer方法是把框架整体目录在系统中的位置以映射方法注入到加载类的属性中。我们来看这个getInitializer方法:
public static function getInitializer(ClassLoader $loader)
{
return \Closure::bind(function () use ($loader) {
$loader->prefixLengthsPsr4 = ComposerStaticInit6835957bc29912b8a4cd2dd758af2e80::$prefixLengthsPsr4;
$loader->prefixDirsPsr4 = ComposerStaticInit6835957bc29912b8a4cd2dd758af2e80::$prefixDirsPsr4;
}, null, ClassLoader::class);
}
通过\Closure::bind把ComposerStaticInit6835957bc29912b8a4cd2dd758af2e80的两个属性指定到加载类的对应属性。这两个属性如下:
public static $prefixLengthsPsr4 = array (
'P' =>
array (
'Phalapi\\Auth\\' => 13,
'PhalApi\\Task\\' => 13,
'PhalApi\\Redis\\' => 14,
'PhalApi\\QrCode\\' => 15,
'PhalApi\\PHPMailer\\' => 18,
'PhalApi\\NotORM\\' => 15,
'PhalApi\\CLI\\' => 12,
'PhalApi\\' => 8,
'PHPMailer\\PHPMailer\\' => 20,
),
'M' =>
array (
'Medoo\\' => 6,
),
'A' =>
array (
'App\\' => 4,
),
);
public static $prefixDirsPsr4 = array (
'Phalapi\\Auth\\' =>
array (
0 => __DIR__ . '/..' . '/phalapi/auth/src',
),
'PhalApi\\Task\\' =>
array (
0 => __DIR__ . '/..' . '/phalapi/task/src',
),
'PhalApi\\Redis\\' =>
array (
0 => __DIR__ . '/..' . '/phalapi/redis/src',
),
'PhalApi\\QrCode\\' =>
array (
0 => __DIR__ . '/..' . '/phalapi/qrcode/src',
),
'PhalApi\\PHPMailer\\' =>
array (
0 => __DIR__ . '/..' . '/phalapi/phpmailer/src',
),
'PhalApi\\NotORM\\' =>
array (
0 => __DIR__ . '/..' . '/phalapi/notorm/src',
),
'PhalApi\\CLI\\' =>
array (
0 => __DIR__ . '/..' . '/phalapi/cli/src',
),
'PhalApi\\' =>
array (
0 => __DIR__ . '/..' . '/phalapi/kernal/src',
),
'PHPMailer\\PHPMailer\\' =>
array (
0 => __DIR__ . '/..' . '/phpmailer/phpmailer/src',
),
'Medoo\\' =>
array (
0 => __DIR__ . '/..' . '/catfan/medoo/src',
),
'App\\' =>
array (
0 => __DIR__ . '/../..' . '/src/app',
),
);
框架的所有主体目录都在这里了。然后我们再来看$loader->register(true);执行了什么操作:
public function register($prepend = false)
{
spl_autoload_register(array($this, 'loadClass'), true, $prepend);
}
public function loadClass($class)
{
if ($file = $this->findFile($class)) {
includeFile($file);
return true;
}
}
我们看到,register主要是把loadClass方法注册为类的自动加载器。
当我们调用一个类时,他会通过这个方法自动加载。
我们看到,loadClass里主要执行了两步操作,先找到文件,然后再include进来
简单点,先看includeFile方法:
function includeFile($file)
{
include $file;
}
就是包含找到的文件,现在看如何索引出被包含文件
private function findFileWithExtension($class, $ext)
{
// PSR-4 lookup
$logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext;
$first = $class[0];
if (isset($this->prefixLengthsPsr4[$first])) {
$subPath = $class;
while (false !== $lastPos = strrpos($subPath, '\\')) {
$subPath = substr($subPath, 0, $lastPos);
$search = $subPath . '\\';
if (isset($this->prefixDirsPsr4[$search])) {
$pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1);
foreach ($this->prefixDirsPsr4[$search] as $dir) {
if (file_exists($file = $dir . $pathEnd)) {
return $file;
}
}
}
}
}
// PSR-4 fallback dirs
foreach ($this->fallbackDirsPsr4 as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) {
return $file;
}
}
// PSR-0 lookup
if (false !== $pos = strrpos($class, '\\')) {
// namespaced class name
$logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1)
. strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR);
} else {
// PEAR-like class name
$logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext;
}
if (isset($this->prefixesPsr0[$first])) {
foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) {
if (0 === strpos($class, $prefix)) {
foreach ($dirs as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
return $file;
}
}
}
}
}
// PSR-0 fallback dirs
foreach ($this->fallbackDirsPsr0 as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
return $file;
}
}
// PSR-0 include paths.
if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) {
return $file;
}
return false;
}
主要是这个方法,可以看到,是从之前绑定到loadClass类中的两个保存框架所有主体目录的属性里(映射),搜索出被调用类的实际所以目录,然后跟传入的带有命令空间类一组装,就是类所在php文件。然后判断文件存在时,返回文件路径,方法通过include引用.
比如:
$this->model = new \App\Model\GroupUser();
自动加载时就会根据这个类路径(App\Model\GroupUser)去之前定义的框架基本目录映射所在的两个loadClas属性去去找,目录范围由小往大的找,比如这里,属性里有App索引对应的值,但是没有App\Model目录对应的值,先找App\Model目录在索引列表里没有,这里没有找到,所以就直接找App目录.这里找到
'App\\' =>
array (
0 => __DIR__ . '/../..' . '/src/app',
),这一项(其实是先根据首字母去第一个映射属性里找:
'A' =>
array (
'App\\' => 4,
),判断对应的路径是否存在)
然后取出值:__DIR__ . '/../..' . '/src/app',去掉重复的路径app,所以就剩下\Model\GroupUser.php
然后判断:
if (file_exists($file = $dir . $pathEnd)) {
return $file;
}
这样就索引到类文件了.当然这是在类的namespace完全遵从目录结构来的。
有正面一种情况,当我定义一个类,同样是$this->model = new \App\Model\EroupUser();但其实我的EroupUser.php文件是放在 Model下的Task目录.这样上面就索引不到.
src\app\Model\EroupUser.php文件肯定是不存在的。看我的错误日记:
2019-11-01 15:25:50|ERROR|App.Home_Site.Test|Error: Class 'App\Model\EroupUser' not found in D:\home\www\ecsapi\src\app\Api\Home\Site.php:132