HardТеория8 min

Service Providers

register() vs boot(), отложенные провайдеры, пакетные провайдеры, создание своих провайдеров

Что такое Service Provider

Service Providers -- это центральное место для конфигурации и инициализации компонентов приложения. Каждый компонент Laravel (маршрутизация, валидация, база данных, очереди и т.д.) загружается через свой Service Provider.

<?php
declare(strict_types=1);

// Service Provider is the BOOTSTRAPPING center of Laravel
// Every major Laravel component has its own provider:
// - RouteServiceProvider (registered internally in Laravel 11)
// - AuthServiceProvider
// - EventServiceProvider
// - DatabaseServiceProvider
// - CacheServiceProvider
// - QueueServiceProvider
// - etc.

// YOUR application also uses providers to:
// - Register container bindings
// - Register event listeners
// - Add middleware
// - Publish configuration files
// - Boot any application service

Ключевой концепт: Service Providers -- единственное место, где вы можете гарантированно зарегистрировать привязки контейнера до того, как приложение начнёт обрабатывать запросы. Вся инициализация приложения проходит через провайдеры.

Создание Service Provider

# Create a new provider
php artisan make:provider PaymentServiceProvider
<?php
declare(strict_types=1);

namespace App\Providers;

use App\Contracts\PaymentGatewayInterface;
use App\Services\StripePaymentGateway;
use Illuminate\Support\ServiceProvider;

final class PaymentServiceProvider extends ServiceProvider
{
    /**
     * Register services in the container.
     * Called BEFORE any other provider is booted.
     */
    public function register(): void
    {
        // ONLY bind things into the container here
        // Do NOT use other services — they may not be registered yet

        $this->app->singleton(
            PaymentGatewayInterface::class,
            StripePaymentGateway::class,
        );
    }

    /**
     * Bootstrap application services.
     * Called AFTER ALL providers have been registered.
     */
    public function boot(): void
    {
        // Safe to use any service here
        // All providers are registered at this point

        // Register views, routes, event listeners, etc.
    }
}

register() vs boot() -- критическое различие

Метод register()

<?php
declare(strict_types=1);

namespace App\Providers;

use App\Contracts\CacheInterface;
use App\Contracts\LoggerInterface;
use App\Contracts\PaymentGatewayInterface;
use App\Services\PaymentGateway;
use App\Services\RedisCacheService;
use Illuminate\Support\ServiceProvider;

final class AppServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        // CORRECT: Simple container bindings
        $this->app->singleton(
            PaymentGatewayInterface::class,
            PaymentGateway::class,
        );

        $this->app->bind(CacheInterface::class, RedisCacheService::class);

        // CORRECT: Merge package configuration
        $this->mergeConfigFrom(
            __DIR__ . '/../../config/payment.php',
            'payment'
        );

        // CORRECT: Conditional binding
        if ($this->app->environment('testing')) {
            $this->app->singleton(
                PaymentGatewayInterface::class,
                FakePaymentGateway::class,
            );
        }

        // ❌ WRONG in register():
        // Route::get(...);          // Routes not loaded yet
        // Event::listen(...);       // Events may not be registered
        // Gate::define(...);        // Auth may not be ready
        // View::composer(...);      // View system may not be ready
        // $this->app->make(SomeService::class); // Other providers not registered
    }
}

Ловушка экзамена: В register() можно ТОЛЬКО регистрировать привязки в контейнере. Нельзя использовать другие сервисы, потому что они могут быть ещё не зарегистрированы. Метод register() вызывается для ВСЕХ провайдеров до вызова boot() для любого из них.

Метод boot()

<?php
declare(strict_types=1);

namespace App\Providers;

use App\Models\Order;
use App\Observers\OrderObserver;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\Facades\View;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\ServiceProvider;

