Підготовка до співбесіди з PHP

Запитання для співбесіди з PHP

15 обраних запитань для співбесіди з PHP бекенду, згрупованих за рівнем досвіду. Використовуйте їх для повторення основ, практичних компромісів та міркувань щодо виробничих рішень на Senior рівні.

Почати AI-співбесіду з PHPКредитна картка не потрібна. Доступна 1 безкоштовна сесія.
Практика технічних співбесід англійськоюРежим для тих, для кого англійська не є рідною, щоб тренувати технічні співбесіди.

Запитання для Junior

1Як система типів PHP розрізняє слабку та сувору типізацію, і що змінюється, коли ввімкнено `declare(strict_types=1)`?

PHP за замовчуванням є слабким/коерцитивним для декларацій скалярних типів: коли функція оголошує скалярні параметри або типи, що повертаються, PHP може перетворювати сумісні значення, такі як числові рядки, на числа, замість того, щоб відхиляти їх. `declare(strict_types=1)` робить перевірку скалярних типів суворою для цього файлу, тому несумісні скалярні значення не перетворюються, і викидається `TypeError`. Директива діє на рівні файлу: для аргументів функцій, визначених користувачем, файл, що викликає функцію, контролює суворість; для операторів повернення файл, де визначено функцію, контролює суворість значення, що повертається. Це не робить PHP глобально суворим і не відключає всі перетворення в мові.

<?php
// lib.php
function takesInt(int $x): int {
    return $x;
}

// weak.php, no strict_types
echo takesInt('123'); // coerces string to int

// strict.php
declare(strict_types=1);
echo takesInt('123'); // TypeError
Відповісти на це запитання з ШІ-тренером

2Поясніть різницю між `==` та `===` в PHP, включаючи підводні камені з числовими рядками, масивами, об'єктами та функцією `in_array()`.

`==` виконує нестроге порівняння, тому PHP може приводити типи перед порівнянням; `===` виконує строге порівняння, вимагаючи як однакового значення, так і однакового типу без примусового перетворення. Числові рядки є поширеною пасткою, оскільки вони можуть розглядатися як числа при нестрогих порівняннях. Масиви та об'єкти мають власні правила порівняння: масиви порівнюються за ключами та значеннями, тоді як `==` для об'єктів порівнює об'єкти одного класу за властивостями, а `===` для об'єктів вимагає точно того самого екземпляра. `in_array()` за замовчуванням використовує нестроге порівняння, тому передайте `true` як третій аргумент, коли тип повинен збігатися.

<?php
var_dump(123 == '123');      // loose: numeric string is converted
var_dump(123 === '123');     // strict: int !== string

$values = [0, 1, 2];
var_dump(in_array('0', $values));       // loose
var_dump(in_array('0', $values, true)); // strict
Відповісти на це запитання з ШІ-тренером

3Чим відрізняються `mixed`, `void` і `never` як типи повернення PHP, і які гарантії вони надають?

Як типи повернення, `mixed`, `void` і `never` надають різні обіцянки. `mixed` означає, що функція може повернути будь-яке значення, включаючи `null`. `void` означає, що функція не призначена для повернення значущого значення; вона може використовувати `return;`, але не може повертати вираз. `never` означає, що функція не повертається до свого викликаючого об'єкта нормально: вона повинна завжди викидати виняток, завершувати роботу або іншим чином переривати потік виконання. `never` є сильнішим за `void`, оскільки виконання не може продовжуватися після нормального виклику функції типу `never`.

<?php
function parse(string $raw): mixed {
    return json_decode($raw, true);
}

function logMessage(string $message): void {
    error_log($message);
    return; // allowed
}

function fail(string $message): never {
    throw new RuntimeException($message);
}

logMessage('starting');
fail('stop');
echo 'unreachable';
Відповісти на це запитання з ШІ-тренером

4Що таке `iterable` в PHP, і як воно пов'язане з `array`, `Traversable`, `Iterator` та `Generator`?

У PHP, `iterable` — це оголошення типу (type declaration), що означає «`array` або об'єкт, що реалізує `Traversable`». `Traversable` — це базовий інтерфейс-маркер (marker interface) для об'єктів, які можна використовувати в конструкції `foreach`; код користувача (userland code) зазвичай реалізує `Iterator` або `IteratorAggregate`, які розширюють `Traversable`. `Generator` — це об'єкт, який повертається функцією-генератором (generator function) за допомогою `yield`; він реалізує `Iterator`, тому він також є `Traversable` і приймається скрізь, де потрібен `iterable`.

