How does Laravel package auto-discovery register a package's service provider without anyone editing bootstrap/providers.php?
answer
- the package's own composer.json
- extra.laravel providers and aliases
- vendor/composer/installed.json
- bootstrap/cache/packages.php
- package:discover after autoload dump
basics
~10 sA package lists its providers and facade aliases under extra.laravel in its composer.json. package:discover reads every installed package's entry into bootstrap/cache/packages.php, and Laravel registers those providers at boot.
solid answer
~30 sThe package declares itself in its own `composer.json`: `"extra": {"laravel": {"providers": [...], "aliases": {...}}}`. Composer copies that metadata into `vendor/composer/installed.json` when it installs the package. The skeleton's `post-autoload-dump` script then runs `php artisan package:discover`, whose `PackageManifest::build()` reads `installed.json`, keeps each package's `extra.laravel` block (minus anything in `dont-discover`) and writes the result to `bootstrap/cache/packages.php`. At boot, the provider registration step loads providers in a fixed order: the framework's `Illuminate\` providers, then the discovered package providers, then the application's own; facade aliases from the manifest are registered through the alias loader. If the manifest file is missing, Laravel rebuilds it on first use.
code
json · 13 lines{
"name": "acme/audit-trail",
"extra": {
"laravel": {
"providers": [
"Acme\\AuditTrail\\AuditTrailServiceProvider"
],
"aliases": {
"AuditTrail": "Acme\\AuditTrail\\Facades\\AuditTrail"
}
}
}
}go deeper
Know that installing a Laravel package usually registers its provider automatically, because the package lists it under extra.laravel in its composer.json.
Trace the chain: installed.json, package:discover, bootstrap/cache/packages.php, then registration order with framework, package and app providers.
Diagnose missing or phantom providers after deploys by checking the manifest and the Composer scripts, and use the registration order to override package bindings.
Decide whether in-house packages rely on discovery or explicit registration, weighing zero-touch installs against visibility of what each app loads.
## The problem discovery solves A Laravel package usually needs a **service provider** to bind services, load routes and views, and register publishable files. Before discovery, every consuming app had to add the provider to its provider list by hand. With three internal apps using an in-house audit-trail package, that is three edits per new provider, and one forgotten edit is a silent bug. **Package discovery** moves that registration into the package itself. ## Step 1: the package declares its providers The package's `composer.json` carries a Laravel-specific block: ```json "extra": { "laravel": { "providers": ["Acme\\AuditTrail\\AuditTrailServiceProvider"], "aliases": {"AuditTrail": "Acme\\AuditTrail\\Facades\\AuditTrail"} } } ``` - `providers` lists provider classes to register. - `aliases` maps a short global class name to a facade class, so `AuditTrail::record(...)` works without an import. ## Step 2: Composer records it, Laravel reads it 1. When the app runs `composer install` or `composer update`, Composer writes every installed package's metadata, including `extra`, into `vendor/composer/installed.json`. 2. After regenerating the autoloader, Composer runs the app's `post-autoload-dump` scripts. The laravel/laravel skeleton's list clears cached bootstrap files and then runs `php artisan package:discover`. 3. `package:discover` calls `PackageManifest::build()`, which reads `installed.json`, takes each package's `extra.laravel` block, drops packages named in `dont-discover` (or all of them for `*`), drops packages with no Laravel block, and writes the result as a PHP array to **`bootstrap/cache/packages.php`**. The command prints each discovered package. If `bootstrap/cache` is not writable, the build throws an exception saying the directory must be present and writable. ## Step 3: registration at boot When the application boots, the provider registration step builds one list: | Order | Source | |---|---| | 1 | framework providers (class names starting with `Illuminate\`) | | 2 | providers from the package manifest | | 3 | the application's providers, such as those in `bootstrap/providers.php` | Because package providers register **before** the app's own, an app's `AppServiceProvider` can override a binding a package made. Facade aliases from the manifest are merged with any configured aliases and handed to Laravel's alias loader, which creates each alias lazily the first time the short name is used. The manifest is read lazily: if `bootstrap/cache/packages.php` does not exist when Laravel first needs it, it is built on the spot from `installed.json`. ## Practical consequences - **No app edits.** Installing `acme/audit-trail` in each of the three apps is enough; removing it removes the provider on the next discovery. - **A stale manifest shows up as a missing or phantom provider.** If a deploy copies `vendor/` without running the Composer scripts, re-running `php artisan package:discover` rebuilds the manifest. - **Only installed metadata counts.** Editing the package's `composer.json` inside `vendor/` does nothing; the change must be released and installed so it reaches `installed.json`. - **Discovery is opt-out.** Apps can exclude a package with `dont-discover` and register its provider themselves. ## Trade-offs of discovery Discovery is convenient, but it has costs worth naming in an interview: - **Less visibility.** Nothing in the app's own files says which package providers boot; you have to read `bootstrap/cache/packages.php` or the installed packages' metadata. - **Everything installed boots.** A package added for one experiment keeps registering its provider until it is removed or excluded. - **Order is fixed by source, not by choice.** Among discovered packages, the order follows the manifest; an app that needs one package's provider to run before another's registers them explicitly instead. For the in-house audit-trail package these costs are small: the three apps all want it on, and the team documents it in each app's README. Teams that want a reviewed list of everything that boots can turn discovery off with `dont-discover` and register providers by hand. ## What interviewers probe - Where the declaration lives (the package's `composer.json`, under `extra.laravel`). - What `package:discover` produces (`bootstrap/cache/packages.php`) and when it runs. - The registration order and why it lets the app override package bindings. - How to diagnose a provider that is not loading: check `installed.json`, re-run discovery, check `dont-discover`.
- The app's AppServiceProvider and the audit-trail package both bind the same interface. Which binding wins?The app's. Laravel registers framework providers first, then discovered package providers, then the application's providers, and a later binding for the same key replaces the earlier one. That order is what lets an app swap a package's implementation without forking the package.
- A deploy copies vendor/ from a build step but the new package's provider never loads. What do you check?Whether `bootstrap/cache/packages.php` was rebuilt. The manifest is written by `package:discover`, which normally runs from the skeleton's `post-autoload-dump` script; a build that skips scripts or ships an old manifest leaves the provider out. Run `php artisan package:discover` in the release and confirm the package is listed.
saying these in an interview costs you the question
- Discovery scans vendor/ for ServiceProvider classes by name
- Package providers must still be added to bootstrap/providers.php
- Discovered providers register after the app's own providers
- Editing composer.json inside vendor/ changes what is discovered
- Facade aliases need a line in config/app.php even with discovery