Skip to main content

About this page

Laravel 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.
This page is a companion to Laravel Package Development. Read that guide first if you want to learn the basic usage of auto-discovery before the internals.

Auto-discovery flow

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.
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.
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.
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.

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.
Package composer.json
Application composer.json
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.

The cache file

getCachedPackagesPath() returns the value of the APP_PACKAGES_CACHE environment variable when it exists; otherwise, it returns bootstrap/cache/packages.php.
Open the file directly and you will see that it simply returns an associative array.
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.
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.
Excerpt from a Laravel application's composer.json

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.
The command itself is a very thin wrapper.
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.
When testing packages with Orchestra Testbench, Testbench provides its own vendor/bin/testbench package:discover command. See Package testing with Orchestra Testbench for details. It is different from Artisan’s package:discover and builds a manifest for Testbench’s dedicated skeleton application.

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.
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.
Last modified on July 19, 2026