final class AppServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        // ALL providers are registered — safe to use any service

        // Register model observers
        Order::observe(OrderObserver::class);

        // Register event listeners
        Event::listen(
            OrderPlaced::class,
            SendOrderConfirmation::class,
        );

        // Register authorization gates
        Gate::define('update-post', function ($user, $post) {
            return $user->id === $post->user_id;
        });

        // Register view composers
        View::composer('layouts.sidebar', function ($view) {
            $view->with('categories', Category::all());
        });

        // Register custom Blade directives
        Blade::directive('datetime', function (string $expression) {
            return "<?php echo ($expression)->format('d.m.Y H:i'); ?>";
        });

        // Register custom validation rules
        Validator::extend('phone', function ($attribute, $value) {
            return preg_match('/^\+?[1-9]\d{10,14}$/', $value);
        });

        // Publish assets from package
        $this->publishes([
            __DIR__ . '/../config/payment.php' => config_path('payment.php'),
        ], 'payment-config');

        // Load routes
        Route::middleware('api')
            ->prefix('api/v2')
            ->group(base_path('routes/api_v2.php'));

        // Load migrations
        $this->loadMigrationsFrom(__DIR__ . '/../database/migrations');

        // Load views
        $this->loadViewsFrom(__DIR__ . '/../resources/views', 'payment');

        // Load translations
        $this->loadTranslationsFrom(__DIR__ . '/../resources/lang', 'payment');
    }
}

Dependency Injection в boot()

<?php
declare(strict_types=1);

namespace App\Providers;

use App\Services\AnalyticsService;
use Illuminate\Routing\Router;
use Illuminate\Support\ServiceProvider;

final class AppServiceProvider extends ServiceProvider
{
    // boot() supports method injection!
    // The container resolves parameters automatically
    public function boot(Router $router, AnalyticsService $analytics): void
    {
        $router->pushMiddlewareToGroup('web', \App\Http\Middleware\TrackVisits::class);

        $analytics->registerDashboard();
    }
}

Для экзамена: Метод boot() поддерживает внедрение зависимостей через параметры метода (method injection). Контейнер автоматически разрешает типизированные параметры. Метод register() НЕ поддерживает method injection.

Порядок выполнения провайдеров

<?php
declare(strict_types=1);

// CRITICAL: Understanding the execution order

// Phase 1: ALL register() methods are called
// Provider A → register()
// Provider B → register()
// Provider C → register()

// Phase 2: ALL boot() methods are called
// Provider A → boot()
// Provider B → boot()
// Provider C → boot()

// This means:
// - In register(): You CANNOT rely on other providers' bindings
// - In boot(): ALL bindings from ALL providers are available

// The order of providers is defined in bootstrap/providers.php:
return [
    App\Providers\AppServiceProvider::class,
    App\Providers\PaymentServiceProvider::class,
    App\Providers\NotificationServiceProvider::class,
];

// Framework providers are loaded BEFORE application providers
// Package auto-discovered providers are loaded BETWEEN them

Регистрация провайдеров

В Laravel 11

<?php
declare(strict_types=1);

// bootstrap/providers.php — the list of service providers
return [
    App\Providers\AppServiceProvider::class,
    // Add your providers here
    App\Providers\EventServiceProvider::class,
    App\Providers\PaymentServiceProvider::class,
];

Автоматическая регистрация в AppServiceProvider

<?php
declare(strict_types=1);

namespace App\Providers;

use Illuminate\Support\ServiceProvider;

final class AppServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        // Register other providers programmatically
        $this->app->register(PaymentServiceProvider::class);

        // Conditional registration
        if ($this->app->environment('local')) {
            $this->app->register(\Laravel\Telescope\TelescopeServiceProvider::class);
        }
    }
}

Отложенные провайдеры (Deferred Providers)

Отложенные провайдеры загружаются ТОЛЬКО когда запрошенный ими сервис реально нужен.

<?php
declare(strict_types=1);

namespace App\Providers;

use App\Contracts\ReportGeneratorInterface;
use App\Services\ReportGenerator;
use Illuminate\Contracts\Support\DeferrableProvider;
use Illuminate\Support\ServiceProvider;

// Implement DeferrableProvider to make it deferred
final class ReportServiceProvider extends ServiceProvider implements DeferrableProvider
{
    public function register(): void
    {
        $this->app->singleton(
            ReportGeneratorInterface::class,
            function ($app) {
                return new ReportGenerator(
                    storage: $app->make('filesystem'),
                    cache: $app->make('cache'),
                );
            }
        );
    }

