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

# 패키지 자동 감지의 내부 구조

> PackageManifest 클래스의 내부 구현을 살펴보고, composer.json의 extra.laravel이 어떻게 캐시되어 bootstrap/cache/packages.php로 로드되는지를 설명합니다.

## 이 페이지에 대해

[패키지 개발 기초](/ko/advanced/package-development)에서는 `composer.json`의 `extra.laravel` 섹션을 작성하면 서비스 프로바이더와 파사드가 자동 등록된다는 점을 소개했습니다. 이 페이지에서는 그 이면, 즉 `Illuminate\Foundation\PackageManifest` 클래스가 어떻게 동작하는지를 소스코드 수준에서 설명합니다.

<Info>
  이 페이지는 [패키지 개발 기초](/ko/advanced/package-development)의 자매 페이지입니다. 자동 감지의 기본적인 사용법을 먼저 읽어 보시길 권장합니다.
</Info>

## 자동 감지의 전체 흐름

```mermaid theme={null}
flowchart TD
    A["composer install / update"] --> B["Composer가 post-autoload-dump<br>이벤트를 발생"]
    B --> C["Illuminate\\Foundation\\ComposerScripts::postAutoloadDump()"]
    C --> D["bootstrap/cache/packages.php 등의<br>캐시 파일을 삭제"]
    D --> E["다음 시작 시 PackageManifest::build()가 실행됨"]
    E --> F["vendor/composer/installed.json 읽기"]
    F --> G["각 패키지의 extra.laravel 수집"]
    G --> H["dont-discover에 의한 제외 처리"]
    H --> I["bootstrap/cache/packages.php에 기록"]
    I --> J["Application이 providers()/aliases()를<br>읽어 서비스 프로바이더 등록"]
```

## `PackageManifest` 클래스

자동 감지의 중심은 `Illuminate\Foundation\PackageManifest`입니다. 다음은 프레임워크 13.x 기준의 구현(요약)입니다.

```php theme={null}
class PackageManifest
{
    public function providers()
    {
        return $this->config('providers');
    }

    public function aliases()
    {
        return $this->config('aliases');
    }

    public function config($key)
    {
        return (new Collection($this->getManifest()))
            ->flatMap(fn ($configuration) => (array) ($configuration[$key] ?? []))
            ->filter()
            ->all();
    }

    protected function getManifest()
    {
        if (! is_null($this->manifest)) {
            return $this->manifest;
        }

        if (! is_file($this->manifestPath)) {
            $this->build();
        }

        return $this->manifest = is_file($this->manifestPath)
            ? $this->files->getRequire($this->manifestPath)
            : [];
    }
}
```

핵심 포인트는 다음 세 가지입니다.

* **매니페스트는 한 번 로드되면 메모리에 캐시된다** (`$this->manifest` 프로퍼티). 한 요청 내에서 `providers()`를 여러 번 호출해도 파일 I/O는 한 번만 발생합니다.
* **매니페스트 파일이 존재하지 않는 경우에만 `build()`가 실행된다.** 일반 운영 시에는 매번 빌드되지 않습니다.
* 실체는 소박한 PHP 배열을 `return`하는 파일(`bootstrap/cache/packages.php`)로, `require`만으로 로드할 수 있는 가장 빠른 형식입니다.

## 매니페스트 빌드 처리

`build()` 메서드가 실제로 `composer.json` 정보를 집약하는 부분입니다.

```php theme={null}
public function build()
{
    $packages = [];

    if ($this->files->exists($path = $this->vendorPath.'/composer/installed.json')) {
        $installed = json_decode($this->files->get($path), true);
        $packages = $installed['packages'] ?? $installed;
    }

    $ignoreAll = in_array('*', $ignore = $this->packagesToIgnore());

    $this->write((new Collection($packages))->mapWithKeys(function ($package) {
        return [$this->format($package['name']) => $package['extra']['laravel'] ?? []];
    })->each(function ($configuration) use (&$ignore) {
        $ignore = array_merge($ignore, $configuration['dont-discover'] ?? []);
    })->reject(function ($configuration, $package) use ($ignore, $ignoreAll) {
        return $ignoreAll || in_array($package, $ignore);
    })->filter()->all());
}
```

