星星博客 »  > 

【TP5灵魂】自动加载Loader 深度分析

 

类的自动加载

首先我们拿到框,肯定要先找到框架的入口文件,Thinkphp5.1的入口文件 tp5_analyze\public\index.php

<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006-2018 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: liu21st <liu21st@gmail.com>
// +----------------------------------------------------------------------

// [ 应用入口文件 ]
namespace think;

// 加载基础文件
require __DIR__ . '/../thinkphp/base.php';

// 支持事先使用静态方法设置Request对象和Config对象

// 执行应用并响应
Container::get('app')->run()->send(); 

我们可以看到在入口文件中,他会去加载基础文件 thinkphp/base.php ,基础文件所在目录 tp5_analyze\thinkphp\base.php

<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: liu21st <liu21st@gmail.com>
// +----------------------------------------------------------------------
namespace think;

// 载入Loader类
require __DIR__ . '/library/think/Loader.php';

// 注册自动加载
Loader::register();

// 注册错误和异常处理机制
Error::register();

// 实现日志接口
if (interface_exists('Psr\Log\LoggerInterface')) {
    interface LoggerInterface extends \Psr\Log\LoggerInterface
    {}
} else {
    interface LoggerInterface
    {}
}

// 注册类库别名
Loader::addClassAlias([
    'App'      => facade\App::class,
    'Build'    => facade\Build::class,
    'Cache'    => facade\Cache::class,
    'Config'   => facade\Config::class,
    'Cookie'   => facade\Cookie::class,
    'Db'       => Db::class,
    'Debug'    => facade\Debug::class,
    'Env'      => facade\Env::class,
    'Facade'   => Facade::class,
    'Hook'     => facade\Hook::class,
    'Lang'     => facade\Lang::class,
    'Log'      => facade\Log::class,
    'Request'  => facade\Request::class,
    'Response' => facade\Response::class,
    'Route'    => facade\Route::class,
    'Session'  => facade\Session::class,
    'Url'      => facade\Url::class,
    'Validate' => facade\Validate::class,
    'View'     => facade\View::class,
]);

base.php中会去加载Loader.php这个文件或者说是类,这个类可以这么说,就是Thinkphp5自动加载的灵魂。可以看到加载的类 Loader.php,类所在目录thinkphp\library\think\Loader.php,这个类库就是Thinkphp5封装的底层基础类库,这个类库就是需要我们进行深度分析的类库。
我们可以看到,在base.php中加载完Loader.php之后,会调用Loader::register();方法,我们追踪进去可以看到这个方法

// 注册自动加载机制
public static function register($autoload = '')
{
    // 注册系统自动加载
    spl_autoload_register($autoload ?: 'think\\Loader::autoload', true, true);

    $rootPath = self::getRootPath();

    self::$composerPath = $rootPath . 'vendor' . DIRECTORY_SEPARATOR . 'composer' . DIRECTORY_SEPARATOR;

    // Composer自动加载支持
    if (is_dir(self::$composerPath)) {
        if (is_file(self::$composerPath . 'autoload_static.php')) {
            require self::$composerPath . 'autoload_static.php';

            $declaredClass = get_declared_classes();
            $composerClass = array_pop($declaredClass);

            foreach (['prefixLengthsPsr4', 'prefixDirsPsr4', 'fallbackDirsPsr4', 'prefixesPsr0', 'fallbackDirsPsr0', 'classMap', 'files'] as $attr) {
                if (property_exists($composerClass, $attr)) {
                    self::${$attr} = $composerClass::${$attr};
                }
            }
        } else {
            self::registerComposerLoader(self::$composerPath);
        }
    }

    // 注册命名空间定义
    self::addNamespace([
        'think'  => __DIR__,
        'traits' => dirname(__DIR__) . DIRECTORY_SEPARATOR . 'traits',
    ]);

    // 加载类库映射文件
    if (is_file($rootPath . 'runtime' . DIRECTORY_SEPARATOR . 'classmap.php')) {
        self::addClassMap(__include_file($rootPath . 'runtime' . DIRECTORY_SEPARATOR . 'classmap.php'));
    }

    // 自动加载extend目录
    self::addAutoLoadDir($rootPath . 'extend');
}

