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

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 і кешування: одна адреса з різним вмістом.

Докладніше в документації: URL: значення за замовчуванням