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

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, події - усе резолвиться через контейнер.

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

Dependency Injection - патерн, за якого клас отримує залежності ззовні (зазвичай через конструктор), а не створює їх сам. Це знижує зв'язування й полегшує тестування (можна підсунути мок).

class OrderController
{
    public function __construct(
        private PaymentGateway $gateway, // інжектується контейнером
    ) {}
}

У Laravel DI працює «з коробки»: контейнер читає type-hints і автоматично будує граф залежностей. Інжектити можна й у методи контролера (method injection), зокрема сам Request.

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

Атрибути переносять налаштування контейнера з провайдера ближче до класу, якого вони стосуються.

#[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.

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

Докладніше в документації: Атрибут Bind

Через контекстні атрибути чи контекстну прив'язку - тоді залежність видно в конструкторі, а не всередині методів.

Атрибутами (найкоротше):

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().

Докладніше в документації: Контекстні атрибути