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

Senior: питання на співбесіді з теми «Сервіс-контейнер»

Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.

3 питання

Contextual binding дозволяє віддавати різні реалізації одного інтерфейсу залежно від класу, що його запитує.

$this->app->when(PhotoController::class)
    ->needs(Filesystem::class)
    ->give(fn () => Storage::disk('local'));

$this->app->when(VideoController::class)
    ->needs(Filesystem::class)
    ->give(fn () => Storage::disk('s3'));

Тобто PhotoController отримає локальний диск, VideoController - S3, хоча обидва просять Filesystem.

Споріднені можливості:

  • giveTagged() - впорснути всі сервіси з певним тегом.
  • Прив'язка примітивів: ->needs('$apiKey')->give(config('services.x.key')).

Корисно, коли одна абстракція має кілька конфігурацій у різних частинах застосунку.

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

У класичному PHP-FPM застосунок народжується й помирає з кожним запитом, тож singleton() фактично означає «один на запит». Під Octane чи у воркері черги процес живе годинами - і різниця стає критичною.

  • singleton() - один екземпляр на весь процес. Під Octane він переживає запит і обслуговує наступних користувачів.
  • scoped() - один екземпляр на запит чи завдання. Контейнер скидає його на початку кожного.
$this->app->singleton(ExchangeRates::class);   // незмінні дані - безпечно
$this->app->scoped(CurrentCart::class);        // стан користувача - лише scoped

Класична помилка:

$this->app->singleton(CartService::class, fn ($app) => new CartService($app['request']->user()));

Під Octane перший користувач «приклеїться» до сервісу, і наступні побачать його кошик.

Правила:

  • синглтон не тримає стану запиту: користувача, Request, локаль, налаштування з сесії;
  • якщо сервісу потрібен запит - scoped() або передавати запит у метод, а не в конструктор;
  • статичні властивості й кеш у пам'яті класу так само переживають запит.

Те саме стосується воркерів черги: завдання N бачить синглтони, створені під час завдання N-1.

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

Методом extend() контейнера - він отримує вже створений сервіс і повертає те, що віддаватиметься всім споживачам.

// AppServiceProvider::register()
$this->app->extend(GeocoderInterface::class, function (GeocoderInterface $geocoder, Application $app) {
    return new CachedGeocoder($geocoder, $app->make('cache.store'));
});
final class CachedGeocoder implements GeocoderInterface
{
    public function __construct(private GeocoderInterface $inner, private Repository $cache) {}

    public function locate(string $address): Coordinates
    {
        return $this->cache->remember('geo:' . md5($address), 86400, fn () => $this->inner->locate($address));
    }
}

Це патерн «декоратор»: той самий інтерфейс, а всередині - оригінал плюс нова поведінка. Код, що просить GeocoderInterface, нічого не помічає.

Чому це краще за альтернативи:

  • не треба наслідувати клас пакета й підміняти прив'язку - пакет може змінити конструктор;
  • декоратори нашаровуються: кеш поверх журналу поверх повторів, кожен простий і тестується окремо.

Суміжні інструменти:

  • resolving() - викликати код щоразу, коли сервіс створюється (налаштувати, а не замінити);
  • tag() і tagged() - зібрати всі реалізації інтерфейсу, щоб передати їх масивом.

Докладніше в документації: Розширення прив'язок