> ### ThinkPHP实现类的加载核心函数 **spl_autoload_register()** 首先我们来看看魔术方法\_\_autoload(),这是一个自动加载函数,在PHP5中,当我们实例化一个未定义的类时,就会触发此函数。 Goods类 ``` class Goods { public function order() { return 'phone'.PHP_EOL; } } ``` test_autoload.php 文件 ``` function __autoload($class_name = "") { echo 'class:' . $class_name . PHP_EOL; include "./{$class_name}.php"; } $goods = new Goods(); echo $goods->order(); ``` 执行结果 ``` bash-5.0# php test_autoload.php class:Goods phone ``` cli 模式运行 `test_autoload.php` 后正常打印 phone。在` test_autoload.php` 中,由于没有包含Goods.php,在实例化Goods类时,自动调用`_autoload`函数,参数`$class_name`的值即为类名`Goods`,此时`Goods`就被引进来了。在面向对象中这种方法经常使用,可以避免书写过多的引用文件。 >[danger] 注意:`__autoload` 自PHP 7.2.0起已弃用此功能 ,[更多了解/function.autoload.php](https://www.php.net/manual/zh/function.autoload.php) test_autoload_register.php 文件 ``` spl_autoload_register('autoload', true, true); function autoload($class_name = '') { echo 'class:' . $class_name . PHP_EOL; include "./{$class_name}.php"; } $goods = new Goods(); echo $goods->order(); ``` 执行结果 ``` bash-5.0# php test_autoload_register.php class:Goods phone ``` 分析: 将魔术方法`__autoload`换成`autoload`函数。`autoload()`只是一个普通的函数,不会像`__autoload`自动触发,这时`spl_autoload_register()`就起作用了,它告诉PHP碰到没有定义的类就执行`autoload()`函数。 >[warning] `spl_autoload_register` — 注册给定的函数作为 __autoload 的实现,[更多]([https://www.php.net/manual/zh/function.spl-autoload-register.php](https://www.php.net/manual/zh/function.spl-autoload-register.php)) > ### 入口文件 在入口文件中引入了 base.php ``` namespace think; // 载入Loader类 require __DIR__ . '/library/think/Loader.php'; // 注册自动加载 Loader::register(); ... ``` Loader::register() 方法 ``` // 注册自动加载机制 public static function register($autoload = '') { // 注册系统自动加载 spl_autoload_register($autoload ?: 'think\\Loader::autoload', true, true); // 变量值:/var/www/wiot.tinywan.com/ $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'; //require $rootPath . 'wiot.tinywan.com/class_autoload/Goods.php'; // 引入自定义类 $declaredClass = get_declared_classes(); // 返回由当前文件中已引入类的名字组成的数组 // $declaredClass数组中的最后一个Composer\Autoload\ComposerStaticInitafad3d1f17f029da67a39c37bf199ba7 $composerClass = array_pop($declaredClass); foreach (['prefixLengthsPsr4', 'prefixDirsPsr4', 'fallbackDirsPsr4', 'prefixesPsr0', 'fallbackDirsPsr0', 'classMap', 'files'] as $attr) { if (property_exists($composerClass, $attr)) { // 该方法用于检测类是否存在指定的属性 self::${$attr} = $composerClass::${$attr}; // 存在则将composer安装的类的私有属性,赋值给本类对应的私有属性 } } } else { self::registerComposerLoader(self::$composerPath); } } // 注册命名空间定义 将think和traits放到 prefixLengthsPsr4 和 prefixDirsPsr4属性中 self::addNamespace([ 'think' => __DIR__, 'traits' => dirname(__DIR__) . DIRECTORY_SEPARATOR . 'traits', // 使用dirname()方法返回路径的目录部分 'scource' => dirname(__DIR__) . DIRECTORY_SEPARATOR . 'scource', // 实现自定义目录下面的类自动加载 ]); // 加载类库映射文件 // 1、默认情况下在runtime目录下是没有classmap.php的 // 2、可通过命令 php think optimize:autoload 生成 可以提升性能 if (is_file($rootPath . 'runtime' . DIRECTORY_SEPARATOR . 'classmap.php')) { self::addClassMap(__include_file($rootPath . 'runtime' . DIRECTORY_SEPARATOR . 'classmap.php')); } // 自动加载extend目录 self::addAutoLoadDir($rootPath . 'extend'); // 实现自定义目录下面的类自动加载 self::addAutoLoadDir($rootPath . 'scource'); } ``` 分析: 1、base.php 中调用`register`方法时传参为空,所以`$autoload = ''`,所以Loader类会调用本类的`autoload()`方法,这里跟上面将的原理是一样的,`autoload()`方法我们后面分析。 ``` // 自动加载 public static function autoload($class) { if (isset(self::$classAlias[$class])) { return class_alias(self::$classAlias[$class], $class); } if ($file = self::findFile($class)) { // Win环境严格区分大小写 if (strpos(PHP_OS, 'WIN') !== false && pathinfo($file, PATHINFO_FILENAME) != pathinfo(realpath($file), PATHINFO_FILENAME)) { return false; } __include_file($file); return true; } } ``` 2、composer安装的扩展类库加载,会找到`composer/autoload_static.php`,这里可以引入我们想要自动加载的类,例如代码中引入了我们刚刚自定义的`Good`类,这是引入自定义类的一种方法,后面我们还会介绍其他方法。 3、`$declaredClass = get_declared_classes()` 返回一个数组,数组是当前文件中已引入类的名字,可以自行断点调试,这里我贴上部分结果 ``` Array ( [0] => stdClass [1] => Exception [2] => ErrorException [3] => Error [74] => ParentIterator [91] => RecursiveDirectoryIterator [105] => PDOException [106] => PDO [107] => PDOStatement [133] => Redis [134] => RedisArray [135] => RedisCluster [136] => RedisException [137] => RedisClusterException [138] => AMQPConnection [139] => AMQPChannel [140] => AMQPQueue [141] => AMQPExchange [153] => PharException [158] => mysqli_driver [159] => mysqli [166] => Swoole\Error [192] => Swoole\Coroutine\Iterator [248] => Swoole\Redis\Server [250] => Yaconf [251] => think\Loader [252] => Composer\Autoload\ComposerStaticInit0ec10a4d3a3df1a88bd61e927cf732f4 ) ``` 4、`$composerClass = array_pop($declaredClass); `获取到`$declaredClass`数组的最后一个,然后进行一个循环,循环体中检查`autoload_static.php`类中是否存在指定的属性,存在则`composer`安装的类的私有属性赋值给本类对应的私有属性,这样Loader类的成员变`$prefixLengthsPsr4`和`$prefixDirsPsr4`都有了`autoload_static.php`中对应的值了 autoload_static.php ``` class ComposerStaticInitafad3d1f17f029da67a39c37bf199ba7 { public static $files = array ( '9b552a3cc426e3287cc811caefa3cf53' => __DIR__ . '/..' . '/topthink/think-helper/src/helper.php', ); /** * @var array * 说明: 1.数组的键是值的首字母 例如 't' 是 'think\\composer\\'和 'think\\'的首字母 * 2.对应的数字表示 'think\\composer\\'和 'think\\'的长度,注意两个\\中有一个是转义字符 不会被计算到长度中 */ public static $prefixLengthsPsr4 = array ( 't' => array ( 'think\\composer\\' => 15, 'think\\' => 6, ), 'a' => array ( 'app\\' => 4, ), ); /** * @var array * 说明:定义$prefixLengthsPsr4中定义的类的目录地址 */ public static $prefixDirsPsr4 = array ( 'think\\composer\\' => array ( 0 => __DIR__ . '/..' . '/topthink/think-installer/src', ), 'think\\' => array ( 0 => __DIR__ . '/..' . '/topthink/think-helper/src', ), 'app\\' => array ( 0 => __DIR__ . '/../..' . '/application', ), ); public static function getInitializer(ClassLoader $loader) { return \Closure::bind(function () use ($loader) { $loader->prefixLengthsPsr4 = ComposerStaticInitafad3d1f17f029da67a39c37bf199ba7::$prefixLengthsPsr4; $loader->prefixDirsPsr4 = ComposerStaticInitafad3d1f17f029da67a39c37bf199ba7::$prefixDirsPsr4; }, null, ClassLoader::class); } } ``` 4、注册命名空间定义,此时$namespace的值为 ``` Array ( [think] => /var/www/wiot.tinywan.com/thinkphp/library/think [traits] => /var/www/wiot.tinywan.com/thinkphp/library/traits [scource] => /var/www/wiot.tinywan.com/thinkphp/library/scource ) ``` 说明一下,这里的scource又是一种我们加入自定义类的方法 ``` // 注册命名空间 public static function addNamespace($namespace, $path = '') { print_r($namespace);die; if (is_array($namespace)) { foreach ($namespace as $prefix => $paths) { self::addPsr4($prefix . '\\', rtrim($paths, DIRECTORY_SEPARATOR), true); } } else { self::addPsr4($namespace . '\\', rtrim($path, DIRECTORY_SEPARATOR), true); } } ``` 循环`$namespace`调用`addPsr4`方法将`think`和`traits`放到 `prefixLengthsPsr4 `和 `prefixDirsPsr4`属性中 5、加载类库映射文件,默认情况下在`runtime`目录下是没有`classmap.php`的,可通过命令 `php think optimize:autoload `生成, 可以提升性能,当Loader类调用`findFile()`方法时,如果生成了`classmap.php`文件就直接return了,不然就要一次执行`findFile()`方法下面的查看文件的方法。 6、自动加载`extend`目录, 会将扩展目录`extend`目录地址存放在Loader类的$fallbackDirsPsr4 成员属性中,这里就衍生出我们的第三种自定义类实现自动加载的方法,我们在项目根目录中定义`scource`目录,在其中定义我们的自定义类,在定义的时候要注意命名空间。 (1)自定义目录下面的类自动加载(二级) ``` // source/tinywan/Email.php namespace tinywan; class Email { public static function send() { echo 'scource-tinywan-Email-send ' . PHP_EOL; } } ``` (2)自定义目录下面的类自动加载(一级) ``` // source/Sms.php class Sms { public static function send() { echo 'scource-SMS-send' . PHP_EOL; } } ``` (3)自定义目录下面的类自动加载(一级) ``` //thinkphp/library/source/EmailSend.php namespace source; class EmailSend { public static function send() { echo 'nikki-push' . PHP_EOL; } } ``` 测试类 ``` class Test extends Controller { /** * @desc: 类的自动加载测试用例 * 1、Email::send() 和 Sms::send() 是通过 $fallbackDirsPsr4 实现 * 2、EmailSend::send() 是通过 $prefixLengthsPsr4 $prefixDirsPsr4 实现 */ public function autoLoad() { Email::send(); \Sms::send(); EmailSend::send(); } } ```