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

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

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

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

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

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

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

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

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

Трейты обеспечивают горизонтальное повторное использование кода: класс импортирует их члены через use, а создать экземпляр самого трейта нельзя. Метод, объявленный в классе, имеет приоритет над методами трейтов; методы трейтов — над унаследованными методами родителя. Если несколько трейтов предоставляют один метод и конфликт не разрешён собственным методом класса, insteadof позволяет выбрать реализацию. Через as методу трейта можно дать дополнительное имя и при необходимости другую видимость. Псевдоним не переименовывает и не удаляет исходный метод. Разрешение конфликтов и поддержание понятного публичного API — отдельные задачи.

<?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, чем различаются чистые и перечисления со скалярным значением и как они помогают моделировать предметную область?

Перечисление задаёт закрытый набор именованных вариантов, например статусы заказа. Чистое перечисление содержит варианты без связанного скалярного значения. У перечисления со значениями (backed enum) каждый вариант имеет уникальное значение int или string, удобное для хранения и обмена. Перечисления заменяют произвольные строки и числа типобезопасными вариантами. Они могут иметь методы и реализовывать интерфейсы, cases() перечисляет варианты, а у backed enum предусмотрено преобразование между вариантом и его значением.

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Как скалярные типы, nullable, объединения, пересечения и DNF выражают контракты PHP API и когда применять каждый вариант?

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

9Чем в PHP различаются callable, Closure, вызываемые значения первого класса, вызываемые объекты и массивы-ссылки на методы?

callable описывает вызываемые значения: строки с именами функций, объекты Closure, ссылки на доступные методы и объекты с __invoke. Closure — конкретный вызываемый объект: его источником может быть анонимная функция либо именованная функция или метод, преобразованные в Closure. Синтаксис вызываемых значений первого класса, например strlen(...) или $object->method(...), создаёт Closure без вызова целевой функции. Массив [$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;
}
Попробовать ответить на вопрос AI-тренеру

10Как PHP обрабатывает переполнение целых, точность чисел с плавающей запятой и точные десятичные расчёты?

Размер int зависит от платформы; верхняя граница доступна как PHP_INT_MAX. При переполнении результат может стать float и потерять точность; PHP не переходит автоматически на целые произвольной точности. Двоичные float приближённо представляют многие десятичные дроби, например 0.1. Для денег и точных десятичных расчётов используйте целые в минимальных единицах, когда это подходит, или BCMath. GMP подходит для целых произвольной точности.

var_dump(0.1 + 0.2 === 0.3);
printf('%.17f\n', 0.1 + 0.2);
Попробовать ответить на вопрос AI-тренеру

11Как работают ссылки PHP (&) и копирование при записи и откуда берутся неожиданные побочные эффекты?

Внутренне значения представлены zval. При обычном $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Как в PHP работают идентификаторы объектов, присваивание, clone и __clone() и чем различаются поверхностная и глубокая копии?

$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 и долгоживущих обработчиков?

Сравните лимит базы и число сессий с совокупным параллелизмом FPM, хостов, очередей, планировщика и развёртываний. Посмотрите состояния сессий, незавершённые транзакции, запросы, блокировки, журналы и ожидание пула. Настройте pm.max_children по общему бюджету, своевременно фиксируйте или откатывайте короткие транзакции и не удерживайте соединение на медленных внешних вызовах. Постоянный PDO уменьшает стоимость подключения, но может удерживать сессии и состояние. Совместимый прокси, например PgBouncer, может обслуживать клиентов меньшим числом серверных соединений. Для долгоживущих процессов явно задайте проверку, переподключение после простоя, перезапуска или переключения базы, сброс состояния и безопасные повторы. После fork создавайте отдельные соединения.

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

14Как организовать в PHP работу с репликами для чтения, разделение чтения и записи, задержку репликации и переключение основной БД при отказе?

Разделяйте соединения с основной БД и репликами по смыслу операции, а не только по наличию SELECT в начале SQL: блокирующие чтения, записи и транзакции, требующие согласованности с основной БД, должны выполняться на ней. Транзакции только для чтения могут использовать подходящую реплику, если это допускают требования к согласованности. Асинхронная репликация может отставать. Настройка 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',
    ],
];
Попробовать ответить на вопрос AI-тренеру

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

Применяйте расширение с последующим сокращением схемы, совместимое со старой и новой версиями приложения во время постепенного развёртывания. Сначала добавьте совместимые структуры, затем код, способный работать с обоими представлениями. Если нужна двойная запись, обеспечьте её согласованность и возобновляемое заполнение старых данных с ограничением нагрузки, не перезаписывающее более свежие записи. Проверьте перенесённые данные и переключите чтение до удаления старых ветвей кода. Онлайн-изменения DDL снижают отдельные риски блокировок, но не гарантируют полного их отсутствия: учитывайте блокировки конкретной БД и версии, ограничения транзакций, ресурсы и влияние на репликацию. Например, PostgreSQL CREATE INDEX CONCURRENTLY имеет ограничения и после сбоя может оставить невалидный индекс. Удаляйте старые столбцы только после остановки старых экземпляров приложения и окончания необходимого окна отката. После разрушительного сокращения схемы откат к коду, которому нужны удалённые данные, уже не является автоматически безопасным.

<?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-тренеру

Собеседования на телефоне: PHP

Отвечайте голосом и получайте обратную связь от ИИ в приложении для Android.

Скачать EngineerSpeak в Google Play
Сканируйте для скачивания