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

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

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

2 питання

Багато класів Laravel використовують трейт Macroable: Str, Stringable, Arr, Collection, Request, Builder, Http, Number, Uri та інші. Макрос додає метод «ззовні» без успадкування.

// AppServiceProvider::boot()
use Illuminate\Support\Str;
use Illuminate\Support\Stringable;

Str::macro('phone', function (string $value): string {
    return preg_replace('/\D+/', '', $value);
});

Stringable::macro('phone', function (): Stringable {
    return new Stringable(Str::phone($this->value));
});

Str::phone('+38 (067) 123-45-67');        // '380671234567'
str('+38 (067) 123-45-67')->phone();      // Stringable

Важливі деталі:

  • Str і Stringable - різні класи: макрос на Str не з'являється у fluent-ланцюжку. Для обох стилів потрібні два макроси;
  • $this у замиканні прив'язується до екземпляра (для Stringable - до об'єкта з властивістю value), а в статичному виклику - ні;
  • mixin реєструє кілька макросів з класу, кожен публічний чи захищений метод якого повертає замикання:
Str::mixin(new StrMixin);

Ризики:

  1. Конфлікт з майбутніми методами фреймворку. Макроси викликаються через __call / __callStatic, тобто лише якщо справжнього методу немає. Якщо в наступній версії Laravel з'явиться Str::phone() з іншою поведінкою, ваш макрос мовчки перестане викликатися. Захист - префікси (Str::appPhone) і тести на поведінку макросів;
  2. Невидимість для інструментів: IDE й PHPStan не бачать макросів без додаткових анотацій чи ide-helper; автодоповнення зникає, а аналізатор лається на «невідомий метод»;
  3. Глобальний стан: макрос зареєстрований для всього процесу. У пакеті він може зіткнутися з макросом іншого пакета чи застосунку з тим самим ім'ям - перемагає той, хто зареєструвався останнім;
  4. Прихована логіка: бізнес-правила в макросах (Str::orderNumber()) важче знайти й тестувати, ніж у звичайному класі.

Коли макрос доречний: невелика загальна утиліта, що природно продовжує API класу (форматування телефону, нормалізація пробілів) і використовується в багатьох місцях.

Коли краще звичайний клас чи value object: доменна логіка, щось із залежностями, чи метод, потрібний в одному модулі. PhoneNumber::fromString($raw)->normalized() явніший, тестується окремо й підтримується IDE без підказок.

Докладніше в документації: Колекції: розширення колекцій

Склеювання URL рядками - джерело тонких помилок: забутий ? чи &, подвійне кодування, втрачені параметри, // у шляху, неекранований пробіл.

// крихко
$url = $base . '/search?q=' . $query . '&page=' . $page;

Illuminate\Support\Uri (на основі бібліотеки League URI) дає незмінний (immutable) об'єкт для розбору й зміни адрес:

use Illuminate\Support\Uri;

$uri = Uri::of('https://example.com/search?q=php')
    ->withQuery(['page' => 2])          // злиття з наявними параметрами
    ->withFragment('results');

(string) $uri;   // 'https://example.com/search?q=php&page=2#results'

$uri->host();                 // 'example.com'
$uri->path();                 // 'search'
$uri->query()->get('q');      // 'php'
$uri->pathSegments();         // колекція сегментів

Змінення параметрів запиту:

Метод Що робить
withQuery([...]) додає й перезаписує параметри (merge: false - замінює всі)
withQueryIfMissing([...]) лише відсутні - зручно для значень за замовчуванням
withoutQuery(['utm_source']) прибрати параметри
replaceQuery([...]) замінити весь рядок запиту
pushOntoQuery('tags', 'php') додати значення до масиву tags[]

Кожен with... повертає новий об'єкт - вихідний не змінюється, тож базову адресу можна безпечно перевикористовувати.

Інтеграція з маршрутизацією:

Uri::route('posts.show', ['post' => $post]);
Uri::signedRoute('unsubscribe', ['user' => $user]);
Uri::temporarySignedRoute('download', now()->addHour(), ['file' => $file]);

return Uri::to('/dashboard')->withQuery(['tab' => 'billing'])->redirect();

Поточний запит: $request->uri() повертає той самий об'єкт, тож «поточна сторінка з іншим фільтром» будується без ручного розбору $_GET:

$request->uri()->withQuery(['sort' => 'price'])->withoutQuery(['page']);

Де це особливо важливо:

  • очищення URL від трекінгових параметрів перед збереженням чи порівнянням;
  • побудова посилань на сторонні API з набором параметрів;
  • перевірка редиректів: Uri::of($next)->host() замість регулярних виразів - щоб не відкрити відкритий редирект на чужий домен.

Обмеження: Uri не валідує, що адреса безпечна чи досяжна. Для SSRF-захисту потрібна окрема перевірка схеми, хоста й IP-адреси, до якої він резолвиться.

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