중요한 점은 `composer.json`을 직접 파싱하는 것이 아니라 **`vendor/composer/installed.json`** 을 읽는다는 것입니다. 이는 Composer가 `composer install` / `composer update` 실행 시 생성하는, 설치된 모든 패키지의 메타데이터 파일입니다. 각 패키지의 `composer.json`에 작성된 `extra` 섹션이 여기에 집약되므로 Laravel 쪽은 Composer 관리하의 정보만 신뢰하여 읽습니다.

<Info>
  `installed.json`은 일반적으로 `.gitignore`되므로, 초기 설정 시(`vendor/`가 없는 상태)에는 자동 감지가 작동하지 않습니다. `composer install`이 완료된 후에야 캐시가 구축됩니다.
</Info>

### `dont-discover`의 두 가지 작성 방식

`dont-discover`는 패키지 측과 애플리케이션 측 어느 `composer.json`에도 작성할 수 있지만, 의미가 다릅니다.

```json title="패키지 측 composer.json" theme={null}
"extra": {
    "laravel": {
        "providers": ["Acme\\Courier\\CourierServiceProvider"],
        "dont-discover": []
    }
}
```

```json title="애플리케이션 측 composer.json" theme={null}
"extra": {
    "laravel": {
        "dont-discover": [
            "acme/courier"
        ]
    }
}
```

`build()`의 구현을 보면 `$ignore` 배열은 각 패키지의 `configuration['dont-discover']`를 `array_merge`하여 축적하고 있습니다. 즉 패키지 자신이 "자신의 의존 패키지의 자동 감지를 비활성화"하는 것도 기술적으로는 가능합니다(예: 내부에서 사용하는 서브 패키지의 프로바이더를 중복 등록시키고 싶지 않은 경우). 다만 실무에서 자주 사용되는 것은 애플리케이션 측에서의 비활성화입니다.

### `dont-discover`에 `*` 지정하기

`packagesToIgnore()`가 반환하는 배열에 `*`가 포함되면 `$ignoreAll = true`가 되어 **모든 패키지의 자동 감지가 통째로 비활성화됩니다**. CI나 테스트 환경에서 자동 감지의 오버헤드를 피하고 싶은 경우나 `bootstrap/providers.php`에서 완전히 수동 관리하고 싶은 경우에 사용합니다.

```json theme={null}
"extra": {
    "laravel": {
        "dont-discover": ["*"]
    }
}
```

## 캐시 파일의 실체

`getCachedPackagesPath()`는 환경 변수 `APP_PACKAGES_CACHE`가 있으면 그것을, 없으면 `bootstrap/cache/packages.php`를 반환합니다.

```php theme={null}
public function getCachedPackagesPath()
{
    return $this->normalizeCachePath('APP_PACKAGES_CACHE', 'cache/packages.php');
}
```

이 파일을 직접 열어보면, 단순한 연관 배열을 `return`하고 있음을 알 수 있습니다.

```php theme={null}
<?php return array (
  'acme/courier' => 
  array (
    'providers' => 
    array (
      0 => 'Acme\\Courier\\CourierServiceProvider',
    ),
  ),
);
```

패키지명이 키가 되므로, `php artisan package:discover`의 출력에서 "어떤 패키지가 감지되었는지" 확인할 수 있습니다.

## 캐시가 재구축되는 시점

`Illuminate\Foundation\ComposerScripts`는 3개의 Composer 이벤트에 훅을 걸고 있으며, 모두 같은 `clearCompiled()`를 호출합니다.

```php theme={null}
public static function postInstall(Event $event)
{
    require_once $event->getComposer()->getConfig()->get('vendor-dir').'/autoload.php';
    static::clearCompiled();
}

protected static function clearCompiled()
{
    $laravel = new Application(getcwd());

    if (is_file($configPath = $laravel->getCachedConfigPath())) {
        @unlink($configPath);
    }

    if (is_file($servicesPath = $laravel->getCachedServicesPath())) {
        @unlink($servicesPath);
    }

    if (is_file($packagesPath = $laravel->getCachedPackagesPath())) {
        @unlink($packagesPath);
    }
}
```

즉 `composer install` / `composer update` / `composer dump-autoload` 중 어느 것을 실행해도 설정 캐시·서비스 캐시·패키지 캐시가 모두 삭제됩니다. 다음에 Laravel이 시작되는 시점에 `PackageManifest::build()`가 실행되어 `installed.json`의 최신 상태에서 재구축됩니다.

이는 `composer.json`의 `scripts`에 등록된, 표준적인 Laravel 프로젝트의 동작입니다.

