Bootstrappers — LoadEnvironmentVariables, LoadConfiguration, RegisterProviders
Concept
Bootstrappers are the first layer of framework initialization — they run before any service provider's register() or boot() method, and before any middleware. Each bootstrapper is a class with a single bootstrap(Application $app) method. They are defined on the HTTP Kernel in the $bootstrappers array and executed via $app->bootstrapWith($bootstrappers).
LoadEnvironmentVariables uses the vlucas/phpdotenv library to parse .env from the application root. It populates $_ENV and $_SERVER superglobals and makes values available via env(). Critically, if .env is missing (production boxes often use real environment variables instead), this bootstrapper is smart enough to not throw — it only loads .env if the file exists. The APP_ENV variable loaded here determines which environment-specific config overrides apply.
LoadConfiguration iterates every PHP file in config/ and loads it into the Illuminate\Config\Repository. It uses a smart caching system: if bootstrap/cache/config.php exists (created by php artisan config:cache), it loads that single pre-compiled file instead of globbing through config/. This is why config:cache gives a measurable performance improvement — a single require replaces dozens of file reads and array merges.
HandleExceptions sets error_reporting(E_ALL), then registers a custom set_error_handler that converts PHP errors to ErrorException instances (so all PHP errors can be caught with try/catch or the exception handler). It also registers set_exception_handler pointing at App\Exceptions\Handler::handleException(). Finally, it registers register_shutdown_function to catch fatal errors that PHP normally wouldn't let you catch.
RegisterFacades takes the aliases array from config/app.php and calls class_alias() for each entry. This is how use Illuminate\Support\Facades\Cache becomes unnecessary — Cache::get() works because Cache is an alias for the full class. It also calls Facade::setFacadeApplication($app) so every Facade knows which container to resolve from.
RegisterProviders reads the providers from config/app.php plus any discovered packages (via Composer's extra.laravel.providers key). For each provider, it instantiates the class and calls register(). Deferred providers are special: they are recorded in a manifest file (bootstrap/cache/services.php) but NOT instantiated yet.
BootProviders calls boot() on every registered (non-deferred) provider. This is when route macros, validation rules, event listeners, and Blade directives get registered.
Code Example
<?php
// Inspecting bootstrapper execution — useful for understanding timing
// Where is the config actually loaded from?
// If this file exists, config:cache is active:
file_exists(base_path('bootstrap/cache/config.php')); // true = cached
// The env() function reads from the array LoadEnvironmentVariables populated
$value = env('APP_KEY'); // reads from $_ENV['APP_KEY']
// LoadConfiguration makes config() work anywhere after this point
config('database.default'); // 'sqlite' or 'mysql' etc.
// HandleExceptions converts PHP notices/warnings to ErrorException
// This means you can catch them:
try {
$arr = [];
$val = $arr['missing']; // Would be a PHP notice
} catch (\ErrorException $e) {
// Caught! Because HandleExceptions registered a custom error handler
}
// RegisterFacades: class_alias() registers these so the short names work
// Defined in config/app.php under 'aliases':
// 'Cache' => Illuminate\Support\Facades\Cache::class
// 'DB' => Illuminate\Support\Facades\DB::class
// ... ~30 more
use Cache; // Works because of class_alias
Cache::put('key', 'value', 3600);<?php
// LoadEnvironmentVariables bootstrapper — simplified internals
namespace Illuminate\Foundation\Bootstrap;
use Illuminate\Contracts\Foundation\Application;
use Dotenv\Dotenv;
class LoadEnvironmentVariables
{
public function bootstrap(Application $app): void
{
// Skip if already loaded (e.g. real server environment variables set)
if ($app->configurationIsCached()) {
return;
}
// Check for environment-specific .env file first
// e.g. .env.testing when APP_ENV=testing
$this->checkForSpecificEnvironmentFile($app);
// Load the .env file if it exists
// vlucas/phpdotenv populates $_ENV and $_SERVER
try {
$this->createDotenv($app)->safeLoad();
} catch (\Dotenv\Exception\InvalidFileException $e) {
$this->writeErrorAndDie($e);
}
}
protected function checkForSpecificEnvironmentFile(Application $app): void
{
// APP_ENV may already be set as a real environment variable
// In that case, look for .env.{APP_ENV}
if (($env = $app->make('request')->server('APP_ENV')) !== null) {
$this->setEnvironmentFilePath($app, $app->environmentFile().'.'.$env);
}
}
}Interview Q&A
Q: What does php artisan config:cache do and why does it improve performance?
config:cache merges all files from your config/ directory into a single flat PHP array and writes it to bootstrap/cache/config.php. On subsequent requests, LoadConfiguration detects this file and does a single require instead of globbing through the config directory, reading each PHP file, and merging all the arrays. This eliminates dozens of file I/O operations and array merge calls on every request. The trade-off: after running config:cache, calling env() directly in your code (outside of config files) no longer works, because the .env file isn't loaded when the config cache exists. This is a common gotcha — always use config('app.var') in your application code, not env().
Q: How does HandleExceptions make PHP notices catchable with try/catch?
HandleExceptions::bootstrap() calls set_error_handler([$this, 'handleError']). That handler converts PHP errors (notices, warnings, deprecations) into \ErrorException instances and throws them. Because they are now Throwable, the normal try/catch machinery works. Without this, a PHP notice would just produce a warning and execution would continue, making silent bugs very easy to miss. With strict_types=1 and this handler active, your application has zero tolerance for undefined variable access, type mismatches in non-strict contexts, and similar sloppiness. Laravel then routes these thrown exceptions through the exception handler for consistent logging and response formatting.
Q: What is the difference between the providers array in config/app.php and the deferred providers manifest?
Providers in config/app.php are divided into eager and deferred. Eager providers are instantiated and registered immediately during RegisterProviders. Deferred providers — those that implement DeferrableProvider and define a provides() method listing the bindings they register — are NOT instantiated during the bootstrap. Instead, their class name and the bindings they provide are written to bootstrap/cache/services.php (the deferred services manifest). When any code first tries to resolve one of those bindings from the container, Laravel checks the manifest, finds the responsible provider, instantiates it, and calls register() right then. This means a provider like QueueServiceProvider is never loaded for web requests that don't use the queue, saving memory and time.