> ## 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 패키지 개발](/ko/advanced/package-development), [Orchestra Testbench로 Laravel 패키지를 테스트하기](/ko/advanced/package-testing), [패키지의 버전 호환성 관리](/ko/advanced/package-versioning)의 보조 가이드입니다. 구현·테스트·버전 전략에 정적 해석을 추가하면, 장기 유지보수하기 쉬운 패키지가 됩니다.
</Info>

## 정적 해석이란

정적 해석은, 코드를 실행하지 않고 타입의 부정합이나 잠재적인 버그를 검출하는 수법입니다. Laravel 패키지에서는, 서비스 컨테이너, Facade, Eloquent 등 동적인 구조가 많기 때문에, 동작 확인만으로는 놓치기 쉬운 문제를 이른 단계에서 잡을 수 있습니다.

정적 해석을 도입하면, 특히 다음의 효과가 있습니다.

* **타입 안전성의 향상** — 잘못된 인수나 반환값을 리뷰 전에 검출할 수 있음
* **버그의 조기 발견** — 존재하지 않는 메서드 호출이나 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의 셋업

Laravel 패키지에서는 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`에서 Larastan의 설정을 include합니다.

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

## phpstan.neon의 설정

`phpstan.neon`은 해석 레벨, 대상 경로, 제외 경로, 예외 룰을 정리하는 중심 설정입니다. Laravel 패키지라면 처음부터 100점을 목표로 하기보다, 지속적으로 올릴 수 있는 설정으로 해 두는 편이 현실적입니다.

이하는 `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/*
```

### 레벨 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` — `find()`가 구체적인 모델 타입을 반환하는 것을 전달
* `@var Collection<int, TModel>` — 컬렉션의 key / value 타입을 명시
* `@template TModel of Model` — 재이용 가능한 generic 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`와 relation의 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 버전을 서포트하는 패키지에서는, [패키지의 버전 호환성 관리](/ko/advanced/package-versioning)의 test matrix와 같은 사고방식으로 돌리면, 호환성과 타입 안전성을 동시에 감시할 수 있습니다.

이하는 `.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
```

이 예에서는 테스트 matrix와 같은 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나 relation의 반환값 타입을 명시한다
3. `mixed`를 구체적인 타입으로 치환한다
4. 그래도 설명할 수 있는 오검지만 ignore한다

## 관련 페이지

<Columns cols={3}>
  <Card title="Laravel 패키지 개발" icon="box" href="/ko/advanced/package-development">
    서비스 프로바이더나 공개 리소스를 포함한 패키지 구현의 기초를 확인합니다.
  </Card>

  <Card title="Orchestra Testbench로 Laravel 패키지를 테스트하기" icon="flask-conical" href="/ko/advanced/package-testing">
    정적 해석과 함께 운용하고 싶은 패키지용 테스트 기반을 해설합니다.
  </Card>

  <Card title="패키지의 버전 호환성 관리" icon="git-branch" href="/ko/advanced/package-versioning">
    Laravel / PHP의 대응표와 GitHub Actions의 matrix 전략을 확인합니다.
  </Card>
</Columns>


## Related topics

- [Collection Deep Dive](/ko/advanced/collection-deep-dive.md)
- [Laravel Package Skeleton — 공식 패키지용 스타터 템플릿](/ko/blog/package-skeleton-introduction.md)
- [Laravel 신규 코드 해석 에코시스템 — surveyor / ranger / roster](/ko/blog/laravel-ecosystem-analysis.md)
- [패키지의 버전 호환성 관리](/ko/advanced/package-versioning.md)
- [패키지의 CHANGELOG와 릴리스 관리](/ko/advanced/package-changelog.md)
