ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Composer 为何不能递归加载依赖的 repositories?——设计缘由、三种求解方案剖析与正确的私有包实践

Composer 为何不能递归加载依赖的 repositories?——设计缘由、三种求解方案剖析与正确的私有包实践 Composer 为何不能递归加载依赖的 repositories——设计缘由、三种求解方案剖析与正确的私有包实践【免费下载链接】composerDependency Manager for PHP项目地址: https://gitcode.com/gh_mirrors/co/composer导读在使用自定义仓库custom repositories时很多开发者会遇到一个困惑Composer 只读取根项目composer.json中声明的仓库而不会去加载依赖包自身声明的仓库因此同样的仓库配置必须在所有composer.json中重复定义。本文基于 Composer 官方 FAQ 文档深入解释这一设计的原因剖析依赖求解器可能的三种工作方式及其取舍并结合仓库源码RepositoryManager、RepositoryFactory、RepositorySet佐证其实现原理最后给出管理私有包的推荐实践Satis / Private Packagist帮助你摆脱每个项目都要抄一遍仓库配置的困境。一、问题背景为什么要在每个 composer.json 里重复声明仓库在 仓库Repositories指南 的概念一节中官方文档明确写下了这样一句关键结论Repositories are only available to the root package and the repositories defined in your dependencies will not be loaded.仓库只对根包生效依赖包中定义的仓库不会被加载。这正是本文要解释的现象当你使用自定义仓库时如果项目 A 依赖项目 B而 B 的composer.json中声明了自己的 VCS 仓库或包仓库Composer 在解析 A 的依赖时不会去读取 B 的仓库配置。因此B 所依赖的那些包必须在 A 的composer.json里也声明对应仓库才能被找到。这一现象在官方 FAQ 中被明确承认Composer does not load the repositories of your requirements, so you have to redefine those repositories in all your composer.json files.Composer 不加载你依赖项的仓库所以你得在所有 composer.json 文件中重新定义这些仓库。在讨论为什么这样设计之前需要先理解一个前提自定义 VCS 与 package 仓库的主要用途是临时性试验。例如临时尝试某个分支上的改动在 pull request 被合并之前临时使用某个项目的 fork。它们不应该被用来长期追踪私有包。如果确实有大量私有包需要统一管理正确的做法是使用集中式托管方案详见下文第四节而不是在每个项目的composer.json里内联一堆 VCS 仓库——后者不仅导致重复配置还会带来与内联 VCS 仓库相关的性能开销每个 VCS 仓库的初始化都可能需要数秒。二、依赖求解器处理自定义仓库的三种可能方案官方 FAQ 指出依赖求解器理论上存在三种与自定义仓库协作的方式。理解这三者的差异是理解为什么不递归加载的关键。方案一只加载根包的仓库现状获取根包的仓库从这些定义的仓库中取得所有包然后解析依赖需求。流程如下读取根包composer.json中的repositories配置初始化这些仓库取得其中的所有包基于这些包解析全部依赖需求。这是Composer 当前的实际行为并且工作得很好works well唯一的限制就是——不递归加载仓库。方案二从包的内部递归初始化所有仓库被否决获取根包的仓库在从这些仓库初始化包的同时递归地初始化这些包以及它们的包的包……里定义的所有仓库然后再解析依赖需求。即只要某个被加载的包自身声明了仓库就把它也拉进来继续初始化层层递归直到穷尽。这个方案理论上可行但存在两个致命问题初始化速度被大幅拖慢VCS 仓库每个都可能需要几秒钟才能完成初始化拉取元数据、扫描分支标签等递归展开后仓库数量呈指数级增长代价不可接受可能得到完全损坏的状态同一个包的多个版本可能在一个包仓库package repository里定义了同名的包但各自的dist/source却不同。递归初始化会把这种互相冲突的定义全部灌进池子导致最终状态完全不可控。原文的原话是it could end up in a completely broken state since many versions of a package could define the same packages inside a package repository, but with different dist/source. There are many ways this could go wrong.由于一个包的许多版本可能在一个包仓库里定义相同的包却带有不同的 dist/source它可能以完全损坏的状态告终。出错的方式有很多。方案三按依赖层级逐层加载仓库看似高效同样被否决获取根包的仓库再获取第一层依赖的仓库然后是它们的依赖的仓库依此类推最后解析依赖需求。这个方案听起来比方案二更高效——它避免了把无关的深层包仓库也全部初始化。但官方 FAQ 指出它遭受与方案二相同的问题因为加载依赖的仓库并没有听起来那么容易要解析一个需求requirement你必须为这个需求的所有潜在匹配版本加载其对应的仓库而这些潜在匹配的仓库中再次可能出现互相冲突的包定义同一个包名被不同仓库以不同 dist/source 声明。也就是说无论按包递归还是按依赖层级递归只要把仓库发现过程从根包扩展到依赖包就必然引入冲突定义与性能膨胀的问题。三方案对比小结方案工作方式优点致命缺陷是否采用方案一只加载根包仓库行为确定、性能可控、无冲突仓库不能递归加载需重复声明✅ 当前实现方案二按包递归初始化所有内嵌仓库仓库自动发现VCS 初始化慢秒级/个同名包 dist/source 冲突导致状态损坏❌方案三按依赖层级逐层加载理论上更高效需加载所有潜在匹配的仓库仍存在冲突定义❌正是因为在递归性与确定性 / 性能之间无法两全Composer 最终选择了方案一仓库只从根包读取牺牲递归发现能力换取可预测、快速的依赖解析。三、源码佐证仓库管理与初始化流程以上设计在实际代码中是如何体现的我们可以从仓库源码中找到直接证据。1. 仓库集合由 RepositoryManager 统一管理RepositoryManager 是管理所有仓库实例的核心类。它内部维护一个$repositories数组private $repositories [];并通过addRepository()/prependRepository()维护顺序、getRepositories()获取全部仓库findPackage(string $name, $constraint)按声明顺序遍历仓库找到第一个匹配的包即返回对应 repository-priorities 文档 描述的自上而下、命中即停行为createRepository(string $type, array $config, ?string $name null)根据repositoryClasses中注册的类型字符串创建仓库实例并支持用only/exclude/canonical配置包装出FilterRepository。2. 仓库类型注册与默认仓库的来源RepositoryFactory 负责装配仓库。它的manager()方法为RepositoryManager注册了全部仓库类型到实现类的映射composer、vcs、package、pear、git、github、gitlab、svn、fossil、perforce、hg、artifact、path 等而defaultRepos()的核心一行是return self::createRepos($rm, $config-getRepositories());也就是说Composer 初始化的仓库集合完全来自根包配置$config-getRepositories()没有任何读取依赖包内 repositories 字段的逻辑。这正是仓库只属于根包设计在源码层面的直接体现仓库发现的入口是且仅是根项目的配置。3. 依赖解析的输入是根包仓库构建的包池RepositorySet 是依赖求解阶段的仓库集合抽象它同样只接收由根包配置创建的RepositoryInterface[]private $repositories [];配合PoolBuilder把这些仓库中的包组装成求解器使用的 Pool。由于池子的来源被限定在根包仓库内求解器永远不会意外看到某个依赖包私有的仓库——冲突定义自然无从产生这也是方案一能保持确定性的底层原因。从源码结构可以推断递归加载仓库的能力方案二、三在 Composer 的架构中没有任何实现入口仓库发现与依赖解析被刻意解耦前者只看根包配置后者只消费前者的产物。四、正确的替代实践用集中式仓库管理私有包FAQ 明确建议不要用内联 VCS 仓库来长期追踪私有包。对于私有包托管官方给出的两个方向分别是Private Packagist商业化的包托管产品可在单一位置配置所有私有包、提供细粒度访问权限与包 ZIP 镜像镜像使得安装更快且不依赖 GitHub 等第三方系统的可用性其部分收入用于支持 Composer 与 Packagist.org 的开发与托管。Satis开源的静态 Composer 仓库生成器本质是超轻量、基于静态文件的迷你版 Packagist可用来托管公司或个人的私有包元数据。关于两者的完整配置教程可阅读仓库内的 处理私有包Handling private packages 一文其中给出了 Satis 的完整搭建流程定义satis.json聚合 VCS 仓库 → 运行php bin/satis build 配置文件 构建目录→ 通过 webhook / cron 定期重建 → 各项目只需声明一个composer类型的仓库 URL 即可引用全部私有包。这样就从根源上解决了每个 composer.json 都要复制一份仓库列表的痛点{ repositories: [ { type: composer, url: http://packages.example.org/ } ], require: { company/package: 1.2.0, company/package2: 1.5.2, company/package3: dev-master } }五、延伸仓库的查找顺序与 canonical 语义理解了仓库不递归之后还需要知道仓库之间的查找规则这与重复声明的体验密切相关。官方在 repository-priorities 文档 中说明Composer 解析依赖时自上而下依次查找仓库一旦某个仓库命中包即停止对应RepositoryManager::findPackage()的遍历逻辑Composer 2.x 中所有仓库默认是canonical权威的只要声明顺序靠前的仓库里有该包就不会再从后面的仓库包括 Packagist加载——这对安全很重要可以防止私有包被同名高版本包顶替可通过canonical: false关闭该行为或用only/exclude支持*通配过滤仓库内可加载的包。这些机制共同构成了仓库只属于根包这一设计下的完整使用规范在根项目里精确、有序地声明好仓库集合其余交给求解器在确定性的池子内工作。结语Composer 不递归加载依赖的 repositories并非功能缺失而是刻意设计的结果递归方案在性能VCS 仓库初始化成本与正确性同名包 dist/source 冲突上都无法满足依赖求解对确定性与速度的要求。作为使用者正确姿势是接受仓库只在根包声明的规则在需要时于每个根项目中显式重复声明必要仓库长期私有包请迁移到 Satis / Private Packagist 等集中式方案避免内联 VCS 仓库的维护与性能负担利用 canonical / only / exclude 等机制精确控制仓库查找行为构建安全、快速、可预测的依赖解析环境。如果你还想深入了解仓库类型composer / vcs / package / path / artifact 等的配置细节与packages.json协议字段可继续阅读 05-repositories.md 全文。【免费下载链接】composerDependency Manager for PHP项目地址: https://gitcode.com/gh_mirrors/co/composer创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表