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

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

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

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

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

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

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

// THREE SEPARATE FILES: save each PHP block under its indicated filename.
// File: lib.php
<?php
function takesInt(int $x): int { return $x; }
?>

// File: weak.php
<?php
require __DIR__ . '/lib.php';
echo takesInt('123'); // 123: coercive caller
?>

// File: strict.php
<?php
declare(strict_types=1);
require __DIR__ . '/lib.php';
try {
    echo takesInt('123');
} catch (TypeError $e) {
    echo 'TypeError';
}
?>
Відповісти на це запитання з ШІ-тренером

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

У PHP == виконує нестроге порівняння з перетворенням типів, коли воно застосовне; === вимагає однакових типів і значень без неявного перетворення скалярів. Числові рядки можуть порівнюватися як числа, тому не використовуй нестроге порівняння, коли потрібен точний збіг з урахуванням типу. Масиви рівні за ==, якщо містять однакові пари ключ/значення відповідно до правил порівняння, незалежно від порядку пар. === для масивів додатково вимагає однакового порядку й відповідних типів ключів та значень. == для об'єктів порівнює властивості об'єктів одного класу; === вимагає того самого екземпляра. in_array типово порівнює нестрого; передай true третім аргументом для строгої перевірки. Поведінка нестрогих порівнянь чисел і рядків змінилася у PHP 8, тому не покладайся на історичні приклади перетворення без зазначення версії.

<?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 дозволяє повертати будь-яке значення, включно з null. void оголошує, що функція не повертає змістовного значення: вона може дійти до кінця або використати return;, але не може повернути вираз. Звичайний виклик функції void дає null. never, доступний від PHP 8.1, означає, що функція ніколи не повертає керування викликачу звичайним способом. Вона може породити виняток, завершити виконання через exit або залишитися в нескінченному циклі; вона не зобов'язана зрештою завершити процес. Функція never не може містити інструкцію return. Головна різниця в тому, що функція 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?

iterable означає array|Traversable. Generator реалізує Iterator, який розширює Traversable, тому відповідає iterable. Користувацькі класи забезпечують поведінку Traversable через Iterator або IteratorAggregate, а не через безпосередню реалізацію Traversable у конкретному класі. Iterator надає методи керування ітерацією; IteratorAggregate повертає ітератор. Генераторна функція з yield повертає Generator. Є важлива відмінність: foreach також може перебирати видимі властивості звичайного об'єкта, який не реалізує 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 та як вони взаємодіють із наслідуванням і трейтами?

Члени public доступні всюди, де можна звернутися до об'єкта або класу. Члени protected доступні всередині класу, який їх оголошує, та пов'язаної ієрархії батьківських і дочірніх класів, але не довільному зовнішньому коду. Члени private доступні всередині класу, що їх оголошує; підклас не може використовувати приватний член батьківського класу як свій успадкований інтерфейс. Звичайний метод, що перевизначає успадкований, не може звужувати його видимість, однак конструктори мають особливі правила наслідування й не є безумовним прикладом цього правила. Члени трейта включаються до класу, який його використовує, і дотримуються його правил видимості. Адаптація 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 і як розв'язуються конфлікти, коли кілька трейтів визначають той самий метод?

Трейти забезпечують горизонтальне повторне використання коду: клас імпортує їхні члени через use, а сам трейт не можна інстанціювати. Метод, оголошений у класі, має пріоритет над методами трейтів, які своєю чергою мають пріоритет над успадкованими методами батьківського класу. Якщо кілька трейтів надають той самий метод і конфлікт ще не розв'язано методом самого класу, використовуй 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Як скалярні типи, типи з null, об'єднання, перетини й DNF виражають контракти інтерфейсів PHP та коли слід використовувати кожен із них?

Оголошення PHP описують прийняті й повернені значення. Скалярні оголошення, як-от int, float, string і bool, вибирають категорію значень; дозвіл сумісних скалярних перетворень також залежить від strict_types і контексту. ?T або T|null допускає null. Об'єднання, доступні від PHP 8.0, допускають альтернативи, наприклад int|string. Чисті перетини, доступні від PHP 8.1, вимагають, щоб об'єкт відповідав усім переліченим типам класів або інтерфейсів, як-от Cacheable&JsonSerializable; int&string не є допустимим перетином. Типи DNF (диз'юнктивна нормальна форма), доступні від PHP 8.2, поєднують альтернативи й перетини в дужках, наприклад (Cacheable&JsonSerializable)|string. Вибирай найвужчий зрозумілий контракт, який описує допустимі вхідні дані, замість додавання зайвих альтернатив.

<?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, викличні значення першого класу, викличні об'єкти й масиви callable у PHP?

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

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

Connection-budget example:
- 6 application servers
- 2 PHP-FPM pools on EACH server
- pm.max_children = 40 for EACH pool
- Assume at most 1 database connection per child

Potential request connections = 6 * 2 * 40 = 480.
If 40 were the TOTAL children per server across both pools, it would be 240.
Budget separately for queue workers, cron, deployments and administrators.
A limit of 300 database connections cannot accommodate 480 simultaneous connections.
Відповісти на це запитання з ШІ-тренером

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

Розділяй підключення до основної бази й реплік за змістом операції, а не лише за тим, чи починається SQL із SELECT: читання з блокуванням, записи й транзакції, яким потрібна узгодженість основної бази, мають використовувати її. Транзакції лише для читання можуть використовувати відповідну репліку, якщо це дозволяють вимоги до узгодженості. Асинхронна реплікація може відставати. Налаштування sticky у Laravel після запису спрямовує подальші читання поточного запиту до підключення для запису; закріплення за основною базою на фіксований інтервал між запитами лише пом'якшує проблему й не гарантує безумовного читання власних записів. Для гарантії читай з основної бази або чекай, доки репліка застосує потрібну позицію запису, з обмеженням часу й переходом на основну базу за його перевищення. Поведінка проксі відрізняється: RDS Proxy використовує окремі ендпойнти читання й запису та не є універсальним автоматичним розподілювачем кожної SQL-інструкції. Під час перемикання після відмови відкидай застарілі підключення та підключайся знову з обмеженим збільшенням пауз. Втрата відповіді на commit залишає результат невизначеним: не повторюй неідемпотентні записи наосліп; використовуй ключі ідемпотентності або спочатку з'ясуй результат.

<?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-застосунках без простою або з мінімальним простоєм?

Використовуй зміни за схемою розширення й скорочення, сумісні зі старою та новою версіями застосунку під час поступового розгортання. Спочатку додай сумісні структури, потім розгорни код, що працює з обома представленнями. Якщо потрібен подвійний запис, підтримуй його узгодженість і проєктуй відновлюване перенесення наявних даних з обмеженою швидкістю, яке не перезаписує новіші записи. Перевір скопійовані дані й переключи читання, перш ніж видаляти старі шляхи коду. Онлайн-DDL (мова визначення даних) зменшує окремі ризики блокування, але не завжди працює без блокувань: перевір специфічні для бази та версії блокування, обмеження транзакцій, витрати ресурсів і вплив на реплікацію. Наприклад, CREATE INDEX CONCURRENTLY у PostgreSQL має обмеження й після збою може залишити недійсний індекс. Видаляй старі стовпці лише після зникнення старих екземплярів застосунку й завершення потрібного періоду можливого відкату. Після руйнівних змін на етапі скорочення повернення до коду, якому потрібні видалені дані, уже не є автоматично безпечним.

<?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
Відповісти на це запитання з ШІ-тренером