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