IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> PHP知识库 -> PHP 依赖管理利器 Composer 源码解读 -> 正文阅读

[PHP知识库]PHP 依赖管理利器 Composer 源码解读

拍片至今,参与的大多项目利用 Composer 管理各种依赖,有效提高了开发效率,也源于社区活跃程度,才出现了形式各样的依赖供我这种低级开发者使用。但从未去真正了解过它的实现,给它的定位就是工具,能用就行。现在看来,似乎行不大通。所以,还是需要抽空去了解具体实现。

下载

这里采用命令操作的,如果 Windows,直接从网页端 Download ZIP 也行,然后解压。至于是否重命名,看个人意愿。

wget https://github.com/composer/composer/archive/refs/heads/master.zip 
tar -zxf master.zip && rm master.zip
mv composr-master composer && rm -rf composr-master

然后使用 Phpstorm 打开对应目录就行了。由于项目源码的原因,这里需要其他的依赖,所以,要在项目根目录执行 composer install 安装依赖。出了异常 Could not scan for classes inside "phpstan/Rules/tests/data" which does not appear to be a file nor a folder,为此,移除了 composer.json 中的 autoload-dev 节点下的 classmap 设置。不对,可以加标记,比如 composer install --no-dev 忽略 require-dev 部分也可以。

分析

目录结构

采用无序列表表示。

  • bin 有关 composer 使用到的命令脚本文件,方便测试;
  • doc 文档,顾名思义;
  • res 存放的模式;
  • src 源码部分;
  • vendor 其他依赖包,经 composer install 产生的。

bin 下的两个命令脚本

compile

将 composer 编译到一个独立文件(默认为 composer.phar)。由于这里会获取版本库最后一次提交时间(git log -n1 --pretty=%ct HEAD)作为后缀拼接,我这边是直接下载压缩包,不是 Git 仓库,所以,下载那里,估计还是得采用 Fork 模式做,当然也可以直接初始化一个仓库,顺便做个提交,似乎也行得通。我采用了第二种方式,但是 php.ini => phar.readonly 需要关闭,注意一下,否则会出错 Failed to compile phar: [UnexpectedValueException] creating archive "composer.phar" disabled by the php.ini setting phar.readonly。速度还行,已经生成了 compser.phar 压缩文件。那先看看这个编译脚本吧。
先使用 require __DIR__.'/../src/bootstrap.php' 引入启动脚本保证 use Composer\Compiler 的可用性。该类实例化后直接调用编译方法 (new Compiler())->compile(),为了表述,代码有一定压缩,当然做了捕获异常处理。接下来,进入这个编译类。
主要有三个私有属性,version 最后提交 Hash 码,branchAliasVersion 分支对应版本,在 composer.json 中配置指向 extra.branch-alias.dev-master 节点,versionDate 最后提交时间,一共有六个方法,其中五个为私有,限制比较严格,也就是说,只有编译那个方法可调用。

  • Composer.getRelativeFilePath 获取文件相对路径;
    对文件绝对路径采用两次 dirname,这里已知的,当前文件和项目根目录的距离,然后定位子串在原串位置,使用 substr_replace 替换,最后 strtr 替换特定字符。
  • Compiler.addFile 向 phar 压缩文件中添加文件;
    获取所要添加文件相对路径后,把文件流载入字符串,然后根据参数 $strip 去除空格,如果是证书文件就不用去,直接拼接。还有个特殊文件 src/Composer/Composer.php,需要替换一些字符串。最后调用 Phar.addFromString 加到压缩文件。
  • Compiler.stripWhitespace 去掉空格;
  • Compile.addComposerBin 添加 composer 文件;
  • Compile.getStub 获取末节内容;
  • Compile.compile 编译方法;
    一来就是压缩文件存在即删除,毕竟要新建。然后就是,通过仓库获取最后提交信息和标签(如果没有标签,就解析 composer.json 中获取)。添加文件的顺序也很讲究啊,依次是:composer 源码(排除了 Compiler.php/ClassLoader.php/InstalledVersions.php)=> ClassLoader.php => InstalledVersions.php => 添加 vendor 下的文件 => 扩展文件 => bin/composer => 末节 => 证书。

所以,大概也了解了生成压缩包的大概流程,但对 composer 的过程,还是不清楚。估计是要看第二个命令脚本。

composer

这里就是正常流程。不过,在非 cli/phpdbg 环境下执行 php bin/composer 会输出警告级异常,不会强行中断。有一行看不懂 setlocale(LC_ALL, 'C'),就挺尴尬,先放在这里,也问过朋友,说是设置地区信息,但是这个字符 C 。紧接着,就引入启动文件,做一些其他检测,内存限制和环境参数设置。最后还是 (new Composer\Console\Application())->run() 就完了。应该就是这个类,入口,我看到了 logo。

流程分析

