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

# Package auto-discovery internals

> Read through the internal implementation of the PackageManifest class and learn how composer.json extra.laravel data is cached and loaded as bootstrap/cache/packages.php.

## About this page

[Laravel Package Development](/en/advanced/package-development) introduced how service providers and facades are automatically registered when you define the `extra.laravel` section in `composer.json`. This page explains what happens behind the scenes at the source-code level, focusing on the `Illuminate\Foundation\PackageManifest` class.

<Info>
  This page is a companion to [Laravel Package Development](/en/advanced/package-development). Read that guide first if you want to learn the basic usage of auto-discovery before the internals.
</Info>

## Auto-discovery flow

```mermaid theme={null}
flowchart TD
    A["composer install / update"] --> B["Composer fires the<br>post-autoload-dump event"]
    B --> C["Illuminate\\Foundation\\ComposerScripts::postAutoloadDump()"]
    C --> D["Deletes cache files such as<br>bootstrap/cache/packages.php"]
    D --> E["PackageManifest::build() runs<br>on the next application boot"]
    E --> F["Reads vendor/composer/installed.json"]
    F --> G["Collects each package's extra.laravel"]
    G --> H["Applies dont-discover exclusions"]
    H --> I["Writes bootstrap/cache/packages.php"]
    I --> J["Application reads providers()/aliases()<br>and registers service providers"]
```

## The `PackageManifest` class

The core of auto-discovery is `Illuminate\Foundation\PackageManifest`. The following is a summarized implementation as of the framework 13.x branch.

```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)
            : [];
    }
}
```

There are three important points:

* **The manifest is cached in memory after the first load** through the `$this->manifest` property. Even if `providers()` is called multiple times in one request, file I/O happens only once.
* **`build()` runs only when the manifest file does not exist**. In normal operation, the manifest is not rebuilt on every request.
* The manifest itself is a plain PHP file that returns an array (`bootstrap/cache/packages.php`), which is the fastest shape for loading it with `require`.

## Building the manifest

The `build()` method is where Laravel aggregates information from Composer metadata.

```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());
}
```

The key detail is that Laravel does not parse each package's `composer.json` directly. Instead, it reads **`vendor/composer/installed.json`**. Composer generates this file during `composer install` and `composer update`, and it contains metadata for every installed package. Because each package's `extra` section is consolidated there, Laravel reads only Composer-managed information.

<Info>
  `installed.json` is normally ignored by Git. Auto-discovery does not work during an initial setup before `vendor/` exists; the cache can be built only after `composer install` finishes.
</Info>

### Two places to define `dont-discover`

You can define `dont-discover` in either a package's `composer.json` or the application's `composer.json`, but the meaning differs.

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

```json title="Application composer.json" theme={null}
"extra": {
    "laravel": {
        "dont-discover": [
            "acme/courier"
        ]
    }
}
```

Looking at the implementation of `build()`, the `$ignore` array is accumulated by merging each package's `configuration['dont-discover']`. In other words, a package can technically disable auto-discovery for one of its dependencies. That can be useful when a package internally uses a subpackage and wants to avoid duplicate provider registration. In day-to-day application development, however, `dont-discover` is most commonly used from the application side.

### Setting `dont-discover` to `*`

When the array returned by `packagesToIgnore()` contains `*`, `$ignoreAll` becomes `true`, and **auto-discovery is disabled for every package**. This is useful when you want to avoid auto-discovery overhead in CI or tests, or when you want to manage providers completely manually in `bootstrap/providers.php`.

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

## The cache file

`getCachedPackagesPath()` returns the value of the `APP_PACKAGES_CACHE` environment variable when it exists; otherwise, it returns `bootstrap/cache/packages.php`.

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

Open the file directly and you will see that it simply returns an associative array.

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

Because package names are used as keys, the output of `php artisan package:discover` shows which packages were discovered.

## When the cache is rebuilt

`Illuminate\Foundation\ComposerScripts` hooks into three Composer events, and all of them call the same `clearCompiled()` method.

```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);
    }
}
```

This means `composer install`, `composer update`, and `composer dump-autoload` all delete the config cache, service cache, and package cache together. The next time Laravel boots, `PackageManifest::build()` runs and rebuilds the cache from the current `installed.json`.

This is the standard behavior of a Laravel project registered in the `scripts` section of `composer.json`.

```json title="Excerpt from a Laravel application's 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"
    ]
}
```

## Manual rebuilds with `php artisan package:discover`

If the cache is stale, or if `vendor/` was changed directly without going through Composer, manually rebuild it with the `package:discover` command.

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

The command itself is a very thin wrapper.

```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());
    }
}
```

It simply calls `$manifest->build()` and prints the result; there is almost no command-specific logic. If your CI pipeline runs `composer install --no-scripts`, Composer events will not fire, so you need to call this command explicitly.

<Warning>
  When testing packages with [Orchestra Testbench](/en/advanced/package-testing), Testbench provides its own `vendor/bin/testbench package:discover` command. See [Package testing with Orchestra Testbench](/en/advanced/package-testing) for details. It is different from Artisan's `package:discover` and builds a manifest for Testbench's dedicated skeleton application.
</Warning>

## Deployment notes

Production deployments commonly run `composer install --no-dev --optimize-autoloader`. If you also pass `--no-scripts`, the package cache will not be updated. Add an explicit rebuild step to the deployment script to stay safe.

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

The order matters. Run `package:discover` before `config:cache`. If the config cache is created first, configuration registered by newly added packages, such as configuration merged with `mergeConfigFrom`, may not be reflected.

## Summary

* Auto-discovery does not read static `composer.json` files directly; it reads the `vendor/composer/installed.json` file generated by Composer.
* Results are cached as a plain PHP array in `bootstrap/cache/packages.php` and loaded quickly with `require`.
* The cache is automatically deleted by `composer install`, `composer update`, and `composer dump-autoload` events, then rebuilt on the next application boot.
* Environments that run Composer with `--no-scripts` need to explicitly call `php artisan package:discover`.
* Setting `dont-discover` to `*` disables auto-discovery entirely.

## Related pages

* [Laravel Package Development](/en/advanced/package-development) — Basic usage of service providers and `extra.laravel`
* [Deferred service providers](/en/advanced/deferred-provider) — Optimize when discovered providers are loaded
* [Package testing with Orchestra Testbench](/en/advanced/package-testing) — Testbench's dedicated `package:discover` command


## Related topics

- [Laravel Package Development](/en/advanced/package-development.md)
- [PHP AST](/en/advanced/php-ast.md)
- [Laravel Telescope](/en/telescope.md)
- [Core — AT Protocol Core Operations](/en/packages/laravel-bluesky/core.md)
- [HTTP Tests](/en/http-tests.md)