function printItems(iterable $items): void {
    foreach ($items as $item) {
        echo $item, PHP_EOL;
    }
}

printItems(['a', 'b']);

function letters(): Generator {
    yield 'x';
    yield 'y';
}

printItems(letters());
Відповісти на це запитання з ШІ-тренером

5Які правила видимості (`public`, `protected` та `private`) існують у PHP, і як вони взаємодіють зі спадкуванням та трейтами (traits)?

У PHP публічні (`public`) члени доступні з будь-якого місця, захищені (`protected`) члени доступні з класу, що їх оголошує, та підкласів, а приватні (`private`) члени доступні лише з класу, який їх оголошує. При спадкуванні підкласи можуть використовувати успадковані публічні та захищені члени, але приватні члени батьківського класу недоступні як успадкований API (Application Programming Interface). При перевизначенні методів дочірній клас зазвичай не може зменшувати видимість; наприклад, публічний метод батьківського класу повинен залишатися публічним. Трейти (traits) включаються до класу, що їх використовує, і члени трейтів дотримуються правил видимості PHP. Видимість методів трейта також може бути адаптована або псевдонімізована за допомогою ключового слова `as` при імпорті трейта.

<?php
class Base {
    public function a() {}
    protected function b() {}
    private function c() {}
}

trait Logs {
    public function log(string $message): void {}
}

class Child extends Base {
    use Logs { log as protected writeLog; }

    public function test(): void {
        $this->a();              // OK
        $this->b();              // OK
        // $this->c();           // Not accessible: private to Base
        $this->writeLog('ok');   // OK inside class, protected alias
    }
}

$child = new Child();
$child->a();
// $child->b();           // Not accessible from outside
// $child->writeLog('x'); // Not accessible from outside
Відповісти на це запитання з ШІ-тренером

6Як працюють трейти в PHP, і як вирішуються конфлікти, коли кілька трейтів визначають один і той же метод?

Трейт (trait) — це механізм PHP для горизонтального повторного використання коду: клас може використовувати один або кілька трейтів, і члени трейта інтегруються в клас. Трейт не є самостійними класами і не можуть бути інстанційовані безпосередньо. Якщо кілька трейтів надають методи з однаковим ім'ям, PHP вимагає явного вирішення конфліктів. Оператор `insteadof` обирає, який метод трейта використовується для конфліктуючого імені. Оператор `as` може створити псевдонім для методу трейта, а також змінити видимість імпортованого методу. Методи класу перевизначають методи трейтів, а методи трейтів мають пріоритет над успадкованими батьківськими методами.

<?php
trait JsonLogger {
    public function log(string $message): void {
        echo "json:$message\n";
    }
}

trait FileLogger {
    public function log(string $message): void {
        echo "file:$message\n";
    }
}

class Service {
    use JsonLogger, FileLogger {
        JsonLogger::log insteadof FileLogger;
        FileLogger::log as logToFile;
        JsonLogger::log as protected logJson;
    }
}

$service = new Service();
$service->log('hello');
$service->logToFile('hello');
// $service->logJson('hello'); // not accessible from outside; protected alias
Відповісти на це запитання з ШІ-тренером

7Що таке перерахування (enums) в PHP, включаючи чисті перерахування та перерахування з підтримкою базового типу, і як вони покращують моделювання предметної області?

Перерахування (enums) в PHP визначають закритий набір іменованих варіантів для типу, таких як статуси замовлень або ролі користувачів. Чисті перерахування мають лише іменовані варіанти; перерахування з підтримкою базового типу (backed enums) надають кожному варіанту унікальне рядкове або цілочисельне значення, корисне для зберігання, API (інтерфейсів прикладного програмування) або сумісності. Перерахування покращують моделювання предметної області, замінюючи крихкі необроблені рядки, цілі числа або константи типобезпечними, самодокументованими значеннями. Перерахування в PHP також можуть мати методи та реалізовувати інтерфейси; усі перерахування підтримують перелік своїх варіантів, а перерахування з підтримкою базового типу підтримують перетворення на їхні скалярні значення та з них.

enum OrderState
{
    case Draft;
    case Paid;
    case Shipped;
}

enum PaymentStatus: string
{
    case Pending = 'pending';
    case Complete = 'complete';
    case Failed = 'failed';
}

$status = PaymentStatus::tryFrom('complete');
var_dump($status === PaymentStatus::Complete);
Відповісти на це запитання з ШІ-тренером

Запитання для Middle

