Middle: питання на співбесіді з теми «Сервіс-контейнер»
Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.
4 питання
Service Container (IoC-контейнер) керує залежностями класів і виконує dependency injection. Він уміє автоматично будувати об'єкти, рекурсивно резолвлячи їхні залежності через type-hints конструктора.
// біндинг інтерфейсу до реалізації (зазвичай у Service Provider)
$this->app->bind(PaymentGateway::class, StripeGateway::class);
// тепер усюди, де просять PaymentGateway, прийде StripeGateway
public function __construct(private PaymentGateway $gateway) {}
bind()- нова реалізація щоразу.singleton()- один екземпляр на весь життєвий цикл запиту.app(PaymentGateway::class)- ручне резолвлення.
Це серце Laravel: контролери, middleware, події - усе резолвиться через контейнер.
Dependency Injection - патерн, за якого клас отримує залежності ззовні (зазвичай через конструктор), а не створює їх сам. Це знижує зв'язування й полегшує тестування (можна підсунути мок).
class OrderController
{
public function __construct(
private PaymentGateway $gateway, // інжектується контейнером
) {}
}
У Laravel DI працює «з коробки»: контейнер читає type-hints і автоматично будує граф залежностей. Інжектити можна й у методи контролера (method injection), зокрема сам Request.
Атрибути переносять налаштування контейнера з провайдера ближче до класу, якого вони стосуються.
#[Bind] на інтерфейсі - яку реалізацію впроваджувати, з урахуванням оточення:
#[Bind(RedisEventPusher::class)]
#[Bind(FakeEventPusher::class, environments: ['local', 'testing'])]
interface EventPusher {}
Замінює $this->app->bind(EventPusher::class, ...) у провайдері.
#[Singleton] і #[Scoped] на класі - скільки екземплярів:
#[Singleton]
class ExchangeRates {}
Singleton - один на весь процес, Scoped - один на запит чи завдання.
Контекстні атрибути на параметрах - що саме впровадити:
public function __construct(
#[Config('app.timezone')] private string $timezone,
#[Storage('s3')] private Filesystem $disk,
#[CurrentUser] private User $user,
) {}
Є також Cache, DB, Log, Auth, Context, Give, RouteParameter, Tag.
Коли що обирати: атрибут зручний, коли правило належить самому класу й читається разом з ним. Провайдер лишається місцем для складної логіки створення, залежної від конфігурації, і для прив'язок до чужих класів, які не можна позначити атрибутом.
Через контекстні атрибути чи контекстну прив'язку - тоді залежність видно в конструкторі, а не всередині методів.
Атрибутами (найкоротше):
class ReportExporter
{
public function __construct(
#[Storage('reports')] private Filesystem $disk,
#[Config('reports.per_page')] private int $perPage,
#[Log('reports')] private LoggerInterface $log,
) {}
}
Контекстною прив'язкою в провайдері - коли атрибут на чужому класі не поставиш:
$this->app->when(ReportExporter::class)
->needs('$perPage')
->giveConfig('reports.per_page');
$this->app->when(ReportExporter::class)
->needs(Filesystem::class)
->give(fn () => Storage::disk('reports'));
Чим це краще за Storage::disk('reports') усередині методу:
- залежності чесно перелічені в конструкторі - видно, від чого клас залежить;
- у тесті їх підставляють напряму, без підміни фасадів;
- різним класам можна дати різні реалізації того самого інтерфейсу.
Власні атрибути створюють, реалізувавши контракт ContextualAttribute з методом resolve().