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-CookieCDN зазвичай не кешує, тож 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) - для посилань, які не можна підробити: відписка, підтвердження пошти.