这个方法其实就是注册自动加载的机制。他把Thinkphp的加载与Composer的加载封装到了一个文件,我们可以看到自动加载机制的方法里面,引入了Composer包里面的autoload_static.php,并针对Composer的这种方式做自动加载,这里先不啰嗦,我们会在后续的章节中详细了解这个方法。
其他的框架在使用的时候,他们的逻辑是是一样的,都会在框架的第一步实现自动加载。

spl_autoload_register初学习

spl_autoload_register的使用

当我们去new一个找不到的class时,PHP就会去自动调用sql_autoload_resister注册的函数,这个函数通过它的参数传进去:

function load($className) {
    require $className . '.php';
}
spl_autoload_register(load);  // 将load函数注册到自动加载队列中
$db = new DB();    // 找不到DB类,就会自动去调用注册的load函数

多个spl_autoload_register的使用

spl_autoload_register是可以多次重复使用的,这一点正是解决了__autoload的短板,那么如果一个页面有多个,执行顺序是按照注册的顺序,一个一个往下找,如果找到了就停止。

function load1 ($className) {...}
function load2 ($className) {...}
...
spl_autoload_register('load1')
spl_autoload_register('load2')

 

命名空间映射

base.php

框架base.php文件,首先载入Loader类,并执行Loader的静态方法register,进行注册自动加载

// 载入Loader类
require __DIR__ . '/library/think/Loader.php';
// 注册自动加载
Loader::register();

可以自定义加载函数,并有register参数传入。

