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

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

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

4 питання

Масиви в PHP передаються за значенням, але копіюються ліниво - за принципом copy-on-write. Присвоєння чи передача в функцію не копіює дані, а лише збільшує лічильник посилань. Справжня копія з'являється в момент, коли одну з «копій» змінюють.

$a = range(1, 1_000_000);
$b = $a;        // пам'ять не виросла: обидві змінні дивляться на ті самі дані
$b[] = 1;       // тепер копія - і ще ~кілька десятків МБ

Що з цього випливає:

  • Передавати великий масив у функцію дешево, поки функція його не змінює. Передача за посиланням (&$items) «для швидкодії» зазвичай нічого не дає.
  • Посилання можуть, навпаки, спричинити копіювання: змішування посилань і звичайних змінних на ті самі дані змушує PHP розділяти масив раніше.
  • Класична пастка - foreach ($items as &$item) без unset($item) після циклу: змінна лишається посиланням на останній елемент, і наступний foreach за значенням його перезапише.

Об'єкти поводяться інакше: змінна зберігає ідентифікатор об'єкта, тож передача в функцію дає доступ до того самого об'єкта. Для копії потрібен clone.

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

=== порівнює рядки посимвольно і зупиняється на першій розбіжності. Отже, час порівняння залежить від того, скільки перших символів збіглося. Вимірюючи час відповіді на багатьох запитах, нападник може підбирати секрет символ за символом - це timing-атака.

hash_equals() порівнює рядки за сталий час: завжди проходить усю довжину, незалежно від того, де розбіжність.

// Перевірка підпису вебхука
$expected = hash_hmac('sha256', $payload, $secret);

if (! hash_equals($expected, $request->header('X-Signature'))) {
    abort(403);
}

Де це потрібно: скрізь, де рядок від користувача порівнюється із секретом - підписи вебхуків, API-токени, коди підтвердження, CSRF-токени.

Що варто знати:

  • Першим аргументом іде відомий рядок, другим - отриманий від користувача.
  • Довжину hash_equals не приховує: рядки різної довжини дають false одразу. Тому порівнюють хеші чи HMAC фіксованої довжини, а не сирі секрети.
  • Для паролів не потрібен ні ===, ні hash_equals: password_verify() уже порівнює безпечно.

Laravel використовує hash_equals усередині - наприклад, у перевірці CSRF-токена й підписаних URL.

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

Універсального «очищення» не існує: екранування залежить від того, куди потрапляє рядок. Тому дані зберігають як є, а екранують у момент виведення - під конкретний контекст.

HTML (вміст і атрибути):

echo htmlspecialchars($comment, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');

ENT_QUOTES екранує обидва типи лапок (важливо для атрибутів), ENT_SUBSTITUTE замінює зламаний UTF-8 замість повернення порожнього рядка. Blade {{ }} робить саме це.

Але HTML-екранування не захищає атрибут href: javascript:alert(1) пройде без змін. URL від користувача перевіряють за схемою (http/https).

SQL - не екранувати, а прив'язувати параметри:

$stmt = $pdo->prepare('SELECT * FROM users WHERE email = ?');
$stmt->execute([$email]);

addslashes для SQL небезпечний (кодування, інші СУБД), а ручне екранування легко забути.

URL:

$url = 'https://example.com/search?' . http_build_query(['q' => $query, 'page' => 2]);
rawurlencode($pathSegment);    // для частини шляху

Командний рядок:

exec('convert ' . escapeshellarg($input) . ' ' . escapeshellarg($output));

Ще краще - не будувати рядок команди взагалі, а передати аргументи масивом (Symfony Process, Process::run([...]) у Laravel), тоді оболонка не бере участі.

JavaScript усередині HTML:

<script>const user = <?= json_encode($user, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT) ?>;</script>

У Blade - @json($user) чи Js::from($user).

Правило: знати контекст виведення і використовувати засіб саме для нього. Рядок, безпечний для HTML, може бути небезпечним у SQL чи shell.

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

Ключ масиву в PHP може бути лише int або string. Усе інше приводиться, і деякі рядки теж:

  • Рядок з десятковим цілим числом ('1', '-5') стає цілим: '1' і 1 - один ключ.
  • Рядок з ведучим нулем чи пробілом ('01', ' 1') лишається рядком.
  • float обрізається до цілого: 1.7 → 1 (з PHP 8.1 - з попередженням про втрату точності).
  • bool стає 0 або 1.
  • null стає порожнім рядком ''.
  • Масиви й об'єкти як ключі - помилка (TypeError), зокрема й enum.
$a = ['1' => 'a', 1 => 'b', true => 'c', '01' => 'd'];
// [1 => 'c', '01' => 'd'] - перші три записи перезаписали один одного

Де це кусає:

  • Ідентифікатори-рядки: масив з ключами '007' і '7' - два ключі, а з '7' і 7 - один.
  • array_merge перенумеровує числові ключі. Масив [10 => 'Київ', 20 => 'Львів'], де ключі - ID міст, після array_merge стане [0 => 'Київ', 1 => 'Львів']. Рядкові ключі, схожі на числа, - теж числові, тож проблема та сама.
  • JSON: json_encode масиву з ключами 0, 1, 2 дає масив [...], а з «дірками» (0, 2) - об'єкт {"0":..,"2":..}. Клієнт отримає інший тип даних.
  • in_array / array_search без третього параметра true порівнюють через ==.

Порада: для словників зі «справжніми» рядковими ключами, де ці правила заважають, - SplObjectStorage, Map з ds-розширення чи колекції з явними ключами; для ID - не покладатися на перенумерацію і використовувати + чи array_replace замість array_merge.

Докладніше в документації: Масиви: ключі