应用主类 Application,位于 src/Composer/Console/Application.php,继承 Symfony 控制台应用的实现。

  • Application.__construct():应用主类构造方法;
    • 关闭 Xdebug 的输出干扰, xdebug.show_exception_trace 和 xdebug.scream;
    • 注册 php 中止函数 register_shutdown_function;
  • Application.run: 应用运行方法;
    • 重写父级 run 方法,如果没有输出流就调用 Factory::createOutput() 创建,红色高亮警告为黄字黑底的终端样式,然后继续执行父级方法,携带输出流;当然,父级方法也是做了判断,不存在就创建,注意 $input 是终端参数类 ArgvInput 的实例化。调用 configureIO() 方法通过用户参数和选项设置 IO;
    • 切入 doRun() 方法。还是有重写;
      • 一来就是检测插件标记 --no-plugins 设置属性 disablePluginsByDefault,然后检测界面是否可交互;
      • 然后将当前 IO 和 QuestionHelper(用户交互助手) 重新实例化一个终端 IO 助手 ConsoleIO,并注册到 将错误转化异常的助手 ErrorHandle;
      • 检测参数 --no-cache,如果为真,就给出提示,并把环境变量 COMPOSER_CACHE_DIR 定到 nul 或 /dev/null,主要根据系统选择的;
      • 切换工作目录,如果命令参数中有设置 -working-dir。然后,获取当前命令名称,也就是参数的第一个,比如 composer install,那么命令名称就是 install。当然,如果更换了工作目录,是要对当前目录下的 composer.json 文件存在性检测,没有的话,会给出相应提示;
      • 继续检测插件命令,这里先留着
      • 检测是否为代理命令。如果非代理模式,检测 PHP 版本,Xdebug 和超过60天没有更新,以及非 Windows 环境检测当前运行用户是否为根用户;
      • 检测系统临时目录 sys_temp_dir,在 php.ini 配置文件中设置;
      • 将当前 composer.json 文件中 scripts 节点下配置非标准命令加入到自身命令集合;
      • 检测是否显示时间,通过参数 --profile
      • 调用父级 doRun()` 方法;
        • 检测命令是否查看版本(--version / -V),如果是,直接输出;
        • 处理帮助命令;
        • 如果没有命令,就设置为 list;
        • 标记当前运行的命令,doRunCommand()` 执行完后,重置;
        • 检测是否设置调度器,没有的话,直接调用命令的执行,这里就不跟进。看看有调度的情况;
        • 如果存在调度的情况,将当前的 IO 和命令作为参数实例化 ConsoleCommandEvent。异步?
        • 如果终端命令事件可以执行,就执行。如果不能,就将状态码设置禁用。出现了异常,就调用终端异常事件类处理,最后主动调用关闭 ConsoleTerminateEvent;
      • 还原目录(如果设置了工作目录),打印时间(如果设置了 --profile),恢复错误;
    • 异常处理,如果有,渲染并设置退出代码,这里如果自动退出,还要将代码控制在256以内。到这里也就全部结束。

到这里。composer 整体的主干流程也就结束了,接下来,该是对具体命令的分析,粗略看了一下,在 src/Composer/Command 目录中大概有三十多个命令脚本文件。也可以挨着看,真要算起来,也不多。

命令分析

