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

Питання на співбесіді: Екосистема Laravel

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

9 питань

Прапорець функції (feature flag) - перемикач, який вмикає чи вимикає частину функціональності без деплою. Код нової можливості вже на продакшені, але бачать її лише ті, кому прапорець увімкнено.

Навіщо це потрібно:

  • поступовий запуск: спершу 5% користувачів, потім 50%, потім усі;
  • trunk-based розробка: незавершену функцію можна зливати в main, бо вона прихована прапорцем;
  • швидкий «вимикач»: якщо нова функція ламає продакшен, її вимикають без відкату коду;
  • A/B-тести й бета-доступ для окремих клієнтів.

Laravel Pennant - офіційний пакет для прапорців. Прапорець оголошують у сервіс-провайдері:

use App\Models\User;
use Illuminate\Support\Lottery;
use Laravel\Pennant\Feature;

Feature::define('new-checkout', fn (User $user) => match (true) {
    $user->isInternal() => true,
    $user->isHighTrafficCustomer() => false,
    default => Lottery::odds(1 / 10),
});

Замикання отримує скоп - за замовчуванням автентифікованого користувача - і повертає результат.

Перевірка в коді й шаблонах:

if (Feature::active('new-checkout')) {
    return $this->newCheckout($request);
}
@feature('new-checkout')
    <x-checkout.new />
@else
    <x-checkout.legacy />
@endfeature

Для маршрутів є middleware EnsureFeaturesAreActive, що повертає помилку, якщо прапорець вимкнено.

Важлива деталь: драйвер за замовчуванням - database. Pennant зберігає обчислене значення в таблиці features, тож користувач, якому одного разу «випало» true в лотереї, і далі бачитиме нову версію. Це робить досвід стабільним, але означає, що зміна визначення не впливає на вже збережені значення.

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

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

Socialite реалізує протокол OAuth для входу через сторонніх провайдерів: Google, GitHub, Facebook, GitLab, LinkedIn, Slack, X та інших.

Як проходить вхід:

  1. користувач натискає «Увійти через GitHub» - застосунок перенаправляє його на GitHub;
  2. GitHub питає користувача, чи дозволити доступ;
  3. GitHub повертає користувача на ваш callback-маршрут з одноразовим кодом;
  4. Socialite обмінює код на токен доступу й отримує профіль користувача;
  5. застосунок знаходить чи створює користувача і входить від його імені.

Налаштування - ключі застосунку провайдера в config/services.php:

'github' => [
    'client_id' => env('GITHUB_CLIENT_ID'),
    'client_secret' => env('GITHUB_CLIENT_SECRET'),
    'redirect' => '/auth/github/callback',
],

Два маршрути:

use Laravel\Socialite\Socialite;

Route::get('/auth/github/redirect', fn () => Socialite::driver('github')->redirect());

Route::get('/auth/github/callback', function () {
    $githubUser = Socialite::driver('github')->user();

    $user = User::updateOrCreate(
        ['github_id' => $githubUser->getId()],
        ['name' => $githubUser->getName(), 'email' => $githubUser->getEmail()],
    );

    Auth::login($user, remember: true);

    return redirect('/dashboard');
});

Що варто знати:

  • шукати користувача за ID провайдера (github_id), а не за email: email у профілі можна змінити, а ID стабільний;
  • email може бути відсутнім - деякі провайдери не повертають його без додаткового скопу або якщо користувач приховав пошту;
  • скопи визначають, до чого застосунок отримає доступ: ->scopes(['read:user']). Просіть мінімум;
  • параметр state Socialite перевіряє сам - це захист від CSRF у процесі OAuth. Для API без сесії є stateless(), але тоді захист доведеться забезпечити інакше;
  • тести: Socialite можна підмінити фейком і не ходити до справжнього провайдера.

Для кількох провайдерів зручно зберігати зв'язки в окремій таблиці social_accounts (provider, provider_id, user_id), а не додавати колонку в users для кожного провайдера.

Докладніше в документації: Socialite: автентифікація та збереження

