Подготовка к собеседованию по PHP

Вопросы для собеседования по PHP

15 отобранных вопросов для собеседования по бэкенду на PHP, сгруппированных по уровням квалификации. Используйте их для повторения основ, понимания практических компромиссов и рассуждений на уровне Senior-разработчика о производственных системах.

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

Вопросы для Junior-разработчиков

1Как система типов PHP различает слабую и строгую типизацию, и что меняется при включении `declare(strict_types=1)`?

PHP по умолчанию использует слабую/принудительную типизацию для объявлений скалярных типов: когда функция объявляет скалярные типы параметров или возвращаемых значений, PHP может преобразовывать совместимые значения, такие как числовые строки, в числа вместо их отклонения. `declare(strict_types=1)` делает проверку скалярных типов строгой для этого файла, поэтому несовместимые скалярные значения не преобразуются, и выбрасывается `TypeError`. Эта директива имеет область видимости файла: для аргументов пользовательских функций файл вызывающей стороны контролирует строгость; для операторов return файл, в котором определена функция, контролирует строгость возвращаемого значения. Это не делает 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
Попробовать ответить на вопрос AI-тренеру

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
Попробовать ответить на вопрос AI-тренеру

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';
Попробовать ответить на вопрос AI-тренеру

4Что такое `iterable` в PHP и как он связан с `array`, `Traversable`, `Iterator` и `Generator`?

В PHP `iterable` — это объявление типа, означающее «массив (`array`) или объект, реализующий `Traversable`». `Traversable` — это базовый интерфейс-маркер для объектов, которые могут использоваться циклом `foreach`; пользовательский код обычно реализует `Iterator` или `IteratorAggregate`, которые расширяют `Traversable`. Генератор (`Generator`) — это объект, возвращаемый функцией-генератором с использованием `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());
Попробовать ответить на вопрос AI-тренеру

5Каковы правила видимости `public`, `protected` и `private` в PHP и как они взаимодействуют с наследованием и трейтами?

В PHP члены, объявленные как `public`, доступны отовсюду; члены `protected` доступны из объявляющего класса и подклассов; а члены `private` доступны только из класса, который их объявляет. При наследовании подклассы могут использовать унаследованные `public` и `protected` члены, но `private` члены родительского класса недоступны как унаследованный API. При переопределении методов дочерний класс, как правило, не может уменьшать видимость; например, `public` метод родительского класса должен оставаться `public`. Трейты (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
Попробовать ответить на вопрос AI-тренеру

6Как работают трейты в PHP, и как разрешаются конфликты, когда несколько трейтов определяют один и тот же метод?

Трейты — это механизм 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
Попробовать ответить на вопрос AI-тренеру

7Что такое перечисления PHP, включая чистые (pure) и поддерживаемые (backed) перечисления, и как они улучшают доменное моделирование?

Перечисления PHP (PHP enums) определяют закрытый набор именованных вариантов для типа, таких как статусы заказа или роли пользователя. Чистые (pure) перечисления имеют только именованные варианты; поддерживаемые (backed) перечисления дают каждому варианту уникальное строковое или целочисленное значение, что полезно для хранения, API (Application Programming Interface) или взаимодействия. Перечисления улучшают доменное моделирование, заменяя хрупкие необработанные строки, целые числа или константы типобезопасными, самодокументируемыми значениями. Перечисления 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);
Попробовать ответить на вопрос AI-тренеру

Вопросы для Middle-разработчиков

8Как скалярные, обнуляемые, объединённые (union), пересекающиеся (intersection) и DNF (дизъюнктивная нормальная форма) типы выражают контракты API (Application Programming Interface) в PHP, и когда следует использовать каждый из них?

Объявления типов PHP описывают значения, которые принимает или возвращает API. Скалярные типы, такие как `int`, `float`, `string` и `bool`, используются, когда ожидается ровно один примитивный тип. Обнуляемые типы, записываемые как `?T` или `T|null`, допускают либо значение типа `T`, либо `null`. Объединённые (union) типы, такие как `A|B`, допускают одну из нескольких альтернатив. Пересекающиеся (intersection) типы, такие как `A&B`, требуют, чтобы значение удовлетворяло всем перечисленным типам, обычно нескольким интерфейсам. DNF (дизъюнктивная нормальная форма) типы комбинируют объединения и пересечения, например `(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
}
Попробовать ответить на вопрос AI-тренеру

9В чем разница между `callable`, `Closure`, вызываемыми первого класса, вызываемыми объектами и `callable`-массивами в PHP?

