15 отобранных вопросов для собеседования по бэкенду на PHP, сгруппированных по уровням квалификации. Используйте их для повторения основ, понимания практических компромиссов и рассуждений на уровне Senior-разработчика о производственных системах.
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, поэтому старые примеры приведения нужно рассматривать с указанием версии.
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 доступны внутри объявившего их класса; подкласс не может использовать закрытый член родителя как унаследованный 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
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
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);
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
}
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;
}
10Как PHP обрабатывает переполнение целочисленных значений, точность чисел с плавающей запятой и точные десятичные вычисления?
Целочисленные значения в PHP имеют размерность, зависящую от платформы, с ограничениями, которые раскрываются константами, такими как `PHP_INT_MAX`. PHP не сохраняет автоматически результаты нативных целочисленных операций как целые числа произвольной точности после переполнения; переполняющая целочисленная арифметика может стать `float` или иным образом потерять точную целочисленную семантику. Числа с плавающей запятой в PHP являются двоичными значениями с плавающей запятой, поэтому многие десятичные дроби, такие как `0.1`, являются приближенными. Для точных десятичных или денежных вычислений избегайте использования чисел с плавающей запятой и применяйте точный подход, например, целочисленные минорные единицы, где это уместно, или инструменты для работы с десятичными числами фиксированной/произвольной точности, такие как BCMath. GMP полезен для целочисленной арифметики произвольной точности.
11Объясните ссылки PHP (`&`), механизм «копирование при записи» (copy-on-write) и как ссылки могут создавать неожиданные побочные эффекты.
Переменные PHP внутренне представлены как zval. Для значений, таких как массивы и строки, PHP использует механизм «копирование при записи» (copy-on-write): присваивание `$b = $a` обычно приводит к совместному использованию одного и того же базового значения до тех пор, пока одна из переменных не будет изменена, после чего PHP разделяет/копирует её. Ссылка, созданная с помощью `&`, делает переменные псевдонимами для одного и того же контейнера, поэтому изменение одной переменной изменяет другую и может обойти ожидаемое поведение разделения, которое люди ожидают от обычного присваивания. Передача по ссылке позволяет функции изменять переменную вызывающей стороны. Ссылки могут вызывать удивительные побочные эффекты, особенно при использовании `foreach ($array as &$value)`: переменная цикла остаётся ссылкой на последний элемент после завершения цикла, поэтому её повторное использование позже может случайно перезаписать этот элемент, если вы не сделаете `unset($value)`.
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;
13Как бы вы устраняли исчерпание пула соединений базы данных и проектировали управление соединениями для PHP-FPM (FastCGI Process Manager) и долгоживущих воркеров?
Для устранения исчерпания пула соединений я бы сравнил лимиты соединений базы данных и текущие сессии с общей конкурентностью PHP: каждый активный дочерний процесс PHP-FPM может удерживать собственное соединение с базой данных, а несколько пулов приложений, хостов, cron-заданий, воркеров очередей, миграций и развертываний увеличивают нагрузку. Я бы просмотрел представления сессий базы данных, состояния соединений, сессии "idle-in-transaction", медленные запросы, ожидания блокировок, логи ошибок, метрики пула и недавние изменения трафика или количества воркеров. Для PHP-FPM необходимо поддерживать `max_children` и конкурентность на хост в пределах бюджета базы данных, использовать короткоживущие транзакции/соединения, привязанные к запросу, оперативно фиксировать или откатывать транзакции, и избегать удержания соединений во время медленных внешних вызовов. Постоянные соединения PDO (PHP Data Objects) могут уменьшить накладные расходы на установку соединения, но могут закрепить множество простаивающих соединений за каждым 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 работу с репликами для чтения, разделение чтения и записи, задержку репликации и переключение основной БД при отказе?
Разделяйте соединения с основной БД и репликами по смыслу операции, а не только по наличию SELECT в начале SQL: блокирующие чтения, записи и транзакции, требующие согласованности с основной БД, должны выполняться на ней. Транзакции только для чтения могут использовать подходящую реплику, если это допускают требования к согласованности. Асинхронная репликация может отставать. Настройка sticky в Laravel после записи направляет последующие чтения текущего запроса на соединение записи; фиксированный период привязки между запросами лишь снижает риск, но не даёт безусловной гарантии чтения собственных записей. Для гарантии читайте с основной БД либо дожидайтесь применения репликой требуемой позиции записи, задавая тайм-аут и переход к основной БД. Поведение прокси различается: RDS Proxy использует отдельные точки подключения для чтения и записи, а не универсальное автоматическое разделение отдельных SQL-операторов. При отказе отбрасывайте устаревшие соединения и переподключайтесь с ограниченными задержками повторов. Потеря ответа на commit оставляет исход неопределённым: не повторяйте вслепую неидемпотентную запись; используйте ключи идемпотентности или сначала выясните результат.
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