Pulse - панель моніторингу продуктивності й використання застосунку. Вона показує агреговану картину за період: що повільне, що найчастіше використовується, де помилки.

Що показує Pulse (картки):

Картка Що видно
Сервери CPU, пам'ять, диск кожного сервера
Використання застосунку користувачі, що роблять найбільше запитів чи запускають найбільше завдань
Винятки найчастіші винятки й коли вони були востаннє
Черги пропускна здатність: поставлено, оброблено, невдалі
Повільні запити, завдання, SQL, вихідні HTTP що перевищує поріг (за замовчуванням 1000 мс)
Кеш влучання й промахи за ключами

Панель доступна за адресою /pulse, за замовчуванням лише в середовищі local. Для продакшену доступ відкривають через гейт viewPulse:

Gate::define('viewPulse', fn (User $user) => $user->isAdmin());

Чим відрізняється Telescope:

Pulse Telescope
що записує агреговані метрики кожен запит, запит до БД, завдання, лист окремо
питання, на яке відповідає «що загалом повільне й часто ламається?» «що саме сталося в цьому запиті?»
де використовувати продакшен переважно розробка й staging
навантаження помірне, з семплюванням значне на продакшені

Простіше кажучи: Pulse показує, де проблема (ендпойнт /reports повільний і викликається тисячу разів на годину), а Telescope - чому (у кожному запиті 300 SQL-запитів через N+1).

Чого Pulse не замінює:

  • зовнішній моніторинг доступності - якщо застосунок лежить, Pulse разом з ним;
  • трекер помилок (Sentry, Flare) з повними стеками й сповіщеннями;
  • централізовані логи для розслідувань.

Для серверних метрик на кожному сервері має працювати команда pulse:check під Supervisor.

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

Скоп - сутність, для якої обчислюється прапорець. За замовчуванням це автентифікований користувач, але скопом може бути будь-що: команда, організація, тенант.

Перевірка для конкретного скопу:

Feature::for($user->team)->active('billing-v2');

Визначення з урахуванням скопу:

use App\Models\Team;

Feature::define('billing-v2', function (Team $team) {
    if ($team->created_at->isAfter('2026-01-01')) {
        return true; // нові команди одразу на новому білінгу
    }

    return Lottery::odds(1 / 100); // старі - поступово
});

Чому це важливо: якщо функція змінює спільні дані команди (білінг, налаштування), прапорець на рівні користувача дасть різну поведінку для колег з однієї команди. Скоп має відповідати межі, в якій функція має бути узгодженою.

Скоп за замовчуванням можна змінити, щоб не писати for() щоразу:

Feature::resolveScopeUsing(fn ($driver) => Auth::user()?->team);

Багатші значення - прапорець може повертати не лише булеве значення:

Feature::define('purchase-button', fn (User $user) => Arr::random([
    'blue-sapphire',
    'seafoam-green',
    'tart-orange',
]));

$color = Feature::value('purchase-button');
@feature('purchase-button', 'seafoam-green')
    <!-- варіант B -->
@endfeature

Так роблять A/B-тести: кожен користувач стабільно отримує свій варіант (значення зберігається), а аналітика порівнює конверсію.

Керування вручну:

Feature::for($team)->activate('billing-v2');
Feature::deactivate('billing-v2');          // для поточного скопу
Feature::activateForEveryone('billing-v2'); // оновити всі збережені значення

Гостьовий скоп: якщо користувач не автентифікований, скоп - null. Визначення має приймати nullable-тип (?User $user), інакше Pennant поверне false, не викликаючи замикання.

Докладніше в документації: Pennant: скоп

Проблема: щоб показувати помилки під полем одразу при введенні, правила валідації часто дублюють на фронтенді (Zod, Yup, VeeValidate). Тепер є два набори правил, які розходяться: unique:users,email на фронтенді взагалі не перевірити.

Precognition дозволяє фронтенду «запитати» сервер: «чи пройшли б ці дані валідацію?» - без виконання самої дії.