8Як скалярні, нульовані (nullable), об'єднані (union), типи перетину (intersection) та ДНФ-типи (DNF - Disjunctive Normal Form) виражають контракти API (Application Programming Interface) PHP, і коли слід використовувати кожен з них?

Оголошення типів PHP описують значення, які API приймає або повертає. Скалярні типи, такі як `int`, `float`, `string` та `bool`, використовуються, коли очікується рівно один примітивний тип. Нульовані типи, записані як `?T` або `T|null`, дозволяють значення типу `T` або `null`. Об'єднані типи, такі як `A|B`, дозволяють одну з кількох альтернатив. Типи перетину, такі як `A&B`, вимагають, щоб значення задовольняло всі перелічені типи, зазвичай декілька інтерфейсів. ДНФ-типи поєднують об'єднані типи та типи перетину, наприклад `(A&B)|C`, щоб виражати складніші контракти. Кожен слід використовувати, коли він відповідає реальній обіцянці API, уникаючи зайво широких або заплутаних типів.

<?php
interface Cacheable {}

class Report implements Cacheable, JsonSerializable {
    public function jsonSerialize(): mixed { return []; }
}

function findName(int $id): ?string {
    return $id > 0 ? 'Alice' : null;
}

function normalize(int|string $id): string {
    return (string) $id;
}

function store(Cacheable&JsonSerializable $item): void {
    // must satisfy both interfaces
}

function handle((Cacheable&JsonSerializable)|string $input): void {
    // either an object satisfying both contracts, or a string
}
Відповісти на це запитання з ШІ-тренером

9Яка різниця між `callable`, `Closure`, першокласними викликовими об'єктами (first-class callables), викликовими об'єктами (invokable objects) та викликовими масивами (callable arrays) в PHP?

`callable` — це широкий тип/контракт для будь-якого значення, яке PHP може викликати, наприклад, рядок з ім'ям функції, `Closure`, викликовий об'єкт або масив-посилання на метод, такий як `[$object, 'method']` або `[ClassName::class, 'method']`. `Closure` — це конкретний об'єкт анонімної функції, і сам по собі є викликовим. Синтаксис першокласних викликових об'єктів (first-class callable), такий як `$obj->method(...)` або `strlen(...)`, створює `Closure` з викликового виразу. Викликовий об'єкт (invokable object) є викликовим, тому що він визначає метод `__invoke()`. Викликові масиви (callable arrays) — це традиційна форма посилання на метод з двох елементів. Функція `is_callable()` може використовуватися для перевірки можливості виклику під час виконання.

class Greeter {
    public function hello(string $name): string { return "Hello $name"; }
    public function __invoke(string $name): string { return "Hi $name"; }
}

$g = new Greeter();

$closure = fn(string $name) => "Hey $name";
$arrayCallable = [$g, 'hello'];
$firstClass = $g->hello(...);
$invokable = $g;

foreach ([$closure, $arrayCallable, $firstClass, $invokable] as $cb) {
    echo $cb('Sam'), PHP_EOL;
}
Відповісти на це запитання з ШІ-тренером

10Як PHP обробляє переповнення цілих чисел, точність чисел з плаваючою комою та точні десяткові обчислення?

Цілі числа PHP мають розмір, що залежить від платформи, з обмеженнями, які відображаються константами, такими як `PHP_INT_MAX`. PHP не зберігає автоматично результати нативних цілих чисел як цілі числа довільної точності після переповнення; цілочисельна арифметика з переповненням може стати `float` або іншим чином втратити точну цілочисельну семантику. Числа з плаваючою комою PHP є двійковими значеннями з плаваючою комою, тому багато десяткових дробів, таких як `0.1`, є наближеними. Для точних десяткових або грошових обчислень уникайте чисел з плаваючою комою та використовуйте точний підхід, такий як мінорні цілочисельні одиниці, де це доречно, або інструменти для роботи з десятковими числами фіксованої/довільної точності, такі як BCMath. GMP корисний для цілочисельної арифметики довільної точності.

var_dump(0.1 + 0.2 === 0.3);
printf('%.17f\n', 0.1 + 0.2);
Відповісти на це запитання з ШІ-тренером

11Поясніть посилання PHP (`&`), копіювання при записі (copy-on-write) та як посилання можуть створювати несподівані побічні ефекти.

