Senior: питання на співбесіді з теми «Відповіді й URL»
Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.
2 питання
Коли однакова логіка формування відповіді повторюється в багатьох контролерах, є два інструменти, щоб винести її в одне місце.
1. Інтерфейс Responsable - об'єкт сам знає, як перетворитися на відповідь:
use Illuminate\Contracts\Support\Responsable;
final class CsvExport implements Responsable
{
public function __construct(
private iterable $rows,
private string $filename,
) {}
public function toResponse($request): StreamedResponse
{
return response()->streamDownload(function () {
$out = fopen('php://output', 'w');
foreach ($this->rows as $row) {
fputcsv($out, $row);
}
fclose($out);
}, $this->filename, ['Content-Type' => 'text/csv']);
}
}
public function export(): CsvExport
{
return new CsvExport(User::query()->cursor()->map->only('id', 'email'), 'users.csv');
}
Контролер повертає намір («ось експорт»), а не деталі HTTP. Так влаштовані API Resources, Uri, Inertia-відповіді.
Responsable з урахуванням формату запиту:
public function toResponse($request)
{
return $request->expectsJson()
? response()->json(['id' => $this->order->id], 201)
: to_route('orders.show', $this->order)->with('status', 'Замовлення створено');
}
Один контролер обслуговує і форму, і API.
2. Макроси відповідей - новий метод на фабриці response():
// AppServiceProvider::boot()
Response::macro('problem', function (string $title, int $status, array $extra = []) {
return Response::json(
['type' => 'about:blank', 'title' => $title, 'status' => $status, ...$extra],
$status,
['Content-Type' => 'application/problem+json'],
);
});
return response()->problem('Недостатньо коштів', 422, ['balance' => $balance]);
Що обрати:
Responsable |
Макрос | |
|---|---|---|
| де живе логіка | у власному класі | у сервіс-провайдері |
| тестування | клас тестується окремо | через HTTP-тест |
| автодоповнення в IDE | повне | потрібні PHPDoc чи ide-helper |
| для чого | складні відповіді з даними | короткі допоміжні методи формату |
Ризики:
- макроси глобальні й неявні: новий розробник не знайде, де визначено
response()->problem(), а статичний аналіз їх не бачить без додаткових анотацій; - конфлікт імен з майбутніми методами фреймворку чи пакетів;
- Responsable не повинен робити побічних дій (запис у базу, відправку листів) - він має лише формувати відповідь; інакше middleware чи тести, що створюють відповідь, викличуть побічні ефекти.
Альтернатива для помилок: власний виняток з методом render() часто кращий за макрос - його можна кинути з будь-якого рівня, а не лише повернути з контролера.
Задача: усі маршрути мають префікс {locale} чи {team}:
Route::prefix('{locale}')->group(function () {
Route::get('/posts', [PostController::class, 'index'])->name('posts.index');
Route::get('/posts/{post}', [PostController::class, 'show'])->name('posts.show');
});
Тепер кожен виклик route() вимагає locale: route('posts.show', ['locale' => app()->getLocale(), 'post' => $post]). Забули - виняток Missing required parameter. Посилань сотні.
URL::defaults - значення параметра за замовчуванням на весь запит:
final class SetUrlDefaults
{
public function handle(Request $request, Closure $next): Response
{
URL::defaults(['locale' => $request->route('locale') ?? config('app.locale')]);
return $next($request);
}
}
Далі просто route('posts.show', $post) - locale підставиться сам.
Пастка з прив'язкою моделей. Якщо маршрут використовує неявну прив'язку моделей ({post}), middleware SubstituteBindings будує й перевіряє параметри маршруту. Якщо ваш middleware зі значеннями за замовчуванням виконується після нього, прив'язка спрацює раніше, ніж з'являться значення, - і ви отримаєте помилки на кшталт неправильних параметрів. Тому middleware ставлять у пріоритеті перед SubstituteBindings:
->withMiddleware(function (Middleware $middleware): void {
$middleware->prependToPriorityList(
before: \Illuminate\Routing\Middleware\SubstituteBindings::class,
prepend: \App\Http\Middleware\SetUrlDefaults::class,
);
})
Де ще потрібні значення за замовчуванням:
- черги, команди, листи - там немає поточного запиту й middleware не виконується. Завдання, яке генерує посилання для листа, має встановити
URL::defaultsсаме (наприклад, з локалі отримувача) - інакше зновуMissing required parameter; - Octane: значення за замовчуванням зберігаються в генераторі URL між запитами в тому самому воркері, тож middleware має встановлювати їх на кожен запит, а не лише коли значення змінилося;
- тести: у тестах генерації посилань поза HTTP-запитом значення треба задати явно.
Альтернативи:
- субдомени (
{team}.app.com) - те саме з параметром домену вRoute::domain('{team}.example.com'); - локаль без префікса URL (з налаштувань користувача чи заголовка) - простіше для маршрутів, але гірше для SEO і кешування: одна адреса з різним вмістом.