Junior: питання на співбесіді з теми «Сервіс-контейнер»
Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.
2 питання
Обидва кажуть контейнеру, як створювати сервіс. Різниця - у тому, скільки разів це станеться.
bind() створює новий екземпляр щоразу:
$this->app->bind(ReportBuilder::class, function ($app) {
return new ReportBuilder($app->make(Connection::class));
});
app(ReportBuilder::class) === app(ReportBuilder::class); // false
singleton() створює один раз і повертає той самий обʼєкт до кінця запиту:
$this->app->singleton(WeatherClient::class, function ($app) {
return new WeatherClient(config('services.weather.key'));
});
app(WeatherClient::class) === app(WeatherClient::class); // true
Є ще scoped() - як singleton, але скидається на кожному запиті. Це важливо для Octane, де застосунок живе між запитами й звичайний singleton зберігав би стан довше, ніж треба.
Що обирати. Singleton доречний, коли створення дороге (клієнт із конфігурацією, зʼєднання) або коли стан має бути спільним. bind() - коли обʼєкт накопичує стан і два різних місця не повинні його ділити.
Головна пастка singleton - утримання стану. Сервіс, що накопичує щось усередині, віддасть ці дані наступному, хто його запросить:
class Basket
{
private array $items = [];
public function add(Item $item): void
{
$this->items[] = $item; // у singleton лишиться на весь запит
}
}
Під Octane це вже не «на запит», а між запитами - і дані одного користувача протікають до іншого. Тому в singleton кладуть сервіси без стану, а стан тримають явно.
Контейнер уміє будувати класи сам: читає конструктор через рефлексію й рекурсивно створює залежності.
class InvoiceService
{
public function __construct(private PdfRenderer $pdf, private Mailer $mailer) {}
}
// у контролері достатньо оголосити - контейнер створить усе дерево
public function send(InvoiceService $invoices) { /* ... */ }
Без прив'язки працює, якщо всі залежності - конкретні класи без скалярних параметрів.
Прив'язка потрібна в трьох випадках:
- Інтерфейс. Контейнер не вгадує реалізацію:
$this->app->bind(PaymentGateway::class, StripeGateway::class);
Без цього - виняток Target [PaymentGateway] is not instantiable.
-
Скалярні параметри - ключ API, таймаут. Їх передають контекстною прив'язкою чи атрибутом
#[Config('services.stripe.key')]. -
Особливий спосіб створення або один екземпляр - клієнт з налаштуванням,
singleton()для дорогого об'єкта.
Порада: не реєструйте прив'язку для кожного класу «про всяк випадок». Конкретні класи контейнер створить сам, а зайві рядки в провайдері лише ускладнюють пошук того, що справді налаштовано.