메인 콘텐츠로 건너뛰기
Laravel 패키지는 한 번 릴리스하면 끝이 아닙니다. Laravel 본체, PHP, 의존 패키지의 갱신에 추종하면서 연 단위로 유지보수하려면, 테스트에 더해 정적 해석도 지속적으로 돌릴 필요가 있습니다.
이 페이지는 Laravel 패키지 개발, Orchestra Testbench로 Laravel 패키지를 테스트하기, 패키지의 버전 호환성 관리의 보조 가이드입니다. 구현·테스트·버전 전략에 정적 해석을 추가하면, 장기 유지보수하기 쉬운 패키지가 됩니다.

정적 해석이란

정적 해석은, 코드를 실행하지 않고 타입의 부정합이나 잠재적인 버그를 검출하는 수법입니다. Laravel 패키지에서는, 서비스 컨테이너, Facade, Eloquent 등 동적인 구조가 많기 때문에, 동작 확인만으로는 놓치기 쉬운 문제를 이른 단계에서 잡을 수 있습니다. 정적 해석을 도입하면, 특히 다음의 효과가 있습니다.
  • 타입 안전성의 향상 — 잘못된 인수나 반환값을 리뷰 전에 검출할 수 있음
  • 버그의 조기 발견 — 존재하지 않는 메서드 호출이나 nullable의 누락을 테스트 전에 파악할 수 있음
  • IDE 자동 완성의 개선 — PHPDoc과 Generics를 정비함으로써 자동 완성 정밀도가 올라감
  • 장기 유지보수의 안정화 — Laravel이나 PHP의 업그레이드 시에 부서진 곳을 씻어내기 쉬움

PHPStan의 셋업

우선은 PHPStan 단체로 해석 기반을 준비합니다. PHPStan 2.x는 mixed나 nullable의 취급이 이전보다 엄밀하므로, 패키지의 공개 API를 정비하는 계기도 됩니다.
1

PHPStan을 개발 의존에 추가한다

composer require --dev phpstan/phpstan:^2.0
2

우선은 대상 디렉토리를 직접 해석한다

패키지에서는 srctests를 최초의 대상으로 하는 것이 많습니다.
vendor/bin/phpstan analyse src tests
3

Composer script에 고정한다

CI와 로컬에서 같은 커맨드를 사용할 수 있도록, composer.json에 script를 정의해 두면 운용하기 쉬워집니다.
{
    "scripts": {
        "analyse": "phpstan analyse"
    }
}

Larastan의 셋업

Laravel 패키지에서는 PHPStan만으로는, 컨테이너 해결, Facade, Eloquent 릴레이션 등 Laravel 고유의 구조를 충분히 이해할 수 없습니다. 그래서 PHPStan의 Laravel 확장인 Larastan을 병용합니다.
1

Larastan을 추가한다

현재의 패키지명은 larastan/larastan입니다. 오래된 기사에서는 nunomaduro/larastan이 나올 때가 있습니다.
composer require --dev larastan/larastan:^3.0
2

패키지 개발이라면 Testbench도 넣어 둔다

Larastan은 Laravel 애플리케이션 컨테이너를 기동하여 타입을 해결합니다. Laravel 패키지를 단체 해석하는 경우에는, orchestra/testbench가 필요할 때가 있습니다.
composer require --dev orchestra/testbench
3

extension.neon을 로드한다

phpstan.neon에서 Larastan의 설정을 include합니다.
includes:
    - vendor/larastan/larastan/extension.neon

phpstan.neon의 설정

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

레벨 0~9의 고르는 법

PHPStan은 단계적으로 도입할 수 있습니다. 우선은 현재의 코드베이스를 다 돌릴 수 있는 레벨부터 시작하고, 수정이 진행되면 올려 가는 운용이 안전합니다.
레벨적합한 단계
0~3우선 미지의 클래스·함수·메서드 호출을 줄이고 싶은 단계
4~6공개 API의 인수·반환값·프로퍼티 타입을 정비하고 싶은 단계
7~9nullable, union, mixed를 엄밀하게 다루고 싶은 장기 유지보수 페이즈
PHPStan 2.x에는 level 10도 있지만, Laravel 패키지에서는 우선 57을 안정시킨 뒤 89로 나아가면 현실적입니다. 신규 패키지라면 처음부터 높은 레벨로 시작하는 편이 되돌아감이 적어집니다.

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

우선 우선해야 할 대처

  1. PHPDoc을 더한다
  2. Facade나 relation의 반환값 타입을 명시한다
  3. mixed를 구체적인 타입으로 치환한다
  4. 그래도 설명할 수 있는 오검지만 ignore한다

관련 페이지

Laravel 패키지 개발

서비스 프로바이더나 공개 리소스를 포함한 패키지 구현의 기초를 확인합니다.

Orchestra Testbench로 Laravel 패키지를 테스트하기

정적 해석과 함께 운용하고 싶은 패키지용 테스트 기반을 해설합니다.

패키지의 버전 호환성 관리

Laravel / PHP의 대응표와 GitHub Actions의 matrix 전략을 확인합니다.
마지막 수정일 2026년 7월 13일