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

Middle: питання на співбесіді з теми «Хелпери й рядки»

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

3 питання

Ці функції закривають типові шаблони коду, які інакше пишуть вручну з try, if і тимчасовими змінними.

retry - повторити операцію, що може тимчасово впасти:

$rate = retry(3, fn () => $client->fetchRate('USD'), 200);

// своя затримка для кожної спроби
retry([100, 500, 2000], fn () => $client->fetchRate('USD'));

// повторювати лише для певних помилок
retry(3, $callback, 200, fn (Throwable $e) => $e instanceof ConnectionException);

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

rescue - виконати й повернути запасне значення замість винятку:

$weather = rescue(fn () => $api->current($city), null);
$weather = rescue(fn () => $api->current($city), fn () => Cache::get("weather:$city"), report: false);

За замовчуванням виняток звітується (потрапляє в лог і Sentry), але не перериває запит. report: false - для очікуваних збоїв.

optional - безпечний доступ до властивостей null:

optional($user->address)->city;

У PHP 8 зазвичай краще nullsafe-оператор $user->address?->city. optional лишається корисним із замиканням: optional($user, fn ($u) => $u->fullName()).

tap - виконати дію над значенням і повернути саме значення:

return tap($user)->update(['last_login_at' => now()]);   // повертає $user, а не true

value - повертає значення, а якщо це замикання - результат його виклику. Зручно для параметрів «значення або callback».

once - мемоізація в межах одного об'єкта чи запиту:

public function stats(): array
{
    return once(fn () => $this->calculateExpensiveStats());
}

Повторні виклики stats() на тому самому екземплярі повернуть збережений результат. Для статичного контексту - один раз на запит.

Також: blank() / filled() (порожній рядок, пробіли, null, порожня колекція), transform() (застосувати callback, якщо значення не порожнє), when().

Міра: хелпер має робити код зрозумілішим. Ланцюжок tap(optional(value(...))) гірший за три рядки звичайного коду.

Докладніше в документації: Хелпери: retry

Str::slug() перетворює текст на фрагмент URL: транслітерує, переводить у нижній регістр і замінює все, крім літер і цифр, на роздільник.

Str::slug('Laravel 13 Framework');   // 'laravel-13-framework'

Сигнатура: Str::slug($title, $separator = '-', $language = 'en', $dictionary = ['@' => 'at']).

Третій параметр - мова транслітерації, і за замовчуванням це 'en'. Для кирилиці це дає результат за загальними правилами, а не за українськими:

Str::slug('Київ - столиця України');               // 'kiyiv-stolicia-ukrayini'
Str::slug('Київ - столиця України', '-', 'uk');    // 'kyyiv-stolytsia-ukrayiny'

Str::slug('Щастя й ґанок');                        // 'shhastia-i-ganok'
Str::slug('Щастя й ґанок', '-', 'uk');             // 'shchastia-y-ganok'

З 'uk' використовується українська таблиця (щ → shch, и → y, ц → ts), близька до офіційної транслітерації.

Unicode-slug без транслітерації:

Str::slug('Привіт', '-', null);   // 'привіт'

Браузери показують такі адреси нормально, але при копіюванні вони перетворюються на %D0%BF%D1%80... - для посилань у месенджерах і логах це незручно.

Практичні правила:

  • обрати одну мову транслітерації й використовувати її скрізь - у моделі, у міграції даних, у тестах. Якщо частина slug-ів створена з 'en', а частина з 'uk', той самий заголовок дасть різні адреси;
  • змінювати правила транслітерації на живому сайті - це зміна URL усіх сторінок. Потрібні 301-редиректи зі старих адрес, інакше втрачаються пошукові позиції й зовнішні посилання;
  • slug не має змінюватися автоматично при редагуванні заголовка опублікованого матеріалу - з тієї самої причини;
  • унікальність: Str::slug не перевіряє базу. Дублікати треба обробляти окремо (суфікс -2, ідентифікатор у URL чи унікальний індекс у базі);
  • $dictionary замінює символи до транслітерації: ['@' => 'at', '&' => 'and'].

Str::transliterate() і Str::ascii() - те саме перетворення без заміни пробілів і нижнього регістру; ascii теж приймає мову другим параметром.

Докладніше в документації: Рядки: Str::slug

Проблема sleep(): код з паузами (очікування між повторами, обмеження частоти запитів до API) робить тести повільними, а перевірити, скільки саме чекав код, неможливо.

Клас Sleep - обгортка над sleep/usleep, яку можна підробити в тестах:

use Illuminate\Support\Sleep;

Sleep::for(500)->milliseconds();
Sleep::for(2)->seconds();
Sleep::until(now()->addMinute());

// між запитами до API з лімітом частоти
foreach ($pages as $page) {
    $client->fetch($page);
    Sleep::for(1)->second();
}

У тесті:

use Illuminate\Support\Sleep;

Sleep::fake();

$importer->run();

Sleep::assertSlept(fn (Carbon\CarbonInterval $d) => $d->totalSeconds === 1, times: 3);
Sleep::assertSequence([
    Sleep::for(1)->second(),
    Sleep::for(2)->seconds(),
]);
Sleep::assertNeverSlept();

Тест проходить миттєво, а затримки перевіряються явно. Sleep::fake(syncWithCarbon: true) ще й пересуває «поточний час» Carbon на тривалість паузи.

Бонус: хелпер retry() під капотом чекає через Sleep, тож Sleep::fake() робить миттєвими й тести з повторними спробами.

Benchmark - швидко порівняти варіанти коду:

use Illuminate\Support\Benchmark;

Benchmark::dd([
    'eager' => fn () => Post::with('author')->get(),
    'lazy' => fn () => Post::all()->each->author,
], iterations: 10);
// ["eager" => "12.3ms", "lazy" => "48.7ms"]

$ms = Benchmark::measure(fn () => $report->build());   // середній час у мс

[$result, $ms] = Benchmark::value(fn () => $report->build());  // результат і час

Обмеження Benchmark:

  • це мікробенчмарк у поточному процесі: кеш бази, OPcache, прогрів впливають на результат. Перша ітерація часто повільніша - тому iterations;
  • для продакшен-продуктивності потрібні профілювальники й моніторинг (Xdebug, Blackfire, Pulse, APM), а не Benchmark::dd;
  • Benchmark::dd() зупиняє виконання - лише для локального налагодження.

Benchmark::value() корисний і в продакшен-коді: повертає результат разом з тривалістю, яку можна записати в лог чи метрику для повільних операцій.

Докладніше в документації: Хелпери: Sleep