Змінні PHP внутрішньо представлені за допомогою `zval`. Для таких значень, як масиви та рядки, PHP використовує копіювання при записі (copy-on-write): присвоєння `$b = $a` зазвичай спільно використовує те саме базове значення, доки одна зі змінних не буде змінена, після чого PHP відокремлює/копіює його. Посилання, створене за допомогою `&`, робить змінні псевдонімами до одного і того ж контейнера, тому зміна однієї змінної змінює іншу і може обійти поведінку розділення, яку люди очікують від звичайного присвоєння. Передача за посиланням дозволяє функції змінювати змінну викликаючої сторони. Посилання можуть спричинити дивні побічні ефекти, особливо з `foreach ($array as &$value)`: змінна циклу залишається посиланням на останній елемент після циклу, тому повторне використання її пізніше може випадково перезаписати цей елемент, якщо ви не використаєте `unset($value)`.

$a = [1, 2];
$b = $a;      // shared until modification
$b[0] = 99;   // separates; $a is unchanged
var_dump($a, $b);

$c =& $a;     // alias/reference
$c[0] = 42;   // modifies $a too
var_dump($a, $c);
Відповісти на це запитання з ШІ-тренером

12Як працюють дескриптори об'єктів, присвоєння, клонування (clone), `__clone`, поверхневе копіювання (shallow copy) та глибоке копіювання (deep copy) у PHP?

У PHP змінна об'єкта зберігає дескриптор об'єкта, а не повну копію об'єкта. Присвоєння `$b = $a` копіює дескриптор, тому обидві змінні посилаються на один і той же об'єкт; зміни об'єкта через будь-яку з цих змінних будуть видимі через іншу, без використання `&`. `clone $a` створює новий екземпляр об'єкта з поверхневою копією властивостей оригінального об'єкта. Якщо властивість сама є об'єктом, властивість клонованого об'єкта все ще посилається на той самий вкладений об'єкт, якщо ви явно не клонуєте його. Метод `__clone()` викликається на новому об'єкті після поверхневого копіювання і є місцем для скидання ідентифікаторів, від'єднання ресурсів або виконання глибокого копіювання вкладених об'єктів. Глибоке копіювання означає рекурсивне копіювання вкладених змінних об'єктів, які не повинні бути спільними.

class Address {
    public function __construct(public string $city) {}
}

class User {
    public function __construct(public string $name, public Address $address) {}

    public function __clone() {
        $this->address = clone $this->address; // make nested object independent
    }
}

$u1 = new User('Ann', new Address('Paris'));
$u2 = $u1;          // same object handle
$u2->name = 'Beth';
echo $u1->name, PHP_EOL;

$u3 = clone $u1;    // new User object; __clone deep-clones address
$u3->address->city = 'Rome';
echo $u1->address->city, PHP_EOL;
echo $u3->address->city, PHP_EOL;
Відповісти на це запитання з ШІ-тренером

Запитання для Senior

13Як би ви діагностували та усували проблему вичерпання підключень до бази даних і спроектували управління підключеннями для PHP-FPM та довготривалих воркерів?

Щоб усунути проблему вичерпання підключень, я б порівняв ліміти підключень до бази даних та поточні сесії із загальною паралельністю PHP: кожен активний дочірній процес PHP-FPM може утримувати власне підключення до бази даних, а численні пули додатків, хости, завдання cron, обробники черг, міграції та розгортання збільшують навантаження. Я б перевірив подання сесій бази даних, стани підключень, сесії, що простоюють у транзакції, повільні запити, очікування блокувань, журнали помилок, метрики пулу, а також нещодавні зміни трафіку чи кількості воркерів. Для PHP-FPM необхідно тримати `max_children` та паралельність для кожного хоста в межах бюджету бази даних, використовувати короткі транзакції/підключення, обмежені областю запиту, негайно виконувати фіксацію або відкат, а також уникати утримання підключень під час повільних зовнішніх викликів. Постійні PDO-підключення можуть зменшити накладні витрати на підключення, але можуть закріпити багато неактивних підключень на кожен процес FPM та зберігати стан сесії, тому вони вимагають обережності. Пулер або проксі, такий як PgBouncer, може мультиплексувати багато PHP-клієнтів на меншу кількість підключень до сервера бази даних, якщо додаток сумісний. Довготривалі воркери потребують явного управління життєвим циклом підключення: перевірок стану, перепідключення після тайм-аутів простою/перезапусків/відмовостійких переключень/форків, скидання стану сесії та повторення лише безпечної або ідемпотентної роботи.

Example:
- 6 application servers
- 40 PHP-FPM max_children per server
- 2 separate FPM pools that can hit the database

Potential concurrent PHP request DB connections = 6 * 40 * 2 = 480