这里仅分析 src/Composer/Command 目录下的命令脚本文件。需要注意,php bin/composer command args 是在 composer 源码项目中调试使用的,如果是在正式环境中,直接 composer command args 即可。这里我采用在 composer 源码项目中调试。

  • about,文件 AboutCommand.php,参考命令 php bin/composer about,查看 Composer 的简要描述;
  • archive,文件 ArchiveCommand.php,参考命令 php bin/composer archive,创建一个发布包的压缩文件;
  • clear-cache,文件 ClearCacheCommand.php,参考命令 php bin/composer clear-cache,清除缓存;
  • config,文件 ConfigCommand.php,设置;
  • create-project,文件 CreateProjectCommand.php,根据包创建项目;
  • depends,文件 DependsCommand.php,参考命令 php bin/compose depends psr/log,查看对此包有相关依赖关系的包;
  • diagnose,文件 DiagnoseCommand.php,参考命令 php bin/composer diagnose,诊断系统以识别常见错误;
  • dump-autoload,文件 DumpAutoloadCommand.php,参考命令 php bin/composer dump-autoload,配置自动加载文件;
  • exec,文件 ExecCommand.php,参考命令 php bin/composer exec,执行申明的二进制或脚本;
  • fund,文件 FundCommand.php,参考命令 php bin/composer fund,了解如何维护依赖;
  • global,文件 GlobalCommand.php,参考命令 php bin/composer global command,全局运行命令;
  • browse,文件 HomeCommand.php,参考命令 php bin/composer browse psr/log,在浏览器中打开这个依赖的仓库地址;
  • init,文件 InitCommand.php,参考命令 php bin/composer init,在当前目录创建 composer.json 文件初始化 composer 仓库;
  • install,文件 InstallCommand.php,参考命令 php bin/composer install,安装依赖;
  • licenses,文件 LicensesCommand.php,参考命令 php bin/composer licenses,查看所有依赖的证书;
  • outdated,文件 OutdatedCommand.php,参考命令 php bin/composer outdated,查看依赖是否有更新;
  • prohibits,文件 ProhibitsCommand.php,参考命令 php bin/composer prohibits,显示阻止安装的依赖;
  • reinstall,文件 ReinstallCommand.php,参考命令 php bin/composer reinstall package,重新安装指定依赖;
  • remove,文件 RemoveCommand.php,参考命令 php bin/composer remove package,移除指定依赖;
  • require,文件 RequireCommand.php,参考命令 php bin/composer require package,添加并安装依赖;
  • run-script,文件 RunScriptCommand.php,参考命令 php bin/composer run-script script-name,运行在 composer.json 中定义的脚本命令;
  • search,文件 SearchCommand.php,参考命令 php bin/composer search package,搜索依赖;
  • self-update,文件 SelfUpdateCommand.php,参考命令 php bin/composer self-update,更新 composer;
  • show,文件 ShowCommand.php,参考命令 php bin/composer show package,获取依赖相关信息;
  • status,文件 StatusCommand.php,参考命令 php bin/composer status,查看已更新的本地依赖;
  • suggests,文件 SuggestsCommand.php,参考命令 php bin/composer suggests,展示依赖建议;
  • update,文件 UpdateCommand.php,参考命令 php bin/composer update,更新依赖;
  • validate,文件 ValidateCommand.php,参考命令 php bin/composer validate,验证 composer.json 和 composer.lock 文件。

这里,主要考虑到命令较多,只是罗列命令和它的作用。至于,分析的话,后面,再抽个时间,补充几个常用命令的逻辑。

自动加载

在需要场景中,引入 vendor/autoload.php 文件即可,使用 require_once 引入 autoload_real.php 文件,调用 getLoader() 静态方法获取加载器,实际是在这里注册好的,采用 spl_autoload_register 函数实现五种加载方式。
临时注册一个文件自动加载,主要是为了引入 ClassLoader.php 文件,实例化后注销文件自动加载,说明对此类应该属于核心,大部分操作都在此类中。

静态加载

autoload_static.php,主要加载静态类(其中方法全为静态)(这里描述有问题,不过好像是这样的) ,需要同时满足三个条件:

  1. php 版本大于 5.6;
  2. 非 HHVM(由 Facebook 打造的 PHP 虚拟机)环境;
  3. 检测函数 zend_loader_file_encoded 是否存在,如果存在就是要可用的。也就是说,要不不存在,要不存在且可用,这两种为真;

如果满足,通过 require 引入 autoload_static.php,然后 call_user_func(\Composer\Autoload\ComposerStaticInitComposerPhar1625709410::getInitializer($loader)),将当前加载器作为参数传入,进行初始化设置。主要匿名闭包处理 Closure::bind

  • 设置 PSR-4 类前缀长度 $loader->prefixLengthsPsr4 = ComposerStaticInitComposerPhar1625709410::$prefixLengthsPsr4
  • 设置 PSR-4 类目录 $loader->prefixDirsPsr4 = ComposerStaticInitComposerPhar1625709410::$prefixDirsPsr4
  • 设置 PSR-0 类目录 $loader->prefixesPsr0 = ComposerStaticInitComposerPhar1625709410::$prefixesPsr0
  • 设置类映射 $loader->classMap = ComposerStaticInitComposerPhar1625709410::$classMap

看来,稍微不讲究,直接生成的静态属性,这里只管读取配置。不过这样挺好,因为生成少,读取多,直接固定,减少了寻找匹配的损耗。

命名空间加载

通过 require 引入 autoload_namespaces.php,使用 $load->set($namespace, $path),将类前缀和 PSR-0 关联起来,类似下面这种:

$vendorDir = dirname(dirname(__FILE__));
$baseDir = dirname($vendorDir);

return array(
    'React\\Promise' => array($vendorDir . '/react/promise/src'),
);

PSR-4 标准加载

通过 require 引入 autoload_psr4.php,使用 $loader->setPsr4($namespace, $path),进行相应注册绑定。

类名映射加载

通过 require 引入 autoload_classmap.php,使用 $loader->addClassMap($classMap),进行相应注册绑定。

文件加载

如果是静态加载,直接读取静态属性获取数组,如果不是,那就通过 require 引入 autoload_files.php,并设置标记,如下:

function composerRequireComposerPhar1625709410($fileIdentifier, $file)
{
    if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) {
        require $file;
        $GLOBALS['__composer_autoload_files'][$fileIdentifier] = true;
    }
}

