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

Middle: питання на співбесіді з теми «Відповіді й URL»

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

3 питання

Додати cookie до відповіді:

return response('Hello')->cookie('theme', 'dark', minutes: 60 * 24 * 365);

return response('Hello')->withoutCookie('theme');   // видалити

Черга cookie - коли відповіді ще немає (сервіс, middleware, слухач події):

use Illuminate\Support\Facades\Cookie;

Cookie::queue('last_seen_post', $post->id, minutes: 60);
Cookie::expire('promo_banner');

Laravel прикріпить cookie з черги до відповіді, яка буде відправлена.

Читання:

$theme = $request->cookie('theme');

Шифрування за замовчуванням. Middleware EncryptCookies шифрує й підписує всі cookie, які створює Laravel, ключем APP_KEY. Наслідки:

  • клієнт не може прочитати чи підробити значення - змінене cookie просто не розшифрується, і $request->cookie() поверне null;
  • JavaScript бачить зашифрований рядок, а не значення;
  • cookie, встановлене не Laravel (з JavaScript чи іншим сервісом), Laravel спробує розшифрувати й отримає null.

Виключення з шифрування - для cookie, які має читати фронтенд чи інший сервіс:

// bootstrap/app.php
->withMiddleware(function (Middleware $middleware): void {
    $middleware->encryptCookies(except: [
        'theme',
        'consent',
    ]);
})

Незашифровані cookie не можна вважати надійними: користувач змінює їх як завгодно, тож у них - лише налаштування інтерфейсу, ніколи не права чи ідентифікатори.

Атрибути безпеки (за замовчуванням з config/session.php): secure, httpOnly, sameSite. Для власних cookie їх можна передати в cookie():

cookie('theme', 'dark', 525600, path: '/', domain: null, secure: true, httpOnly: false, sameSite: 'lax');

Що пам'ятати:

  • зміна APP_KEY робить усі зашифровані cookie, включно з сесією, недійсними - користувачів розлогінить (допомагає APP_PREVIOUS_KEYS);
  • розмір: браузер обмежує cookie приблизно 4 КБ, а шифрування збільшує значення - великі дані в cookie не кладуть;
  • кешування сторінок: відповідь з Set-Cookie CDN зазвичай не кешує, тож cookie на кожній сторінці ламає кеш на краю мережі.

Докладніше в документації: Відповіді: cookie у відповідях

Звичайна відповідь формується повністю, а потім відправляється. Потокова - відправляється частинами в міру готовності: користувач бачить перші дані раніше, а сервер не тримає всю відповідь у пам'яті.

stream - довільні дані частинами:

return response()->stream(function (): void {
    foreach ($this->generator->chunks() as $chunk) {
        echo $chunk;
        ob_flush();
        flush();
    }
}, 200, ['X-Accel-Buffering' => 'no']);

Простіше - передати замикання-генератор: Laravel сам відправлятиме кожне yield і скидатиме буфер:

Route::get('/answer', function (LlmClient $llm) {
    return response()->stream(function () use ($llm) {
        foreach ($llm->stream(request('prompt')) as $token) {
            yield $token;
        }
    });
});

streamJson - великий JSON, де частина даних - лінива колекція:

return response()->streamJson([
    'users' => User::query()->cursor(),
]);

Рядки читаються з бази й кодуються в JSON по одному - пам'ять не залежить від кількості записів.

eventStream - Server-Sent Events (text/event-stream):

return response()->eventStream(function () {
    while ($progress = $this->import->progress()) {
        yield new StreamedEvent(event: 'progress', data: ['percent' => $progress]);
        sleep(1);
    }
});

Браузер слухає через EventSource, а наприкінці Laravel надсилає завершальне повідомлення </stream>. Для React і Vue є готові хуки useStream і useEventStream з пакетів @laravel/stream-react / @laravel/stream-vue.

Що ламає потокову передачу:

  • буферизація на шляху: Nginx за замовчуванням буферизує відповідь від PHP-FPM - заголовок X-Accel-Buffering: no чи налаштування proxy_buffering off; так само CDN і стиснення gzip, що чекає на заповнення буфера;
  • тайм-аути: max_execution_time, тайм-аути проксі й балансувальника обривають довге з'єднання;
  • воркер зайнятий: кожен відкритий потік тримає процес PHP-FPM. Сотня користувачів, що слухають SSE, - сотня зайнятих воркерів. Для масових підписок на події краще WebSocket через Reverb;
  • сесія: блокування сесії (->block()) чи сесія, відкрита на весь час потоку, затримує інші запити того самого користувача;
  • помилка посеред потоку: статус 200 уже відправлено - про помилку доведеться повідомити в самих даних.

Коли доречно: відповіді LLM по токенах, експорт великих даних, прогрес довгої операції.

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

Основні хелпери:

url('/posts/42');                              // абсолютний URL з APP_URL чи поточного хоста
route('posts.show', $post);                    // за іменем маршруту - рекомендований спосіб
route('posts.show', ['post' => $post, 'tab' => 'comments']);  // зайві параметри стають query
route('posts.index', absolute: false);         // відносний шлях /posts
action([PostController::class, 'show'], $post);
secure_url('/checkout');                       // примусово https

Чому route(), а не рядки: змінили URL у файлі маршрутів - усі посилання оновилися. Помилка в імені маршруту - виняток одразу, а не тихе посилання в нікуди.

Модель як параметр: Laravel підставляє ключ маршруту моделі (getRouteKey()), тож якщо маршрут використовує {post:slug} чи модель перевизначає getRouteKeyName(), у URL буде slug.

Поточна й попередня адреса:

url()->current();      // без query
url()->full();         // з query
url()->previous();     // з заголовка Referer чи сесії
url()->previousPath();
request()->routeIs('posts.*');   // для активних пунктів меню

Змінити параметри поточної адреси (наприклад, сторінку чи сортування) зручніше не склеюванням рядків, а через $request->fullUrlWithQuery(['page' => 3]) чи об'єкт Uri.

Типові проблеми:

  • неправильний домен чи http замість https за балансувальником - не налаштовані довірені проксі, і Laravel не знає, що запит прийшов по HTTPS;
  • URL у листах і завданнях черги будуються з APP_URL, бо поточного запиту немає. Неправильний APP_URL - посилання на localhost у листах;
  • url()->previous() для логіки безпеки - заголовок Referer контролює клієнт;
  • підписані URL (URL::signedRoute) - для посилань, які не можна підробити: відписка, підтвердження пошти.

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