直接改 vendor 代码不可行,因 Composer 每次 install/update 都会覆盖,且无法提交、测试和协作;推荐用 path 仓库 + symlink 热开发、写 Composer 插件注入逻辑或 fork + PR 长期维护。

直接改 vendor 里的代码为什么不行
改 vendor/acme/utils/src/Helper.php 看似最快,但每次 composer install 或 composer update 都会覆盖——因为 Composer 默认把包当“只读产物”处理。更麻烦的是,你本地改的代码无法提交、无法测试、无法被 CI 检查,协作时别人根本不知道你动了哪一行。
用 path 仓库 + symlink 实现热开发
这是最轻量、最贴近真实开发流的方式,适用于你有权限修改原包源码(比如 fork 后本地调试),且希望主项目实时响应改动。
- 确保你的扩展包目录已初始化 Git:
git init && git add . && git commit -m "init",否则 Composer 拒绝加载 - 在主项目的
composer.json中添加本地仓库声明:"repositories": [ { "type": "path", "url": "../my-forked-utils", "options": { "symlink": true } } ] - 运行
composer require acme/utils:dev-main(假设原包分支是main);注意不能用*,否则 Composer 可能忽略 path 配置 - 验证是否生效:
ls -la vendor/acme/utils应显示指向../my-forked-utils的符号链接;Windows 下用dir vendorcmeutils看是否为“快捷方式”类型 - 改完本地代码后,不用重装,直接运行主项目即可看到效果——但注意:Composer 不会自动重建已有 symlink,只补缺;若链接失效或想强制刷新,得运行
composer update acme/utils
写 Composer 插件注入逻辑,不碰原包代码
当你无权修改原包、或功能属于“横切关注点”(如日志埋点、参数校验、自动发布前检查),插件是更干净的选择。它不侵入原包,靠事件监听在生命周期中插入行为。
- 插件必须满足三个硬性条件才能被加载:
"type": "composer-plugin"、"extra": {"class": "My\Plugin"}、"autoload"配置正确(如"psr-4": {"My\Plugin\": "src/"}) - 入口类实现
PluginInterface,并在activate()中注册事件监听,例如:public function activate(Composer $composer, IOInterface $io) { $dispatcher = $composer->getEventDispatcher(); $dispatcher->addSubscriber(new MyEventSubscriber()); } - 监听常见事件:
post-install-cmd、post-update-cmd、pre-autoload-dump;不要在activate()里直接调用$composer->getPackage(),此时拿到的可能是锁文件解析后的CompletePackage,而非原始composer.json内容 - 插件本身需作为独立包引入主项目,通过
repositories+require加载;若放在项目内,需手动加 autoload 映射并确保命名空间不冲突
fork + 提 PR 是长期维护的唯一正途
临时改、本地跑通不等于可持续。真正要让新功能落地,最终得回到上游:fork 原仓库 → 在 feature 分支开发 → 补齐测试和文档 → 提交 PR。
- PR 被合入前,主项目仍可用 path 方式引用你的 fork 分支,例如:
"acme/utils": "dev-feature/my-new-method as 1.2.3",其中as别名保证版本约束兼容 - 别跳过测试:Composer 插件的事件触发时机很关键,
post-autoload-dump发生在 autoloader 生成后,pre-install-cmd在依赖下载前——选错时机会导致逻辑不执行或报错 - 所有路径、类名、命名空间必须严格匹配 autoload 配置,一个斜杠或大小写错误都会导致类找不到,且 Composer 不报明确错误,只静默忽略插件
最常被忽略的一点:插件的 activate() 方法只在首次启用时调用一次,后续 composer install 不会再进这个方法——所以动态逻辑必须靠事件订阅,而不是堆在 activate 里。