public static function register($autoload = '') {
    // 注册系统自动加载
    spl_autoload_register($autoload ?: 'think\\Loader::autoload', true, true')
}

Loader.php

由base.php中调用Loader的register静态方法。该方法首先注册一个自动加载函数。接着将框架的aplication,extend,composer的目录和各自的命名空间相映射

注册自动加载

// 注册系统自动加载
spl_autoload_register($autoload ?: 'think\\Loader::autoload', true, true);

composer自动加载支持

获取composer目录

$rootPath = self::getRootPath();    // 项目的根目录
self::$composerPath = $rootPath . 'vendor' . DIRECTORY_SEPARATOR . 'composer' . DIRECTORY_SEPARATOR;

载入composer自动加载文件,并将composer下载的安装包关系映射赋值给Loader属性。
$prefixLengthsPsr4,$prefixDirsPsr4两个属性描述了命名空间和实际目录位置的映射。当通过composer进行下载安装包时,会自动更新composer的autoload_static.php的属性

// Composer自动加载支持
if (is_dir(self::$composerPath)) {
    if (is_file(self::$composerPath . 'autoload_static.php')) {
        require self::$composerPath . 'autoload_static.php';
        // get_declared_classes函数获取当前所有载入的类
        $declaredClass = get_declared_classes();
        $composerClass = array_pop($declaredClass);

        foreach (['prefixLengthsPsr4', 'prefixDirsPsr4', 'fallbackDirsPsr4', 'prefixesPsr0', 'fallbackDirsPsr0', 'classMap', 'files'] as $attr) {
            // 将composer的autoload_static.php类的prefixLengthsPsr4,prefixDirsPsr4属性赋值给Loader的prefixLengthsPsr4, prefixDirsPsr4属性
            if (property_exists($composerClass, $attr)) {
                self::${$attr} = $composerClass::${$attr};
            }
        }
    } else {
        self::registerComposerLoader(self::$composerPath);
    }
}

由于composer的autoload_static.php中只定义了think\composerapp和其他通过composer安装的安装包命名空间对于的目录的映射。所以还需要把框架核心的think``traits命名空间的映射添加到Loader的$prefixLengthsPsr4,$prefixLengthsPsr4的属性上

// 注册命名空间定义
self::addNamespace([
    'think'  => __DIR__,
    'traits' => dirname(__DIR__) . DIRECTORY_SEPARATOR . 'traits',
]);

将类库的映射文件中的数据,载入到Loader属性$classMap。类库映射文件是需要手动生成的,里面是所有类名映射到类的绝对地址。

// 加载类库映射文件
if (is_file($rootPath . 'runtime' . DIRECTORY_SEPARATOR . 'classmap.php')) {
    self::addClassMap(__include_file($rootPath . 'runtime' . DIRECTORY_SEPARATOR . 'classmap.php'));
}

extend扩展目录的位置赋值给Loader类的$fallbackDirsPsr4。若自定义目录也想要自动加载。可以参考extend的方式。

// 自动加载extend目录
self::addAutoLoadDir($rootPath . 'extend')

最后在base.php文件中注册了类的别名。在使用一个不存在的类时,优先判断是否包含类的别名,在根据类的别名所对应的类来加载。

base.php
...
// 注册类库别名

Loader::addClassAlias([
'App' => facade\\App::class,
'Build' => facade\\Build::class,
'Cache' => facade\\Cache::class,
'Config' => facade\\Config::class,
'Cookie' => facade\\Cookie::class,
'Db' => Db::class,
'Debug' => facade\\Debug::class,
'Env' => facade\\Env::class,
'Facade' => Facade::class,
'Hook' => facade\\Hook::class,
'Lang' => facade\\Lang::class,
'Log' => facade\\Log::class,
'Request' => facade\\Request::class,
'Response' => facade\\Response::class,
'Route' => facade\\Route::class,
'Session' => facade\\Session::class,
'Url' => facade\\Url::class,
'Validate' => facade\\Validate::class,
'View' => facade\\View::class,
]);

到此,相关命名空间和目录的映射都准备好了。

类的自动加载

上节已经把命名空间和目录的映射都赋值给了Loader的相关属性了。此时在使用一个不存在的类时,就可以触发Loader的autoload方法进行自动加载了。
我们看一下上一节执行完成后,Loader类的属性值情况

class Loader {
...
$prefixLengthsPsr4 = [
"t" => [
     "think\composer\" => 15,
     "think\" => 6 ,
     "traits\" => 7
      ],
"a" => [ "app\" => 4 ]
]

$prefixDirsPsr4 = [
"think\\composer\\" => " /Users/lihui/Documents/tp/tp5.1/vendor/composer/../topthink/think-installer/src ",
"app\\" => "/Users/lihui/Documents/tp/tp5.1/vendor/composer/../../application",
"think\\" => "/Users/lihui/Documents/tp/tp5.1/thinkphp/library/think",
"traits\\" => "/Users/lihui/Documents/tp/tp5.1/thinkphp/library/traits"
]

$fallbackDirsPsr4 = ["/Users/lihui/Documents/tp/tp5.1/extend"]
...
}

autoload方法实现自动加载

在代码中使用一个不存在的类时,会触发autoload方法,首页会查找$classAlias属性,是否定义了类的别名。如果定义了类的别名和实际类的映射。则可通过类的别名直接使用该类。主要是通过php内置函数class_alias函数实现

if (isset(self::$classAlias[$class])) {
    return class_alias(self::$classAlias[$class], $class);
}

若没有定义类的别名和实际类的映射,则需要走命名空间的映射来找到实际类,并加载。

// 查找 PSR-4
$logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . '.php';

$first = $class[0];
if (isset(self::$prefixLengthsPsr4[$first])) {
    foreach (self::$prefixLengthsPsr4[$first] as $prefix => $length) {
        if (0 === strpos($class, $prefix)) {
            foreach (self::$prefixDirsPsr4[$prefix] as $dir) {
                if (is_file($file = $dir . DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $length))) {
                    return $file;
                }
            }
        }
    }
}

// 查找 PSR-4 fallback dirs
foreach (self::$fallbackDirsPsr4 as $dir) {
    if (is_file($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) {
        return $file;
    }
}

最终找到文件在系统中的位置,然后调用include 加载文件。

__include_file($file);
return true;

 

相关文章