> ## Documentation Index
> Fetch the complete documentation index at: https://kawax.biz/llms.txt
> Use this file to discover all available pages before exploring further.

# 包的静态分析（PHPStan / Larastan）

> 使用 PHPStan 与 Larastan 提升 Laravel 包类型安全与可维护性的安装、配置、类型注解与 CI 自动化。

Laravel 包并不是发一次版本就结束。为了在 Laravel 本体、PHP、依赖包持续更新的同时长期维护，除测试外还需要持续跑静态分析。

<Info>
  本页是 [Laravel 包开发](/zh/advanced/package-development)、[使用 Orchestra Testbench 测试 Laravel 包](/zh/advanced/package-testing)、[包的版本兼容性管理](/zh/advanced/package-versioning) 的辅助指南。将静态分析加入实现、测试、版本策略之中，包会更利于长期维护。
</Info>

## 什么是静态分析

静态分析是在不执行代码的前提下，检测类型不匹配与潜在 Bug 的手段。Laravel 包中有服务容器、Facade、Eloquent 等许多动态机制，仅靠运行验证容易漏掉的问题，静态分析可以在早期捕获。

引入静态分析尤其带来以下好处：

* **提升类型安全** — 在评审前就发现错误的参数或返回值
* **提早发现 Bug** — 在测试前掌握不存在的方法调用、nullable 遗漏
* **改进 IDE 补全** — 完善 PHPDoc 与 Generics 后补全更准确
* **稳定长期维护** — Laravel 或 PHP 升级时能更容易梳理受影响处

## PHPStan 的搭建

先用 PHPStan 独立准备分析基础设施。PHPStan 2.x 对 `mixed` 与 nullable 的处理比以往更严格，也是重整包公共 API 的契机。

<Steps>
  <Step title="将 PHPStan 加入开发依赖">
    ```bash theme={null}
    composer require --dev phpstan/phpstan:^2.0
    ```
  </Step>

  <Step title="先直接分析目标目录">
    包一般先以 `src` 和 `tests` 为初始对象。

    ```bash theme={null}
    vendor/bin/phpstan analyse src tests
    ```
  </Step>

  <Step title="在 Composer script 中固化">
    为了让 CI 与本地使用同样命令，在 `composer.json` 中定义 script 更方便运维。

    ```json theme={null}
    {
        "scripts": {
            "analyse": "phpstan analyse"
        }
    }
    ```
  </Step>
</Steps>

## Larastan 的搭建

只用 PHPStan 无法充分理解容器解析、Facade、Eloquent 关联等 Laravel 特有机制。因此还需要一起使用 PHPStan 的 Laravel 扩展 Larastan。

<Steps>
  <Step title="加入 Larastan">
    当前包名是 `larastan/larastan`。旧文章中可能出现的 `nunomaduro/larastan` 已经不用了。

    ```bash theme={null}
    composer require --dev larastan/larastan:^3.0
    ```
  </Step>

  <Step title="包开发时最好也装 Testbench">
    Larastan 会启动 Laravel 应用容器来解析类型。单独分析 Laravel 包时可能需要 `orchestra/testbench`。

    ```bash theme={null}
    composer require --dev orchestra/testbench
    ```
  </Step>

  <Step title="引入 extension.neon">
    在 `phpstan.neon` 中 include Larastan 的配置。

    ```neon theme={null}
    includes:
        - vendor/larastan/larastan/extension.neon
    ```
  </Step>
</Steps>

## phpstan.neon 的配置

`phpstan.neon` 是聚合分析等级、目标路径、排除路径、豁免规则的核心配置。对 Laravel 包来说，比起一开始就追求满分，采用一份能持续提升的配置更为现实。

以下是 `phpstan.neon` 的例子。

```neon theme={null}
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` 的长期维护期 |

<Tip>
  PHPStan 2.x 也有 level 10，但对 Laravel 包而言，先稳定在 5〜7 再迈向 8〜9 更现实。新包如从更高级别开始，返工会更少。
</Tip>

### `paths`

`paths` 中显式列出要分析的目录。包里不仅是 `src`，类型也容易崩坏的 `tests` 也建议纳入。

### `excludePaths`

只排除生成文件、缓存、验证用 Workbench 等静态分析价值较低的位置。过度排除会遮盖本应发现的错误。

### `ignoreErrors`

`ignoreErrors` 是最后手段。请用正则精确匹配消息，并搭配 `path` 限制影响范围。这样将来 Laravel 或 Larastan 改进后更容易还原。

## 常用类型注解

PHPStan 与 Larastan 的准确度很大程度上取决于 PHPDoc 的写法。Laravel 包中，`@param`、`@return`、`@var`、Generics（`@template`）的整备尤其有价值。

```php theme={null}
<?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 theme={null}
<?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，最好增加参数与返回值可见的包装方法。

<Warning>
  仅仅为了让静态分析通过而不断增加 `ignoreErrors`，会把 Facade 或 Macro 真正的破损情况掩盖起来。请优先考虑通过明确的方法、类型化的 value object、追加 PHPDoc 来解决。
</Warning>

### Eloquent 模型的类型指定

对 Eloquent 关联或动态属性，组合 `@property` 与关联的 Generics 会很有效。

```php theme={null}
<?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 版本的包，请像 [包的版本兼容性管理](/zh/advanced/package-versioning) 中的测试矩阵一样运行，可以同时监控兼容性与类型安全。

以下是 `.github/workflows/static-analysis.yml` 的示例。

```yaml theme={null}
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`

作为临时规避方式可以使用，但请限制在你能自行解释原因的行。

```php theme={null}
// @phpstan-ignore-next-line Laravel 的 macro 在运行时注册
$builder->whereLike('name', $keyword);
```

### `ignoreErrors`

仅在同一错误出现在多处时才考虑。务必收窄 path，不要用宽松正则一把捂住全局。

```neon theme={null}
parameters:
    ignoreErrors:
        -
            message: '#Call to an undefined method Illuminate\\Database\\Eloquent\\Builder::whereLike\(\)#'
            path: tests/Fixtures/*
```

### 优先考虑的对策

1. 补齐 PHPDoc
2. 明确 Facade 或关联返回值类型
3. 将 `mixed` 替换为具体类型
4. 只对能解释的误报使用 ignore

## 相关页面

<Columns cols={3}>
  <Card title="Laravel 包开发" icon="box" href="/zh/advanced/package-development">
    了解包含 Service Provider 与公开资源的包实现基础。
  </Card>

  <Card title="使用 Orchestra Testbench 测试 Laravel 包" icon="flask-conical" href="/zh/advanced/package-testing">
    了解与静态分析并用的包测试基础设施。
  </Card>

  <Card title="包的版本兼容性管理" icon="git-branch" href="/zh/advanced/package-versioning">
    查看 Laravel / PHP 兼容表与 GitHub Actions matrix 策略。
  </Card>
</Columns>


## Related topics

- [Collection Deep Dive](/zh/advanced/collection-deep-dive.md)
- [Laravel 新代码分析生态 — surveyor / ranger / roster](/zh/blog/laravel-ecosystem-analysis.md)
- [Laravel Package Skeleton — 官方包开发用启动模板](/zh/blog/package-skeleton-introduction.md)
- [Laravel Console Starter](/zh/packages/laravel-console-starter/index.md)
- [PHP AST](/zh/advanced/php-ast.md)
