Что такое 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
);
}
}