0

register() vs boot() — timing and what belongs where

Intermediate5 min read·lv-03-002
interviewlaravel-src

Concept

The register() and boot() methods in a service provider have strict rules about what they're allowed to do. Violating these rules causes subtle bugs that only manifest in edge cases or under certain loading orders.

register() — Container bindings ONLY:

  • Bind interfaces to implementations.
  • Register singletons, scoped bindings, instances.
  • DO NOT call other services: no app()->make(), no Event::listen(), no Route::get().
  • Why: When register() runs for provider X, other providers may not have registered their bindings yet. Accessing a service that depends on another provider's binding can fail.

boot() — Everything else:

  • Safe to call any service because all register() methods have already run.
  • Register routes ($this->loadRoutesFrom()).
  • Register event listeners.
  • Publish configuration, migrations, views.
  • Register Blade directives and components.
  • Extend models, macros, etc.

Common mistake: Trying to resolve a service inside register():

php
public function register(): void
{
    $logger = app()->make(LoggerInterface::class); // WRONG — may not exist yet
    $this->app->bind(MyService::class, fn() => new MyService($logger));
}

Fix: Wrap in a closure — resolution is deferred to when the binding is actually needed:

php
public function register(): void
{
    $this->app->bind(MyService::class, fn($app) => new MyService(
        $app->make(LoggerInterface::class) // resolved at usage time, not registration time
    ));
}

Code Example

php
<?php
namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\Facades\Event;

class OrderServiceProvider extends ServiceProvider
{
    /**
     * REGISTER — bindings only.
     * Closure form defers resolution to usage time.
     */
    public function register(): void
    {
        $this->app->singleton(\App\Services\OrderService::class, function($app) {
            // $app->make() calls here are safe — they're inside a closure
            // resolved lazily when OrderService is first needed
            return new \App\Services\OrderService(
                $app->make(\App\Repositories\OrderRepository::class),
                $app->make(\App\Services\InventoryService::class),
            );
        });
    }

    /**
     * BOOT — everything else.
     * All providers have been registered — safe to use any service.
     */
    public function boot(): void
    {
        // Routes
        $this->loadRoutesFrom(__DIR__ . '/../../routes/orders.php');

        // Event listeners
        Event::listen(
            \App\Events\OrderPlaced::class,
            \App\Listeners\SendOrderConfirmation::class,
        );

        // Gate policy
        Gate::policy(\App\Models\Order::class, \App\Policies\OrderPolicy::class);

        // Blade directive
        \Illuminate\Support\Facades\Blade::directive('orderStatus', function($expression) {
            return "<?php echo App\Helpers\OrderHelper::statusBadge($expression); ?>";
        });
    }
}