抛开文件加载,在静态加载和其他三加载,其实大体相似,不过,静态加载,拥有更多的信息,比如长度和首字母等,然后一次性注册。不过需要注意的是,在文件加载,会将当前实例注册为自动加载器并且预载入 spl_autoload_register(array($this, 'loadClass'), true, $prepend),这里 $this 是 ClassLoader.php 的实例。既然注册已经完成,那来看下具体加载方式吧。

具体加载 loadClass

如果找到了相应文件,直接 include 的,不过还是简单包装了一下。主要分析查找过程 findFile,由于 include 实现,所以这个方法需要返回类路径。

  • 类映射,这个很方便,直接从属性 classMap 中检测当前类是否存在对应,如果有,直接返回即可,否则继续往下走。检测是否只允许这种方式的标记 classMapAuthoritative 或是否被动过滤的类(查找一次失败后,会被记录在缺少类(missingClasses)中),如果为真,返回 false。
  • 检测 apcu 缓存,$file = apcu_fetch($this->apcuPrefix.$class, $hit),这块也没怎么用过,操作码缓存,如果 $hit 为真,就返回 f i l e , 如 果 后 续 检 测 到 了 相 关 类 , 还 要 使 用 ‘ a p c u a d d ( file,如果后续检测到了相关类,还要使用 `apcu_add( file使apcua?dd(this->apcuPrefix.$class, $file)` 进行补充;
  • 根据后缀检索文件,主要是 php 和 hh(HHVM环境),看来, PSR-0 和 PSR-4 以及文件加载都在这步,这里直接贴源码解释;
private function findFileWithExtension($class, $ext)
{
        // PSR-4 查找 命名空间和路径一致,类名和文件名一致。将分隔符转换,补充后缀
        $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext;
        $first = $class[0]; // 获取首字符,在 prefixLengthsPsr4 检测当前首字母相关命令空间长度,是否定义
        if (isset($this->prefixLengthsPsr4[$first])) { // 如果首字符存在定义
            $subPath = $class; // 路径和命名空间一致 strrpos 最后一次出现的位置,也就是倒着切
            while (false !== $lastPos = strrpos($subPath, '\\')) { // Psr/
                $subPath = substr($subPath, 0, $lastPos); // 切到最后一个 \\ 的位置
                $search = $subPath . '\\'; // 合成 PSR-4 目录前缀
                if (isset($this->prefixDirsPsr4[$search])) { // 在 PSR-4 目录数组中是否定义
                    $pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1); // 获取文件名称
                    foreach ($this->prefixDirsPsr4[$search] as $dir) { // 循环查找
                        if (file_exists($file = $dir . $pathEnd)) return $file;
                    }
                }
            }
        }

        // PSR-4 默认目录中,查找,拼接物理路径,然后检测存在性
        foreach ($this->fallbackDirsPsr4 as $dir) if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) return $file;

        // PSR-0 查找
        if (false !== $pos = strrpos($class, '\\')) {
            // 类命即文件名
            $logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1).strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR);
        } else {
            // PEAR(由 PHP 开发的 PHP 扩展) 风格命名 PSR_Log
            $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 默认目录,拼接引入
        foreach ($this->fallbackDirsPsr0 as $dir) if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) return $file;

        // PSR-0 直接文件引入,
        if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) return $file;
        return false;
}
  • 最后如果找到了,就返回,否则 false;

写在最后

差点就放弃了,不过中间有关命令的具体解析,还是略过,不太想引入大片大片源码。从自动加载器看,主要是对 include 进行了包装和优化,将依赖集中管理。对于项目来说,确实挺方便,就在 composer.json 文件中管理具体需要使用的依赖,采用 composer 命令进行维护。省略了细节和底层。而这种细节,确实一般也不是开发所关心的,他们大多着重业务逻辑的实现。至于,采用什么依赖,大多方便行事。

比如框架这种产物,是需要明确固定依赖,毕竟维护框架的生命周期和固化功能的有效性。不是必需的,一般还是看具体场景抉择。按需引入,符合 CRP 原则。这也是在保证固有功能的前提下,尽量轻量化。

  PHP知识库 最新文章
Laravel 下实现 Google 2fa 验证
UUCTF WP
DASCTF10月 web
XAMPP任意命令执行提升权限漏洞(CVE-2020-
[GYCTF2020]Easyphp
iwebsec靶场 代码执行关卡通关笔记
多个线程同步执行,多个线程依次执行,多个
php 没事记录下常用方法 (TP5.1)
php之jwt
2021-09-18
上一篇文章      下一篇文章      查看所有文章
加:2021-07-13 17:13:58  更:2021-07-13 17:14:06 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年4日历 -2024/4/30 19:52:36-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码