    /**
     * Get the services provided by the provider.
     * REQUIRED for deferred providers.
     *
     * @return array<class-string>
     */
    public function provides(): array
    {
        return [
            ReportGeneratorInterface::class,
        ];
    }
}
<?php
declare(strict_types=1);

// How deferred providers work:

// 1. On boot, Laravel reads provides() from each deferred provider
// 2. Creates a map: service → provider class name
// 3. Stores this in bootstrap/cache/services.php

// 4. When app()->make(ReportGeneratorInterface::class) is called:
//    a. Container checks if it has a binding → NO
//    b. Container checks deferred providers map → FOUND
//    c. Loads and registers the provider
//    d. Resolves the service

// Benefits:
// - Provider is NOT loaded on every request
// - Only loaded when its service is actually needed
// - Improves bootstrap performance for rarely-used services

// The map is cached in bootstrap/cache/services.php
// Rebuild with: php artisan optimize

Для Senior: Отложенные провайдеры реализуют паттерн Lazy Loading на уровне сервис-провайдеров. Метод provides() ОБЯЗАТЕЛЕН и должен возвращать массив абстракций (интерфейсов или имён классов), которые регистрирует провайдер. Без provides() фреймворк не знает, какие сервисы связаны с провайдером.

Пакетные провайдеры (Package Providers)

Автообнаружение пакетов (Package Discovery)

{
    "extra": {
        "laravel": {
            "providers": [
                "Vendor\\Package\\PackageServiceProvider"
            ],
            "aliases": {
                "Package": "Vendor\\Package\\Facades\\Package"
            }
        }
    }
}
<?php
declare(strict_types=1);

// Package auto-discovery:
// 1. When composer install/update runs
// 2. Laravel scans all packages for "extra.laravel" in composer.json
// 3. Found providers are cached in bootstrap/cache/packages.php
// 4. These providers are automatically registered

// To disable auto-discovery for a specific package:
// In your app's composer.json:
// "extra": {
//     "laravel": {
//         "dont-discover": [
//             "vendor/package"
//         ]
//     }
// }

// To disable all auto-discovery:
// "dont-discover": ["*"]

Создание пакетного провайдера

<?php
declare(strict_types=1);

namespace Vendor\Analytics;

use Illuminate\Support\ServiceProvider;

final class AnalyticsServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        // Merge default config with user's config
        $this->mergeConfigFrom(
            __DIR__ . '/../config/analytics.php',
            'analytics'
        );

        // Register package bindings
        $this->app->singleton(AnalyticsManager::class, function ($app) {
            return new AnalyticsManager(
                config: $app['config']['analytics'],
            );
        });
    }

    public function boot(): void
    {
        // Publishable configuration
        $this->publishes([
            __DIR__ . '/../config/analytics.php' => config_path('analytics.php'),
        ], 'analytics-config');

        // Publishable migrations
        $this->publishes([
            __DIR__ . '/../database/migrations/' => database_path('migrations'),
        ], 'analytics-migrations');

        // Publishable views
        $this->publishes([
            __DIR__ . '/../resources/views' => resource_path('views/vendor/analytics'),
        ], 'analytics-views');

        // Load migrations automatically (without publishing)
        $this->loadMigrationsFrom(__DIR__ . '/../database/migrations');

        // Load package routes
        $this->loadRoutesFrom(__DIR__ . '/../routes/web.php');

        // Load package views with namespace
        $this->loadViewsFrom(__DIR__ . '/../resources/views', 'analytics');
        // Usage: view('analytics::dashboard')

        // Load translations
        $this->loadTranslationsFrom(__DIR__ . '/../resources/lang', 'analytics');
        // Usage: __('analytics::messages.welcome')

        // Register Artisan commands
        if ($this->app->runningInConsole()) {
            $this->commands([
                \Vendor\Analytics\Commands\GenerateReport::class,
                \Vendor\Analytics\Commands\PruneOldData::class,
            ]);
        }
    }
}

Важно: Метод mergeConfigFrom() выполняет рекурсивное слияние конфигурации пакета с пользовательской конфигурацией. Это позволяет пользователю переопределять только те параметры, которые он хочет изменить, остальные берутся из конфигурации пакета по умолчанию.

Публикация ресурсов пакета

# Publish specific tag
php artisan vendor:publish --tag=analytics-config
php artisan vendor:publish --tag=analytics-migrations

