Middle: питання на співбесіді з теми «Сервіс-провайдери»
Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.
4 питання
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() - усе інше.
Провайдери завантажуються у дві фази, і плутанина між ними дає помилки, які важко пояснити.
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