If PostgreSQL max_connections is 300 and queues/cron/admin tools also connect,
connection exhaustion is expected during traffic spikes.
Відповісти на це запитання з ШІ-тренером

14Як би ви обробляли репліки для читання, розділення запитів на читання/запис, затримку реплікації та відмовостійкість основної бази даних у PHP (Hypertext Preprocessor)?

Обробка реплік для читання та відмовостійкості в PHP (Hypertext Preprocessor) вимагає розділення трафіку читання та запису, зменшення затримки реплікації та керування перебоями у з'єднанні під час відмовостійкості. На рівні застосунку конфігурація бази даних або проміжне програмне забезпечення (middleware) спрямовує запити на читання (`SELECT`) до реплік для читання, а запити на запис (`INSERT`, `UPDATE`, `DELETE`) – до основного екземпляра (альтернативно керується через проксі баз даних, такі як ProxySQL або AWS RDS (Amazon Web Services Relational Database Service) Proxy). Оскільки реплікація є асинхронною, затримка реплікації може спричинити застарілі дані при читанні. Щоб гарантувати консистентність 'read-your-writes' (читання своїх записів), застосунок повинен спрямовувати запити на читання в межах активної транзакції до основного сервера і тимчасово "приклеювати" подальші запити на читання до основного сервера для конкретної сесії користувача або життєвого циклу запиту відразу після запису. Під час відмовостійкості основного сервера сучасні кластери баз даних підвищують репліку та оновлюють кінцеві точки DNS (Domain Name System)/проксі. У PHP, рівень з'єднання повинен перехоплювати втрати з'єднання та помилки тільки для читання, відкидати застарілі сокети PDO (PHP Data Objects) та застосовувати контрольовані повторні спроби з експоненційною затримкою (exponential backoff).

<?php
// config/database.php
return [
    'mysql' => [
        'read' => [
            'host' => [env('DB_REPLICA_HOST_1'), env('DB_REPLICA_HOST_2')],
        ],
        'write' => [
            'host' => [env('DB_PRIMARY_HOST')],
        ],
        'sticky'    => true,
        'driver'    => 'mysql',
        'database'  => env('DB_DATABASE'),
        'username'  => env('DB_USERNAME'),
        'password'  => env('DB_PASSWORD'),
        'charset'   => 'utf8mb4',
        'collation' => 'utf8mb4_unicode_ci',
    ],
];
Відповісти на це запитання з ШІ-тренером

15Як міграції бази даних та зміни схеми розгортаються безпечно з нульовим або мінімальним часом простою в PHP-додатках?

Розгортання міграцій бази даних без простоїв у PHP-додатках вимагає відокремлення еволюції схеми від розгортання коду програми за допомогою шаблону "Розширення та Скорочення" (Expand and Contract / паралельний запуск) поряд з неблокуючими практиками онлайн-DDL (Data Definition Language). На фазі **Розширення (Expand)** застосовуються зворотньо сумісні зміни схеми (наприклад, додавання колонок, що допускають NULL, нових таблиць або паралельних індексів за допомогою інструментів, таких як `pt-online-schema-change`, `gh-ost`, або нативного онлайн-DDL у MySQL/PostgreSQL). Додаток потім розгортається для підтримки подвійного запису (запис як у старі, так і в нові структури схеми) при читанні з будь-якої з них. Історичні записи потім асинхронно заповнюються у фонових пакетах з обмеженням швидкості, щоб запобігти блокуванню таблиць або спричиненню затримки реплікації. На фазі **Скорочення (Contract)**, після того як перевіряється цілісність даних і читання повністю переключаються на нову структуру, розгортання коду видаляє посилання на стару схему. Нарешті, міграція очищення видаляє застарілі стовпці або таблиці. Безпечний відкат вимагає, щоб кожен проміжний стан схеми залишався повністю сумісним як з попередніми, так і з новими версіями програми під час покрокового розгортання (rolling releases).

<?php
// Step 1: Migration (Expand) -> Add new column as nullable
Schema::table('users', function (Blueprint $table) {
    $table->string('full_name')->nullable()->after('name');
});

// Step 2: Code deployment -> Write to both, read with fallback
class User extends Model {
    public function setNameAttribute($value) {
        $this->attributes['name'] = $value;
        $this->attributes['full_name'] = $value; // Dual write
    }
}

// Step 3: Background worker backfills existing rows in small batches
// Step 4: Code deployment -> Read/write exclusively from 'full_name'
// Step 5: Migration (Contract) -> Drop legacy 'name' column
Відповісти на це запитання з ШІ-тренером