Увійти Реєстрація
Блог Серії
Кар'єра
Вакансії Компанії
Навчання
Документація Співбесіди Тестування Відео
Екосистема
Пакети Ресурси Проєкти Інструменти Події
Інше
Про нас Реклама

Питання на співбесіді: Сервіс-провайдери

Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.

7 питань

У файлі bootstrap/providers.php - він повертає масив класів провайдерів застосунку.

<?php

return [
    App\Providers\AppServiceProvider::class,
    App\Providers\BillingServiceProvider::class,
];

Новий провайдер:

php artisan make:provider BillingServiceProvider

Команда створить клас і сама допише його в bootstrap/providers.php. Створений вручну клас треба додати туди самостійно, інакше Laravel про нього не знає.

Що змінилося: до Laravel 11 провайдери перелічували в масиві providers у config/app.php, а в застосунку їх було п'ять-шість стандартних. Тепер у свіжому проєкті один AppServiceProvider, а реєстрація винесена в окремий короткий файл.

Провайдери пакетів зазвичай не реєструють узагалі: Composer-пакет оголошує свій провайдер у composer.json, і Laravel підхоплює його сам (package discovery).

У самому провайдері - два методи: register() для прив'язок у контейнер і boot() для всього, що спирається на інші сервіси: макроси, події, політики, Model::preventLazyLoading().

Докладніше в документації: Реєстрація провайдерів

Service Providers - центральне місце бутстрапу застосунку. Саме тут реєструються біндинги контейнера, слухачі подій, middleware, маршрути та публікація конфігів.

class AppServiceProvider extends ServiceProvider
{
    // лише біндинги в контейнер
    public function register(): void
    {
        $this->app->singleton(Parser::class);
    }

    // виконується після реєстрації всіх провайдерів
    public function boot(): void
    {
        Gate::define('admin', fn ($u) => $u->is_admin);
    }
}

Правило: у register() - лише біндинги (інші сервіси можуть бути ще не зареєстровані); у boot() - усе інше.

Докладніше в документації: Service Providers

Провайдери завантажуються у дві фази, і плутанина між ними дає помилки, які важко пояснити.

register() - тільки привʼязки до контейнера. На цей момент інші провайдери ще не зареєстровані, тож звертатися до чужих сервісів не можна:

public function register(): void
{
    $this->app->singleton(WeatherClient::class, function ($app) {
        return new WeatherClient(config('services.weather.key'));
    });
}

Замикання виконається пізніше - у момент першого резолву, коли все вже піднято.

boot() - усе інше. Викликається після того, як усі провайдери відпрацювали register(), тож тут доступні будь-які сервіси:

public function boot(): void
{
    Model::preventLazyLoading(! app()->isProduction());

    View::composer('layouts.app', SidebarComposer::class);

    Gate::define('publish', fn (User $user) => $user->is_editor);
}

Типова помилка - зробити в register() щось на кшталт:

public function register(): void
{
    // Провайдер конфігурації міг ще не відпрацювати.
    $key = config('services.weather.key');
    $this->app->singleton(WeatherClient::class, fn () => new WeatherClient($key));
}

Значення читається негайно, і якщо потрібний провайдер ще не завантажився, отримаєте null без жодної помилки. Всередині замикання те саме читання безпечне.

Правило: register() - сказати контейнеру, як створювати; boot() - зробити щось із уже готовим застосунком.

Докладніше в документації: Сервіс-провайдери

AppServiceProvider - зручне місце за замовчуванням, і для невеликого застосунку його цілком досить. Проблема починається, коли він розростається до сотень рядків змішаних налаштувань.

Ознаки, що час розділяти:

  • у boot() упереміш макроси, правила Model::shouldBeStrict(), RateLimiter::for(), Gate::before(), Event::listen() і прив'язки інтеграцій;
  • щоб змінити одну інтеграцію, доводиться гортати весь файл;
  • різні частини належать різним модулям чи командам.

Як розділяють: за предметною областю, а не за типом коду.

// bootstrap/providers.php
return [
    App\Providers\AppServiceProvider::class,       // загальні налаштування фреймворку
    App\Providers\BillingServiceProvider::class,   // усе про оплату: шлюз, вебхуки, лімітери
    App\Providers\SearchServiceProvider::class,    // клієнт пошуку, індексатори
];

Провайдер модуля тримає прив'язки, події й політики свого модуля - тоді модуль можна зрозуміти, відкривши один файл.