```json title="Laravel 앱의 composer.json (발췌)" theme={null}
"scripts": {
    "post-autoload-dump": [
        "Illuminate\\Foundation\\ComposerScripts::postAutoloadDump"
    ],
    "post-update-cmd": [
        "@php artisan vendor:publish --tag=laravel-assets --ansi --force"
    ],
    "post-root-package-install": [
        "@php -r \"file_exists('.env') || copy('.env.example', '.env');\""
    ],
    "post-create-project-cmd": [
        "@php artisan key:generate --ansi"
    ]
}
```

## `php artisan package:discover`로 수동 재구축

캐시가 오래된 상태로 남아 있거나, Composer를 거치지 않고 `vendor/`를 직접 변경한 경우에는 `package:discover` 명령으로 수동 재구축할 수 있습니다.

```shell theme={null}
php artisan package:discover
```

이 명령의 실체는 매우 얇은 래퍼입니다.

```php theme={null}
#[AsCommand(name: 'package:discover')]
class PackageDiscoverCommand extends Command
{
    public function handle(PackageManifest $manifest)
    {
        $this->components->info('Discovering packages');

        $manifest->build();

        (new Collection($manifest->manifest))
            ->keys()
            ->each(fn ($description) => $this->components->task($description))
            ->whenNotEmpty(fn () => $this->newLine());
    }
}
```

`$manifest->build()`를 호출하여 결과를 출력할 뿐이며, 명령 고유의 로직은 거의 없습니다. CI 파이프라인에서 `composer install --no-scripts`를 사용하는 등, Composer 이벤트가 발생하지 않는 상황에서는 이 명령을 명시적으로 호출할 필요가 있습니다.

<Warning>
  패키지 테스트에서 [Orchestra Testbench](/ko/advanced/package-testing)를 사용하는 경우, Testbench는 독자적인 `vendor/bin/testbench package:discover` 명령을 제공합니다. 자세한 내용은 [Testbench에서 패키지 테스트](/ko/advanced/package-testing)를 참조하세요. Artisan의 `package:discover`와는 별개로, Testbench 전용 스켈레톤 앱을 위해 매니페스트를 구축합니다.
</Warning>

## 배포 시 주의점

프로덕션 배포에서는 `composer install --no-dev --optimize-autoloader`를 실행하는 것이 일반적이지만, `--no-scripts`를 지정한 경우에는 패키지 캐시가 업데이트되지 않습니다. 배포 스크립트에 명시적인 재구축 단계를 넣어두면 안전합니다.

```shell theme={null}
composer install --no-dev --optimize-autoloader --no-scripts
php artisan package:discover --ansi
php artisan config:cache
php artisan route:cache
```

순서도 중요합니다. `package:discover`는 `config:cache`보다 먼저 실행하세요. 설정 캐시가 먼저 만들어지면 나중에 추가된 패키지 설정(`mergeConfigFrom`으로 등록되는 설정 등)이 반영되지 않을 수 있습니다.

## 정리

* 자동 감지는 `composer.json`의 정적 내용이 아니라, Composer가 생성한 `vendor/composer/installed.json`을 읽어 동작한다.
* 결과는 `bootstrap/cache/packages.php`에 순수한 PHP 배열로 캐시되며, `require`만으로 빠르게 로드된다.
* 캐시는 `composer install/update/dump-autoload`의 각 이벤트에서 자동으로 삭제되고, 다음 시작 시 재구축된다.
* `--no-scripts`로 Composer를 실행하는 환경에서는 `php artisan package:discover`를 명시적으로 호출할 필요가 있다.
* `dont-discover`에 `*`를 지정하면 자동 감지 전체를 비활성화할 수 있다.

## 관련 페이지

* [패키지 개발 기초](/ko/advanced/package-development) — 서비스 프로바이더와 `extra.laravel`의 기본 사용법
* [지연 서비스 프로바이더](/ko/advanced/deferred-provider) — 감지된 프로바이더의 로드 타이밍 최적화
* [Orchestra Testbench에서 패키지 테스트](/ko/advanced/package-testing) — Testbench 고유의 `package:discover` 명령


## Related topics

- [Laravel 패키지 개발](/ko/advanced/package-development.md)
- [패키지의 정적 해석 (PHPStan / Larastan)](/ko/advanced/package-static-analysis.md)
- [패키지의 CHANGELOG와 릴리스 관리](/ko/advanced/package-changelog.md)
- [패키지의 버전 호환성 관리](/ko/advanced/package-versioning.md)
- [Laravel Boost](/ko/boost.md)
