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

Senior: питання на співбесіді з теми «Функції й замикання»

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

3 питання

Чиста функція:

  1. для тих самих аргументів завжди повертає той самий результат;
  2. не має побічних ефектів - не змінює нічого за своїми межами й не залежить від прихованого стану.

Побічні ефекти: запис у базу чи файл, HTTP-запит, надсилання листа, зміна глобальних чи статичних змінних, зміна переданих за посиланням аргументів, читання поточного часу, випадкових чисел, $_GET, конфігурації.

// Нечиста: залежить від часу й бази
function isExpired(int $subscriptionId): bool
{
    $sub = Subscription::find($subscriptionId);
    return $sub->ends_at < now();
}

// Чиста: усе потрібне - в аргументах
function isExpired(DateTimeImmutable $endsAt, DateTimeImmutable $now): bool
{
    return $endsAt < $now;
}

Чому це важливо для тестів:

  • Чисту функцію тестують одним рядком: дав вхід - перевірив вихід. Без бази, моків, фейкового часу.
  • Тести швидкі й стабільні: немає залежності від порядку, часу запуску, стану бази.
  • Легко перевірити граничні випадки - просто передати інші аргументи.

Як застосувати на практиці - «функціональне ядро, імперативна оболонка»:

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

Більшість логіки опиняється в легко тестованому ядрі, а оболонку покривають кількома інтеграційними тестами.

Не все має бути чистим: застосунок існує заради побічних ефектів. Мета - не прибрати їх, а відокремити й зібрати на краях, щоб логіка між ними лишалася простою для перевірки.

Докладніше в документації: Pure function

yield from делегує частину генерації іншому генератору чи ітерованому значенню: усі його значення віддаються так, ніби їх видав поточний генератор.

function walk(array $node): Generator
{
    yield $node['path'];

    foreach ($node['children'] as $child) {
        yield from walk($child);    // рекурсивний обхід дерева
    }
}

foreach (walk($tree) as $path) {
    echo $path, PHP_EOL;
}

Працює з генераторами, масивами й будь-яким Traversable.

Повернення значення з генератора: return у генераторі не віддає значення в цикл, а зберігає його як результат - його читають через getReturn() після завершення:

function importRows(iterable $rows): Generator
{
    $imported = 0;

    foreach ($rows as $row) {
        yield $row['id'];      // проміжні результати
        $imported++;
    }

    return $imported;          // підсумок
}

$gen = importRows($rows);
foreach ($gen as $id) { /* ... */ }
echo $gen->getReturn();        // кількість

А yield from повертає результат делегованого генератора:

$count = yield from importRows($rows);

Пастки:

  • Ключі. yield from зберігає ключі делегованого джерела. Два yield from по масивах з ключами 0, 1, 2 дадуть дублікати ключів, і iterator_to_array() перезапише значення. Рішення - iterator_to_array($gen, preserve_keys: false).
  • getReturn() до завершення генератора кидає виняток.
  • Генератор одноразовий: пройти його вдруге не можна.

Двосторонній обмін: $received = yield $value; разом із $gen->send($data) дозволяє передавати дані в генератор - на цьому будували співпрограми до появи Fibers.

Докладніше в документації: Синтаксис генераторів

extract($array) створює змінні в поточній області видимості з ключів масиву:

extract(['name' => 'Оля', 'role' => 'admin']);
echo $name;   // 'Оля'

compact('name', 'role') - зворотне: збирає змінні в масив за іменами.

Чому extract небезпечний:

  • Перезапис змінних. За замовчуванням extract перезаписує наявні змінні. Якщо масив прийшов від користувача, нападник може підмінити будь-яку змінну функції:
$isAdmin = false;
extract($_POST);         // POST isAdmin=1 → $isAdmin = '1'
if ($isAdmin) { /* ... */ }

Історично це була реальна вразливість у CMS і плагінах.

  • Невидимі змінні. Читач коду не бачить, звідки взялася $role - її оголошення ніде немає. IDE й статичний аналіз теж не бачать.
  • Опечатки не ловляться: відсутній ключ просто не створить змінну, і код отримає «Undefined variable» далі.

Якщо extract таки потрібен: ніколи з даними від користувача і з прапорцем, що забороняє перезапис - extract($data, EXTR_SKIP).

compact безпечніший, але має свої мінуси:

return view('orders.show', compact('order', 'items', 'total'));
  • Імена змінних - рядки: перейменування змінної в IDE не оновить рядок у compact, і ключ зникне.
  • Відсутня змінна - лише попередження.

Явний масив читабельніший і надійніший при рефакторингу:

return view('orders.show', ['order' => $order, 'items' => $items, 'total' => $total]);

Де ці функції живуть легітимно: шаблонізатори (Blade під капотом робить extract даних подання в області видимості шаблону) - там джерело даних контрольоване, і це їхня пряма задача.

Докладніше в документації: extract