因为Shell默认只在$PATH中查找命令,而vendor/bin不在其中;需通过shell函数动态定位当前项目vendor/bin下的可执行文件,或用direnv等工具按目录临时注入PATH。

为什么 vendor/bin 里的命令在终端里直接敲就报 “command not found”
因为 Shell 默认只在 $PATH 列表里的目录中查找可执行文件,而 vendor/bin 不在其中。Composer 确实把 phpunit、phpcs、larastan 这类工具软链进来了,但你得告诉 Shell:“这个目录我也要搜”。
常见错误现象:phpunit 报错,但 ./vendor/bin/phpunit 能跑;或者 which phpunit 返回空。
- 不是 Composer 没装对,是 Shell 根本没看到它
- 不同项目
vendor/bin路径不同(比如~/project1/vendor/binvs~/project2/vendor/bin),硬加进$PATH不现实 -
export PATH="vendor/bin:$PATH"这种写法只对当前 shell 有效,且容易污染全局(尤其当你 cd 到别的项目时)
推荐做法:用 shell 函数动态识别当前项目的 vendor/bin
不改全局 $PATH,也不靠 IDE 或脚本预设路径,而是让命令本身“自己找路”。原理是:每次敲命令时,先检查当前或上级目录是否存在 vendor/bin/{cmd},存在就执行,否则 fallback 给系统原命令。
以 zsh/bash 为例,在 ~/.zshrc 或 ~/.bashrc 里加一个函数:
phpunit() {
local cmd="phpunit"
local bin_path
bin_path=$(composer config --file composer.json --global vendor-dir 2>/dev/null | xargs dirname 2>/dev/null)/bin
if [[ -z "$bin_path" ]]; then
bin_path="./vendor/bin"
fi
if [[ -x "$bin_path/$cmd" ]]; then
"$bin_path/$cmd" "$@"
else
command $cmd "$@"
fi
}
你可以为常用工具复制粘贴几份,只需改函数名和 cmd 变量值(如 phpcs、psalm)。更通用的做法是封装成一个 dispatcher 函数,但多数人用 2–3 个就够了。
- 不依赖项目根目录必须有
composer.json,只要vendor/bin存在就生效 - 不会覆盖系统级同名命令(比如系统装了全局
phpunit,当前项目没装,就自动 fallback) - 比修改
$PATH安全:不会误触发其他目录下的同名二进制(比如node_modules/.bin)
如果真想加进 $PATH,必须限定作用域
有些 CI 场景或老项目确实需要全局可用,但绝不能无脑 export PATH=./vendor/bin:$PATH —— 这会导致路径随 cwd 变化,Shell 缓存的 hash 表失效,甚至引发命令冲突。
正确做法是:只在进入项目目录时临时注入,并在离开时清理。可以用 direnv 或 zsh-autoenv 实现:
- 安装
direnv后,在项目根目录建.envrc,内容为:PATH_add vendor/bin
-
direnv allow一次后,每次cd进来自动生效,cd出去自动还原 - 注意:不要在
.envrc里写绝对路径(如/full/path/to/vendor/bin),PATH_add是相对当前目录解析的 - Windows 用户用 Git Bash 的话,
direnv支持有限,建议优先走函数方案
别忽略 Windows 和 PHPStorm 终端的特殊行为
Windows 上 PowerShell 或 CMD 默认不读取 ~/.bashrc,函数方案基本无效;PHPStorm 内置终端默认启动非登录 shell,可能跳过 .zshrc 加载。
- PowerShell 用户可加到
$PROFILE,用function phpunit { & "./vendor/bin/phpunit.ps1" @args }(注意 .ps1 后缀) - PHPStorm:设置 → Tools → Terminal → Shell path,改成
zsh -l或bash -l,强制加载登录配置 - 某些旧版 Composer(php vendor/bin/phpunit
跨平台一致性的最大障碍不是技术,而是团队里有人用 Mac、有人用 Win、还有人用 WSL —— 函数方案在三者上都可适配,只是写法略有差异。真正麻烦的是忘记告诉新同事这事儿得手动配一次。