Як це працює:

  1. фронтенд надсилає запит на той самий маршрут зі спеціальним заголовком Precognition: true;
  2. middleware HandlePrecognitiveRequests запускає лише middleware маршруту й валідацію (Form Request), але не викликає контролер;
  3. відповідь - 204 (усе гаразд) або 422 з помилками;
  4. фронтенд показує помилки під полями.

Бекенд:

use Illuminate\Foundation\Http\Middleware\HandlePrecognitiveRequests;

Route::post('/users', [UserController::class, 'store'])
    ->middleware([HandlePrecognitiveRequests::class]);

Правила лишаються в одному місці - у StoreUserRequest.

Фронтенд (Vue, React чи Alpine):

<script setup>
import { useForm } from 'laravel-precognition-vue';

const form = useForm('post', '/users', { name: '', email: '' });
</script>

<template>
    <input v-model="form.email" @change="form.validate('email')" />
    <div v-if="form.invalid('email')">{{ form.errors.email }}</div>
    <button :disabled="form.processing" @click="form.submit()">Зберегти</button>
</template>

validate('email') перевіряє лише це поле - помилки інших, ще не заповнених полів не показуються.

На що зважати:

  • побічні ефекти в middleware: прекогнітивний запит проходить через middleware маршруту. Middleware, що рахує взаємодії чи пише аудит, має пропускати такі запити: $request->isPrecognitive();
  • авторизація в authorize() Form Request теж виконується - це правильно, але враховуйте вартість;
  • навантаження: кожна зміна поля - запит до сервера. Для дорогих правил (зовнішні API) варто подумати, чи потрібна жива перевірка;
  • файли: за замовчуванням файли під час прекогнітивної валідації не надсилаються, щоб не завантажувати їх повторно.

Inertia 2.3+ має вбудовану підтримку Precognition у формах, тож окремий пакет там не потрібен.

Докладніше в документації: Precognition: жива валідація

Laravel Prompts - бібліотека гарних інтерактивних полів у терміналі: текст із плейсхолдером і валідацією, вибір зі списку, пошук, мультивибір, підтвердження, прогрес-бар, таблиці. Вона вбудована в Laravel і використовується в його власних командах (make:model, install:api).

Основні промпти:

use function Laravel\Prompts\{text, select, multiselect, confirm, search, spin};

$name = text(
    label: 'Назва проєкту',
    placeholder: 'acme-shop',
    required: true,
    validate: ['name' => 'alpha_dash|max:40'],
);

$plan = select('Тариф', ['free' => 'Free', 'pro' => 'Pro'], default: 'free');

$userId = search(
    label: 'Власник',
    options: fn (string $value) => User::where('email', 'like', "{$value}%")
        ->limit(10)->pluck('email', 'id')->all(),
);

$result = spin(fn () => $this->provision($name), 'Створюю середовище...');

Валідація приймає правила Laravel чи замикання, що повертає текст помилки. Користувач бачить помилку під полем і виправляє введене, а не отримує виняток наприкінці.

Форми - кілька промптів з можливістю повернутися на попередній крок:

$data = form()
    ->text('Ім\'я', required: true, name: 'name')
    ->password('Пароль', validate: ['password' => 'min:8'], name: 'password')
    ->confirm('Надіслати запрошення?', name: 'invite')
    ->submit();

Прогрес для довгих операцій:

progress(label: 'Імпорт', steps: $rows, callback: fn ($row) => $this->import($row));

Що важливо для команд, які запускають і люди, і скрипти:

  • неінтерактивний режим: у CI чи з --no-interaction промпти не можуть питати. Значення мають приходити з аргументів і опцій, а промпт - лише запасний варіант, коли аргумент не передано. Для цього в Laravel є інтерфейс PromptsForMissingInput;
  • Windows без WSL - Prompts автоматично переходить на запасну реалізацію Symfony Console;
  • тести: $this->artisan('project:create')->expectsQuestion('Назва проєкту', 'acme')->assertSuccessful() працює і з Prompts.

Докладніше в документації: Prompts: форми

Причина - збережені значення. Драйвер 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: продуктивність