# Publish by provider
php artisan vendor:publish --provider="Vendor\Analytics\AnalyticsServiceProvider"

# Publish all publishable assets
php artisan vendor:publish --all

# Force overwrite existing files
php artisan vendor:publish --tag=analytics-config --force

Практический пример: полный Service Provider

<?php
declare(strict_types=1);

namespace App\Providers;

use App\Contracts\NotificationChannelInterface;
use App\Contracts\PaymentGatewayInterface;
use App\Contracts\ShippingCalculatorInterface;
use App\Models\Order;
use App\Models\Product;
use App\Observers\OrderObserver;
use App\Observers\ProductObserver;
use App\Services\Notifications\EmailChannel;
use App\Services\Notifications\SmsChannel;
use App\Services\Notifications\PushChannel;
use App\Services\Payment\StripeGateway;
use App\Services\Shipping\FedExCalculator;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\ServiceProvider;

final class AppServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        // 1. Interface bindings
        $this->app->singleton(
            PaymentGatewayInterface::class,
            StripeGateway::class,
        );

        $this->app->bind(
            ShippingCalculatorInterface::class,
            FedExCalculator::class,
        );

        // 2. Tagged services
        $this->app->bind('notification.email', EmailChannel::class);
        $this->app->bind('notification.sms', SmsChannel::class);
        $this->app->bind('notification.push', PushChannel::class);

        $this->app->tag(
            ['notification.email', 'notification.sms', 'notification.push'],
            'notification.channels'
        );

        // 3. Contextual bindings
        $this->app->when(\App\Services\OrderNotifier::class)
            ->needs(NotificationChannelInterface::class)
            ->give(EmailChannel::class);

        // 4. Environment-specific bindings
        if ($this->app->environment('local')) {
            $this->app->register(\Laravel\Telescope\TelescopeServiceProvider::class);
        }
    }

    public function boot(): void
    {
        // 1. Model observers
        Order::observe(OrderObserver::class);
        Product::observe(ProductObserver::class);

        // 2. Authorization gates
        Gate::define('manage-orders', function ($user) {
            return $user->hasRole('admin') || $user->hasRole('manager');
        });

        // 3. Blade directives
        Blade::directive('money', function (string $expression) {
            return "<?php echo number_format($expression, 2, '.', ' ') . ' ₽'; ?>";
        });

        Blade::if('admin', function () {
            return auth()->check() && auth()->user()->isAdmin();
        });

        // 4. Macros
        \Illuminate\Support\Str::macro('initials', function (string $name): string {
            return collect(explode(' ', $name))
                ->map(fn (string $part) => strtoupper(mb_substr($part, 0, 1)))
                ->join('');
        });
    }
}

Тестирование с провайдерами

<?php
declare(strict_types=1);

namespace Tests\Feature;

use App\Contracts\PaymentGatewayInterface;
use App\Services\Payment\FakeGateway;
use App\Providers\PaymentServiceProvider;
use Tests\TestCase;

final class PaymentTest extends TestCase
{
    protected function setUp(): void
    {
        parent::setUp();

        // Override binding for tests
        $this->app->singleton(
            PaymentGatewayInterface::class,
            FakeGateway::class,
        );
    }

    public function test_provider_registers_binding(): void
    {
        // Verify provider registers the correct binding
        $resolved = $this->app->make(PaymentGatewayInterface::class);

        $this->assertInstanceOf(FakeGateway::class, $resolved);
    }

    public function test_provider_is_deferred(): void
    {
        $provider = new PaymentServiceProvider($this->app);

        // Check if provider implements DeferrableProvider
        $this->assertInstanceOf(
            \Illuminate\Contracts\Support\DeferrableProvider::class,
            $provider
        );
    }
}

Проверь себя

Поддерживает ли метод `boot()` сервис-провайдера внедрение зависимостей через параметры?

Что ОБЯЗАН возвращать метод `provides()` в отложенном (deferred) провайдере?

В Laravel 11, где определяется список сервис-провайдеров приложения?

Как работает автообнаружение пакетов (Package Discovery) в Laravel?

Можно ли использовать другие сервисы (например, `Route::get()` или `Event::listen()`) внутри метода `register()` сервис-провайдера?