安装 Psalm 不能只用 composer require,因默认不生成配置文件且 --init 易误判源码路径;需先 require --dev 再立即 ./vendor/bin/psalm --init,并确认工作目录为项目根目录,同时设 5、启用相关插件与 stubs,并确保 CI 环境 PHP 版本与 psalm.xml 中 严格一致。

安装 Psalm 时为什么不能只用 composer require?
直接运行 composer require vimeo/psalm --dev 虽然能装上,但默认不生成配置文件,后续执行 psalm --init 又可能因项目结构复杂而误判源码路径。尤其当 src/ 不在根目录、或存在多个入口(如 app/ + lib/)时,psalm.xml 里 <projectfiles></projectfiles> 的 <directory></directory> 会漏掉关键路径。
- 推荐先运行
composer require vimeo/psalm --dev,再立即执行./vendor/bin/psalm --init - 如果提示 “Found 0 files to analyze”,说明 Psalm 没识别到源码目录——此时别急着改
psalm.xml,先检查当前工作目录是否为项目根目录(即composer.json所在位置) -
--init后生成的psalm.xml默认启用totallyTyped="false",对已有项目更友好;若强行设为true,会立刻报几百个MissingReturnType错误,实际无益于发现安全漏洞
如何让 Psalm 真正检测到空数组解引用、未校验的用户输入等高危逻辑?
Psalm 默认级别(level=3)不会触发所有安全相关检查,比如 InvalidArgument 或 PossiblyNullReference 在低 level 下被静默忽略。真正起作用的是插件和自定义 stubs,而非基础扫描。
- 在
psalm.xml中显式启用关键插件:<plugins> <pluginClass class="Psalm\Plugin\Hook\AfterFunctionCallAnalysisInterface" /> </plugins>
实际上不需要手动加这个——重点是启用psalm-plugin-phpunit(如果用了 PHPUnit)和psalm-plugin-symfony(如果用了 Symfony),它们会注入框架特有的类型断言 - 对用户输入类风险(如
$_GET、json_decode返回值),需确保函数返回类型被 Psalm 认知:把vendor/vimeo/psalm/src/Psalm/Plugin/PhpUnit/Plugin.php这类路径加入<stubs></stubs>,或使用@psalm-var array<string mixed></string>注解强制标注 - 空数组解引用(如
$arr[0])需开启InvalidOperand和PossiblyInvalidArrayAccess,这两个问题类型在level=4或更高才激活,建议将<level>5</level>写死,再用<excludefiles></excludefiles>屏蔽历史代码,避免阻塞 CI
CI 流程中 Psalm 报错但本地不报,常见原因有哪些?
最典型的是 Composer autoloader 加载顺序差异导致类型推导失败,其次是 PHP 版本与 Psalm 支持矩阵不匹配(例如 Psalm 5.21+ 要求 PHP 8.1+,但 CI 用的是 8.0)。
- 检查 CI 的
composer install --no-dev是否误删了vimeo/psalm—— 必须保证--dev安装且vendor/bin/psalm可执行 - 确认
psalm.xml中<phpversion>8.1</phpversion>与 CI 的 PHP 版本严格一致,否则array_key_exists等函数的返回类型推导会出错 - 本地开发常依赖 IDE 缓存或已加载的 opcache,而 CI 是干净环境;可在 CI 命令末尾加
--no-cache强制重新分析,避免误报 - 某些错误(如
UnsafeInstantiation)只在 Psalm 解析new $class_name()时触发,而该语法在 PHP 8.2+ 才被完全支持——CI 若用旧版 PHP,这类问题根本不会出现,但不代表代码安全
为什么修复了 Psalm 报出的 RedundantConditionGivenDocblockType 却没降低真实风险?
这类警告本质是“你写了注解,但 Psalm 发现实际类型更窄”,它本身不指向漏洞,而是暴露了文档与实现脱节。真正危险的是背后隐藏的类型绕过,比如注解写 @param string $id,但实际传入了 $_GET['id'] 未经过滤的原始值。
- 不要只删注解来消除该警告;应补上校验逻辑:
if (!is_string($id) || !ctype_alnum($id)) { throw new InvalidArgumentException(); } - Psalm 对
filter_input()的返回类型识别有限,建议配合@psalm-assert string $id注解,比单纯依赖函数签名更可靠 - 当 Psalm 提示
RedundantConditionGivenDocblockType时,往往意味着你已在某处做了类型判断(如is_int()),但注解仍声明为混合类型——此时应同步更新注解,否则其他开发者会误以为变量仍可能为null
Psalm 不会自动发现 SQL 注入或 XSS,它只告诉你“这个变量可能不是你认为的类型”。真正要命的,永远是类型断言失效后那几行没写的校验代码。


















