Bootstrapper vs Service Provider — two different boot phases
Concept
Bootstrapper vs Service Provider — two distinct phases in the Laravel boot process. Both prepare the application, but at different levels.
Bootstrapper (internal): A low-level class that prepares the framework itself before any application code runs. Bootstrappers run early in the HTTP kernel's bootstrap() method. They're framework internals — you don't usually write your own.
Built-in bootstrappers (in order):
LoadEnvironmentVariables— loads.envfile.LoadConfiguration— merges allconfig/*.phpfiles.HandleExceptions— registers the global exception handler.RegisterFacades— resolves thealiasesarray (Facade::class).RegisterProviders— discovers and registers all service providers.BootProviders— callsboot()on every registered service provider.
Service Provider: An application-level class that registers bindings and bootstraps services. This is where YOUR code hooks into the framework's lifecycle.
Two phases in Service Providers:
register(): Bind things into the service container. This runs early — other providers may not be booted yet. Only do bindings here, nothing that depends on other services.boot(): Called after ALL providers are registered. Safe to depend on other providers' bindings. Register routes, event listeners, view composers, macros, validation rules, gates.
The key rule: register() = bind things. boot() = use things. Never call other services in register() — they may not be registered yet.
Service Provider discovery: Listed in config/app.php providers array. Laravel 11+ uses bootstrap/providers.php. Packages add themselves via Composer's extra.laravel.providers auto-discovery.
Code Example
<?php
// BOOTSTRAPPER — framework internal (you'd never write this, but understand it)
// vendor/laravel/framework/src/Illuminate/Foundation/Bootstrap/LoadConfiguration.php
class LoadConfiguration
{
public function bootstrap(Application $app): void
{
$config = new Repository;
// Loads every PHP file from config/ directory
foreach ($this->getConfigurationFiles($app) as $key => $path) {
$config->set($key, require $path);
}
$app->instance('config', $config);
date_default_timezone_set($config->get('app.timezone', 'UTC'));
}
}
// You don't interact with bootstrappers — they run automatically
// SERVICE PROVIDER — this is what YOU write
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
class OrderServiceProvider extends ServiceProvider
{
// REGISTER — only bind things into the container
public function register(): void
{
// ✅ Correct: bind interfaces to implementations
$this->app->bind(
\App\Contracts\OrderRepositoryInterface::class,
\App\Repositories\EloquentOrderRepository::class,
);
$this->app->singleton(
\App\Services\PaymentGateway::class,
fn($app) => new \App\Services\StripeGateway(config('services.stripe.key')),
);
// ❌ WRONG: don't call other services in register — they may not be ready
// \Event::listen(...); // Event facade might not be registered yet
}
// BOOT — use things from the container, register behavior
public function boot(): void
{
// ✅ Safe here — all providers are registered by now
\Gate::define('manage-orders', fn(User $u) => $u->hasRole('manager'));
\Event::listen(
\App\Events\OrderPlaced::class,
\App\Listeners\SendOrderConfirmation::class,
);
\Validator::extend('valid_phone', function ($attribute, $value) {
return preg_match('/^\+?\d{10,15}$/', $value);
});
// Register macros
\Illuminate\Support\Collection::macro('toAssoc', function(string $key) {
return $this->keyBy($key);
});
// Load routes from a custom location
$this->loadRoutesFrom(__DIR__ . '/../../routes/orders.php');
$this->loadMigrationsFrom(__DIR__ . '/../../database/migrations');
}
}