`callable` — это широкий тип/контракт для любого значения, которое PHP может вызвать, например, строка с именем функции, `Closure`, вызываемый объект или массив-ссылка на метод, такой как `[$object, 'method']` или `[ClassName::class, 'method']`. `Closure` — это конкретный объект анонимной функции, и он сам по себе является `callable`. Синтаксис вызываемых первого класса, такой как `$obj->method(...)` или `strlen(...)`, создаёт `Closure` из `callable`-выражения. Вызываемый объект является `callable`, потому что он определяет метод `__invoke()`. `Callable`-массивы — это традиционная двухэлементная форма ссылки на метод. Функция `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;
}
Попробовать ответить на вопрос AI-тренеру

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);
Попробовать ответить на вопрос AI-тренеру

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);
Попробовать ответить на вопрос AI-тренеру

12Как работают дескрипторы (ссылки) объектов, присваивание, клонирование, `__clone`, поверхностное и глубокое копирование в 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;
Попробовать ответить на вопрос AI-тренеру

Вопросы для Senior-разработчиков

13Как бы вы устраняли исчерпание пула соединений базы данных и проектировали управление соединениями для PHP-FPM (FastCGI Process Manager) и долгоживущих воркеров?

Для устранения исчерпания пула соединений я бы сравнил лимиты соединений базы данных и текущие сессии с общей конкурентностью PHP: каждый активный дочерний процесс PHP-FPM может удерживать собственное соединение с базой данных, а несколько пулов приложений, хостов, cron-заданий, воркеров очередей, миграций и развертываний увеличивают нагрузку. Я бы просмотрел представления сессий базы данных, состояния соединений, сессии "idle-in-transaction", медленные запросы, ожидания блокировок, логи ошибок, метрики пула и недавние изменения трафика или количества воркеров. Для PHP-FPM необходимо поддерживать `max_children` и конкурентность на хост в пределах бюджета базы данных, использовать короткоживущие транзакции/соединения, привязанные к запросу, оперативно фиксировать или откатывать транзакции, и избегать удержания соединений во время медленных внешних вызовов. Постоянные соединения PDO (PHP Data Objects) могут уменьшить накладные расходы на установку соединения, но могут закрепить множество простаивающих соединений за каждым 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.
Попробовать ответить на вопрос AI-тренеру

14Как бы вы обрабатывали реплики для чтения, разделение чтения/записи, задержку репликации и отказоустойчивость основной базы данных в PHP?

Обработка реплик для чтения и отказоустойчивости в PHP требует разделения трафика чтения и записи, смягчения задержки репликации и управления сбоями подключений во время отказа. На уровне приложения конфигурация базы данных или промежуточное ПО (middleware) направляет запросы на чтение (`SELECT`) к репликам для чтения, а запросы на запись (`INSERT`, `UPDATE`, `DELETE`) к основной инстанции (альтернативно управляется через прокси-серверы базы данных, такие как ProxySQL или AWS RDS Proxy). Поскольку репликация асинхронна, задержка репликации может вызывать устаревшие данные при чтении. Чтобы гарантировать согласованность 'read-your-writes' (чтение своих записей), приложение должно направлять чтение внутри активной транзакции к основной базе данных и временно привязывать последующие чтения к основной базе данных для конкретной пользовательской сессии или жизненного цикла запроса сразу после записи. Во время отказа основной базы данных современные кластеры баз данных повышают реплику до основной и обновляют конечные точки DNS (Domain Name System)/прокси. В PHP слой подключения должен перехватывать ошибки потери соединения и 'только для чтения', отбрасывать устаревшие сокеты PDO и применять контролируемые повторные попытки с экспоненциальной задержкой.

<?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',
    ],
];
Попробовать ответить на вопрос AI-тренеру

15Как миграции баз данных и изменения схемы развертываются безопасно с нулевым или минимальным временем простоя в PHP-приложениях?

Развертывание миграций баз данных с нулевым временем простоя в PHP-приложениях требует отделения эволюции схемы от развертывания кода приложения с использованием шаблона «Расширение и Сужение» (Expand and Contract, параллельное выполнение) наряду с неблокирующими методами онлайн DDL (язык определения данных). На фазе расширения (Expand) применяются обратно совместимые изменения схемы (например, добавление столбцов, допускающих NULL, новых таблиц или параллельных индексов с использованием таких инструментов, как pt-online-schema-change, gh-ost, или встроенных онлайн DDL в MySQL/PostgreSQL). Затем приложение развертывается для поддержки двойной записи (dual-writing) — записи как в старую, так и в новую структуры схемы, при этом чтение осуществляется из любой из них. Исторические записи затем асинхронно заполняются порционными фоновыми пакетами, чтобы предотвратить блокировку таблиц или задержку репликации. На фазе сужения (Contract), после того как согласованность данных подтверждена и чтение полностью переключено на новую структуру, развертывание кода удаляет ссылки на старую схему. Наконец, миграция для очистки удаляет устаревшие столбцы или таблицы. Безопасный откат требует, чтобы каждое промежуточное состояние схемы оставалось полностью совместимым как с предыдущей, так и с новой версиями приложения во время скользящих (rolling) релизов.

<?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
Попробовать ответить на вопрос AI-тренеру