Чого уникати:

  • провайдер на кожен рядок - десяток порожніх класів гірший за один охайний файл;
  • важкої роботи в boot() - він виконується на кожен запит, тож запит до бази чи HTTP-виклик там гальмує все;
  • залежності від порядку провайдерів у register(): там можна лише реєструвати, а користуватися сервісами - у boot().

Провайдер, що лише реєструє прив'язки, можна зробити відкладеним (DeferrableProvider).

Докладніше в документації: Написання сервіс-провайдерів

Через властивості $bindings і $singletons замість викликів у register().

class AppServiceProvider extends ServiceProvider
{
    public array $bindings = [
        ServerProvider::class => DigitalOceanServerProvider::class,
    ];

    public array $singletons = [
        DowntimeNotifier::class => PingdomDowntimeNotifier::class,
        ServerToolsProvider::class => ServerToolsProvider::class,
    ];
}

Фреймворк сам пройде ці масиви, коли завантажуватиме провайдер, і зареєструє кожну пару «абстракція - реалізація».

Коли цього досить: прив'язка інтерфейсу до класу, який контейнер може створити сам.

Коли потрібен register(): якщо об'єкт треба створити по-особливому - з параметрами з конфігурації, з умовою, з замиканням:

public function register(): void
{
    $this->app->singleton(PaymentGateway::class, fn () => new StripeGateway(
        config('services.stripe.secret'),
    ));
}

Ще одна зручність: метод boot() теж підтримує впровадження залежностей - потрібні сервіси оголошують його параметрами, а не дістають через $this->app->make().

public function boot(ResponseFactory $response): void
{
    $response->macro('caps', fn (string $value) => $response->make(strtoupper($value)));
}

Докладніше в документації: Властивості bindings і singletons

Кожен провайдер завантажується на кожному запиті. Якщо він лише реєструє сервіс, потрібний в одному місці з сотні, це марна робота на всіх інших запитах.

Відкладений провайдер реєструється лише тоді, коли його сервіс справді резолвлять:

use Illuminate\Contracts\Support\DeferrableProvider;

class WeatherServiceProvider extends ServiceProvider implements DeferrableProvider
{
    public function register(): void
    {
        $this->app->singleton(WeatherClient::class, function ($app) {
            return new WeatherClient(config('services.weather.key'));
        });
    }

    /**
     * @return array<int, class-string>
     */
    public function provides(): array
    {
        return [WeatherClient::class];
    }
}

Laravel запамʼятовує зіставлення «сервіс → провайдер» у маніфесті й піднімає провайдер лише за потреби.

Умови, за яких це працює:

  • Провайдер має лише register(). Якщо є boot(), слухачі подій, маршрути чи публікація ресурсів - вони не виконаються, поки сервіс ніхто не запросить, тобто фактично ніколи.
  • provides() мусить перелічувати всі привʼязки. Забута привʼязка просто не знайдеться в контейнері.

Коли не варто: якщо провайдер реєструє щось, що потрібне майже завжди, відкладення дає нуль виграшу й додає місце для помилки.

Скільки це коштує. Виграш помітний на застосунку з десятками провайдерів; на невеликому - в межах похибки. Перед тим як відкладати, варто виміряти профайлером, а не робити це «про всяк випадок».

Докладніше в документації: Сервіс-провайдери

Через package discovery: пакет оголошує провайдер і фасади в секції extra.laravel свого composer.json.

"extra": {
    "laravel": {
        "providers": [
            "Acme\\Billing\\BillingServiceProvider"
        ],
        "aliases": {
            "Billing": "Acme\\Billing\\Facades\\Billing"
        }
    }
}

Після composer install чи update Laravel виконує package:discover, збирає ці оголошення з усіх пакетів і кешує список. Провайдер реєструється автоматично - у bootstrap/providers.php нічого дописувати не треба.

Як застосунку відмовитися від автопідключення - наприклад, щоб зареєструвати провайдер умовно лише локально:

"extra": {
    "laravel": {
        "dont-discover": ["barryvdh/laravel-debugbar"]
    }
}

"*" у dont-discover вимикає виявлення для всіх пакетів.

Що варто знати авторові пакета:

  • провайдер пакета має бути легким і бажано відкладеним, якщо лише реєструє прив'язки - він завантажується в кожному застосунку, що встановив пакет;
  • конфігурацію, міграції й шаблони пакет пропонує опублікувати (publishes()), а не копіює сам;
  • mergeConfigFrom() у register() дає значення за замовчуванням, які застосунок може перекрити своїм файлом.

Якщо після встановлення пакета щось «не підхопилося», перевіряють кеш: php artisan package:discover і optimize:clear.

Докладніше в документації: Виявлення пакетів