Питання на співбесіді: Service Providers
Найпопулярніші питання з реальних Laravel/PHP співбесід для всіх рівнів
3 питання
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() - зробити щось із уже готовим застосунком.
Кожен провайдер завантажується на кожному запиті. Якщо він лише реєструє сервіс, потрібний в одному місці з сотні, це марна робота на всіх інших запитах.
Відкладений провайдер реєструється лише тоді, коли його сервіс справді резолвлять:
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()мусить перелічувати всі привʼязки. Забута привʼязка просто не знайдеться в контейнері.
Коли не варто: якщо провайдер реєструє щось, що потрібне майже завжди, відкладення дає нуль виграшу й додає місце для помилки.
Скільки це коштує. Виграш помітний на застосунку з десятками провайдерів; на невеликому - в межах похибки. Перед тим як відкладати, варто виміряти профайлером, а не робити це «про всяк випадок».