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

Senior: питання на співбесіді з теми «Екосистема Laravel»

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

3 питання

Причина - збережені значення. Драйвер database при першій перевірці обчислює прапорець для скопу і записує результат у таблицю features. Далі Pennant читає збережене значення, а замикання з визначенням не викликається.

Сценарій: прапорець new-checkout визначено як Lottery::odds(1 / 10). Через тиждень команда змінює його на true для всіх - але 90% користувачів, яким раніше випало false, так і бачать стару версію.

Як оновити вже збережені значення:

Feature::activateForEveryone('new-checkout');   // усім true
Feature::deactivateForEveryone('new-checkout'); // усім false
Feature::purge('new-checkout');                 // видалити збережене - наступна перевірка обчислить заново
php artisan pennant:purge new-checkout

Продуктивність:

  • кеш у пам'яті: у межах запиту значення кешується, тож повторні перевірки не йдуть у базу;
  • цикли: перевірка прапорця для кожного з сотні користувачів - сотня запитів. Рішення - жадібне завантаження:
Feature::for($users)->load(['notifications-beta']);
  • драйвер array не зберігає нічого між запитами - для тестів (PENNANT_STORE=array) або для прапорців, які завжди обчислюються з даних;
  • власний драйвер - якщо прапорці керуються зовнішнім сервісом (LaunchDarkly, Unleash) чи конфігурацією.

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

Життєвий цикл прапорця:

  1. створення з власником і очікуваною датою видалення;
  2. поступовий запуск і моніторинг помилок та метрик;
  3. повний запуск - activateForEveryone;
  4. прибирання: видалити перевірки й стару гілку коду, визначення прапорця, purge збережених значень.

Чому прибирання - найважливіше: кожен активний прапорець подвоює кількість шляхів виконання. Десять прапорців - потенційно тисяча комбінацій, які ніхто не тестує. Корисні практики: список прапорців з датами в README чи задачах, нагадування в CI для прапорців, старших за N тижнів, тести для обох станів кожного прапорця.

Не плутати з конфігурацією: прапорці - тимчасовий механізм для запуску. Постійні налаштування клієнтів (тарифні можливості) краще моделювати явно - через план підписки чи налаштування тенанта.

Докладніше в документації: Pennant: очищення можливостей

Cashier - обгортка над Stripe (і окремо над Paddle) для підписок: клієнти, способи оплати, пробні періоди, зміна тарифу, рахунки.

// оформлення через Stripe Checkout
return $request->user()
    ->newSubscription('default', 'price_monthly_pro')
    ->trialDays(14)
    ->checkout(['success_url' => route('billing.done'), 'cancel_url' => route('billing')]);

// перевірка доступу
if ($user->subscribed('default')) { /* ... */ }
$user->subscription('default')->swap('price_yearly_pro');
$user->subscription('default')->cancel(); // до кінця оплаченого періоду

Чому вебхуки критичні. Оплата - асинхронний процес, що відбувається поза вашим застосунком:

  • автоматичне продовження підписки щомісяця;
  • невдале списання, повторні спроби, скасування після кількох невдач;
  • 3-D Secure (SCA): користувач підтверджує платіж у банку пізніше;
  • повернення коштів, суперечки, зміни в панелі Stripe вручну.

Про все це застосунок дізнається лише через вебхуки. Cashier реєструє маршрут /stripe/webhook і сам оновлює таблиці subscriptions і subscription_items за подіями customer.subscription.*, invoice.payment_succeeded тощо. Без налаштованих вебхуків стан у базі розходиться з реальністю: користувач перестав платити, а доступ лишився.

php artisan cashier:webhook   # створює вебхук у Stripe з потрібними подіями

Обов'язкове для продакшену:

  • перевірка підпису - STRIPE_WEBHOOK_SECRET; без неї будь-хто може надіслати «оплату пройшла»;
  • виключення з CSRF для маршруту вебхука;
  • ідемпотентність власних обробників: Stripe може надіслати подію повторно, а порядок подій не гарантований. Обробник має давати той самий результат при повторі;
  • черга для важкої роботи: відповісти Stripe швидко, а лист чи нарахування - у фонове завдання, інакше тайм-аут і повторна доставка;
  • стани incomplete і past_due: підписка створена, але платіж не підтверджено. Вирішіть, чи давати доступ у цих станах, і перевіряйте їх явно.

Stripe чи Paddle: зі Stripe ви - продавець і самі відповідаєте за податки (Stripe Tax допомагає). Paddle - «merchant of record»: він продає від свого імені й сам сплачує ПДВ у різних країнах, що спрощує продаж по світу, але бере більшу комісію.

Тестування: тестові ключі, Stripe CLI (stripe listen --forward-to) для локальних вебхуків, тести обробників на збережених прикладах подій.

Докладніше в документації: Cashier: обробка вебхуків Stripe

Pulse записує події з кожного запиту, завдання, SQL-запиту й звернення до кешу. На застосунку з тисячами запитів за хвилину це означає мільйони записів, які треба зберегти й агрегувати для панелі. Без налаштувань моніторинг починає навантажувати ту саму базу, яку мав би спостерігати.

1. Окрема база даних для Pulse:

PULSE_DB_CONNECTION=pulse

Записи й важкі агрегуючі запити панелі не конкурують з основними запитами застосунку.

2. Приймання через Redis:

PULSE_INGEST_DRIVER=redis
PULSE_REDIS_CONNECTION=pulse

За замовчуванням Pulse пише в базу після відправлення відповіді - запит користувача не чекає, але воркер PHP зайнятий. З драйвером redis записи йдуть у Redis-стрім, а окремий процес php artisan pulse:work переносить їх у базу пакетами. Підключення Redis для Pulse має бути іншим, ніж для черги.

3. Семплювання:

Recorders\UserRequests::class => [
    'sample_rate' => env('PULSE_USER_REQUESTS_SAMPLE_RATE', 0.1),
],

Записується лише ~10% подій, значення на панелі масштабуються й позначаються ~. Чим частіша подія, тим нижчу частоту можна ставити без втрати точності. Рідкісні події (винятки, повільні запити) семплювати не варто.

4. Фільтрація й групування:

  • ігнорувати шумні маршрути (/up, /livewire/*, /horizon/*) і ключі кешу;
  • групувати ключі кешу й URL з ідентифікаторами регулярними виразами - інакше user:1, user:2... стають мільйоном окремих рядків;
  • пороги «повільного» під свій застосунок (threshold), можна окремо для конкретних маршрутів.

5. Обрізання даних - Pulse видаляє записи поза вікном панелі за лотереєю під час приймання.

6. Процеси, що треба запускати й перезапускати:

  • pulse:check на кожному сервері для картки серверів;
  • pulse:work при прийманні через Redis;
  • pulse:restart під час деплою - обидві команди довгоживучі й не бачать нового коду.

7. Збої самого Pulse: якщо сховище Pulse недоступне, помилка не має ламати застосунок. Pulse за замовчуванням логує свої винятки, а обробник можна перевизначити через Pulse::handleExceptionsUsing().

Доступ: гейт viewPulse - панель показує email користувачів, SQL-запити й URL, це чутливі дані.

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