Laravel 包并不是发一次版本就结束。为了在 Laravel 本体、PHP、依赖包持续更新的同时长期维护,除测试外还需要持续跑静态分析。
什么是静态分析
静态分析是在不执行代码的前提下,检测类型不匹配与潜在 Bug 的手段。Laravel 包中有服务容器、Facade、Eloquent 等许多动态机制,仅靠运行验证容易漏掉的问题,静态分析可以在早期捕获。
引入静态分析尤其带来以下好处:
- 提升类型安全 — 在评审前就发现错误的参数或返回值
- 提早发现 Bug — 在测试前掌握不存在的方法调用、nullable 遗漏
- 改进 IDE 补全 — 完善 PHPDoc 与 Generics 后补全更准确
- 稳定长期维护 — Laravel 或 PHP 升级时能更容易梳理受影响处
PHPStan 的搭建
先用 PHPStan 独立准备分析基础设施。PHPStan 2.x 对 mixed 与 nullable 的处理比以往更严格,也是重整包公共 API 的契机。
将 PHPStan 加入开发依赖
composer require --dev phpstan/phpstan:^2.0
先直接分析目标目录
包一般先以 src 和 tests 为初始对象。vendor/bin/phpstan analyse src tests
在 Composer script 中固化
为了让 CI 与本地使用同样命令,在 composer.json 中定义 script 更方便运维。{
"scripts": {
"analyse": "phpstan analyse"
}
}
Larastan 的搭建
只用 PHPStan 无法充分理解容器解析、Facade、Eloquent 关联等 Laravel 特有机制。因此还需要一起使用 PHPStan 的 Laravel 扩展 Larastan。
加入 Larastan
当前包名是 larastan/larastan。旧文章中可能出现的 nunomaduro/larastan 已经不用了。composer require --dev larastan/larastan:^3.0
包开发时最好也装 Testbench
Larastan 会启动 Laravel 应用容器来解析类型。单独分析 Laravel 包时可能需要 orchestra/testbench。composer require --dev orchestra/testbench
引入 extension.neon
在 phpstan.neon 中 include Larastan 的配置。includes:
- vendor/larastan/larastan/extension.neon
phpstan.neon 的配置
phpstan.neon 是聚合分析等级、目标路径、排除路径、豁免规则的核心配置。对 Laravel 包来说,比起一开始就追求满分,采用一份能持续提升的配置更为现实。
以下是 phpstan.neon 的例子。
includes:
- vendor/larastan/larastan/extension.neon
parameters:
level: 5
paths:
- src
- tests
excludePaths:
analyse:
- vendor
- workbench
- bootstrap/cache/*
ignoreErrors:
-
message: '#Call to an undefined method Illuminate\\Support\\HigherOrderCollectionProxy::#'
path: tests/Fixtures/*
level 0〜9 的选择
PHPStan 可以逐步引入。先从能跑通当前代码库的等级开始,随修复推进再逐步提高,是最稳妥的方式。
| 级别 | 适合阶段 |
|---|
| 0〜3 | 先减少未知类、函数、方法调用的阶段 |
| 4〜6 | 想整理公共 API 参数、返回值、属性类型的阶段 |
| 7〜9 | 想严格处理 nullable、union、mixed 的长期维护期 |
PHPStan 2.x 也有 level 10,但对 Laravel 包而言,先稳定在 5〜7 再迈向 8〜9 更现实。新包如从更高级别开始,返工会更少。
paths
paths 中显式列出要分析的目录。包里不仅是 src,类型也容易崩坏的 tests 也建议纳入。
excludePaths
只排除生成文件、缓存、验证用 Workbench 等静态分析价值较低的位置。过度排除会遮盖本应发现的错误。
ignoreErrors
ignoreErrors 是最后手段。请用正则精确匹配消息,并搭配 path 限制影响范围。这样将来 Laravel 或 Larastan 改进后更容易还原。
常用类型注解
PHPStan 与 Larastan 的准确度很大程度上取决于 PHPDoc 的写法。Laravel 包中,@param、@return、@var、Generics(@template)的整备尤其有价值。
<?php
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;
/**
* @template TModel of Model
*/
final class ModelRepository
{
/**
* @param class-string<TModel> $modelClass
*/
public function __construct(private string $modelClass)
{
}
/**
* @return TModel|null
*/
public function find(int $id): ?Model
{
return $this->modelClass::query()->find($id);
}
/**
* @return Collection<int, TModel>
*/
public function all(): Collection
{
/** @var Collection<int, TModel> $models */
$models = $this->modelClass::query()->get();
return $models;
}
}
这个例子向 PHPStan 传达了以下信息:
@param class-string<TModel> — 表明字符串不是任意字符串,而是 Model 类名
@return TModel|null — 告诉 PHPStan find() 返回具体的模型类型
@var Collection<int, TModel> — 明确 Collection 的 key/value 类型
@template TModel of Model — 表达可复用的通用 repository
Laravel 特有注意点
Laravel 「便捷的 magic」不会自动传达给静态分析。需要在包侧补充类型信息,以让分析器能够理解。
Facade
对自定义 Facade,在 PHPDoc 中补上用户会调用的方法,可同时受益于 IDE 与静态分析。
<?php
namespace Vendor\Package\Facades;
use Illuminate\Support\Facades\Facade;
/**
* @method static string issueToken(int $userId)
*/
class Tokenizer extends Facade
{
protected static function getFacadeAccessor(): string
{
return 'package.tokenizer';
}
}
魔术方法
依赖 __call() 或 Macroable 的 API 很方便,但类型极易失控。若作为公开 API,最好增加参数与返回值可见的包装方法。
仅仅为了让静态分析通过而不断增加 ignoreErrors,会把 Facade 或 Macro 真正的破损情况掩盖起来。请优先考虑通过明确的方法、类型化的 value object、追加 PHPDoc 来解决。
Eloquent 模型的类型指定
对 Eloquent 关联或动态属性,组合 @property 与关联的 Generics 会很有效。
<?php
namespace Vendor\Package\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
/**
* @property-read Team $team
*/
class Member extends Model
{
/**
* @return BelongsTo<Team, self>
*/
public function team(): BelongsTo
{
return $this->belongsTo(Team::class);
}
}
在 CI 中自动执行
静态分析不仅要在本地,也务必要在 CI 中执行。特别是支持多个 Laravel 版本的包,请像 包的版本兼容性管理 中的测试矩阵一样运行,可以同时监控兼容性与类型安全。
以下是 .github/workflows/static-analysis.yml 的示例。
name: Static analysis
on:
push:
pull_request:
jobs:
static-analysis:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
php: [8.2, 8.3, 8.4]
laravel: ["^12.0", "^13.0"]
include:
- laravel: "^13.0"
testbench: "^10.0"
- laravel: "^12.0"
testbench: "^9.0"
name: PHP ${{ matrix.php }} - Laravel ${{ matrix.laravel }} - PHPStan
steps:
- uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
extensions: dom, curl, libxml, mbstring, zip
coverage: none
- name: Install dependencies
run: |
composer require "laravel/framework:${{ matrix.laravel }}" \
"orchestra/testbench:${{ matrix.testbench }}" \
--no-interaction --no-update
composer update --prefer-dist --no-interaction
- name: Run static analysis
run: vendor/bin/phpstan analyse --error-format=github
该例中使用与测试矩阵相同的 Laravel / Testbench 对应关系。测试与静态分析都监控相同组合时,「运行能过但类型已坏」的状态就不易被漏掉。
常见误报与应对
见到静态分析警告时,先怀疑是否「类型信息不足」。看似误报的情况,很多其实只是 PHPDoc 缺失。
@phpstan-ignore-next-line
作为临时规避方式可以使用,但请限制在你能自行解释原因的行。
// @phpstan-ignore-next-line Laravel 的 macro 在运行时注册
$builder->whereLike('name', $keyword);
ignoreErrors
仅在同一错误出现在多处时才考虑。务必收窄 path,不要用宽松正则一把捂住全局。
parameters:
ignoreErrors:
-
message: '#Call to an undefined method Illuminate\\Database\\Eloquent\\Builder::whereLike\(\)#'
path: tests/Fixtures/*
优先考虑的对策
- 补齐 PHPDoc
- 明确 Facade 或关联返回值类型
- 将
mixed 替换为具体类型
- 只对能解释的误报使用 ignore
相关页面
Laravel 包开发
了解包含 Service Provider 与公开资源的包实现基础。
使用 Orchestra Testbench 测试 Laravel 包
了解与静态分析并用的包测试基础设施。
包的版本兼容性管理
查看 Laravel / PHP 兼容表与 GitHub Actions matrix 策略。