> ## 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 加载。

## 关于本页

在[包开发基础](/zh-CN/advanced/package-development)中，我们介绍了只要在 `composer.json` 的 `extra.laravel` 部分进行配置，服务提供者和门面就会被自动注册。本页将从源码层面讲解其背后的实现，即 `Illuminate\Foundation\PackageManifest` 类是如何工作的。

<Info>
  本页是[包开发基础](/zh-CN/advanced/package-development)的姊妹篇。建议先阅读自动发现的基本用法。
</Info>

## 自动发现机制的整体视图

```mermaid theme={null}
flowchart TD
    A["composer install / update"] --> B["Composer 触发<br>post-autoload-dump 事件"]
    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()`**。在常规运行中并不会每次都进行构建。
* 其实体是一个仅通过 `return` 返回朴素 PHP 数组的文件（`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` 数组通过 `array_merge` 累积各包的 `configuration['dont-discover']`。也就是说，从技术上讲包自身也可以"禁用其依赖包的自动发现"（例如：不希望内部使用的子包的提供者被重复注册的情况）。不过实际中最常用的还是应用侧的禁用。

### 在 `dont-discover` 中指定 `*`

若 `packagesToIgnore()` 返回的数组中包含 `*`，则 `$ignoreAll = true`，会**整体禁用所有包的自动发现**。适用于在 CI 或测试环境下希望避免自动发现开销的情况，或希望通过 `bootstrap/providers.php` 完全手动管理的场景。

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

## 缓存文件的实体

若存在环境变量 `APP_PACKAGES_CACHE`，`getCachedPackagesPath()` 会返回它，否则返回 `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` 钩入了三个 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](/zh-CN/advanced/package-testing) 时，Testbench 提供了独有的 `vendor/bin/testbench package:discover` 命令。详情请参阅 [Testbench 中的包测试](/zh-CN/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
```

顺序也很重要。请务必在 `config:cache` 之前执行 `package:discover`。如果先创建了配置缓存，后续新增的包配置（例如通过 `mergeConfigFrom` 注册的配置）可能不会被反映。

## 小结

* 自动发现不是基于 `composer.json` 的静态内容，而是通过读取 Composer 生成的 `vendor/composer/installed.json` 来运行。
* 结果作为纯 PHP 数组缓存到 `bootstrap/cache/packages.php`，仅需 `require` 即可高速加载。
* 缓存在 `composer install/update/dump-autoload` 各事件中被自动删除，下次启动时进行重建。
* 在使用 `--no-scripts` 执行 Composer 的环境中，需要显式调用 `php artisan package:discover`。
* 在 `dont-discover` 中指定 `*` 可以整体禁用自动发现。

## 相关页面

* [包开发基础](/zh-CN/advanced/package-development) — 服务提供者与 `extra.laravel` 的基本用法
* [延迟服务提供者](/zh-CN/advanced/deferred-provider) — 优化已发现的提供者的加载时机
* [Orchestra Testbench 中的包测试](/zh-CN/advanced/package-testing) — Testbench 独有的 `package:discover` 命令


## Related topics

- [Laravel 包开发](/zh-CN/advanced/package-development.md)
- [Laravel 11 之后的应用结构](/zh-CN/advanced/app-structure.md)
- [使用 Testbench Workbench 进行包开发](/zh-CN/advanced/package-workbench.md)
- [实现自定义认证 Guard](/zh-CN/advanced/custom-auth-guard.md)
- [引擎 API 集成应用开发指南 - VOICEVOX for Laravel](/zh-CN/packages/laravel-voicevox/app-guide.md)
