ООП в PHP
Объектно-ориентированный PHP — интерфейсы против абстрактных классов, трейты и разрешение конфликтов имён, магические методы, области видимости и позднее статическое связывание, продвижение свойств и readonly, перечисления, статические члены и паттерны, которые просят написать на собеседовании.
22 вопросов
JuniorТеорияОчень частоЧто проверяет instanceof и как он обходится с интерфейсами и родителями?
Что проверяет instanceof и как он обходится с интерфейсами и родителями?
$x instanceof Foo истинно, когда $x — объект класса Foo, его наследника или класса, реализующего интерфейс Foo: проверка идёт по всей иерархии, а не только по точному классу. На не-объекте возвращается false, а не ошибка.
Типичные ошибки
- ✗Считать, что
instanceofсопоставляет только точный класс, а не наследников и интерфейсы - ✗Ждать ошибки вместо
false, когда слева не объект - ✗Применять
instanceofк скалярному типу вродеstringилиint
Уточняющие вопросы
- →Чем
instanceofотличается отget_class($x) === Foo::class? - →Почему длинная цепочка проверок
instanceofобычно признак плохого дизайна?
JuniorТеорияОчень частоВ чём разница между абстрактным классом и интерфейсом в PHP?
В чём разница между абстрактным классом и интерфейсом в PHP?
Интерфейс — чистый контракт: сигнатуры методов и константы, без состояния, и класс может реализовать сколько угодно интерфейсов сразу. Абстрактный класс — частичная база со свойствами, конструктором и готовыми методами, но наследуется только один.
Типичные ошибки
- ✗Считать, что интерфейс может держать свойства или конструктор и нести состояние
- ✗Полагать, что PHP разрешает наследовать больше одного абстрактного класса
- ✗Объявлять интерфейс там, где цель — разделить готовую реализацию
Уточняющие вопросы
- →Когда интерфейс с trait-реализацией по умолчанию лучше абстрактного класса?
- →Может ли абстрактный класс реализовать интерфейс, не определив все его методы?
JuniorТеорияОчень частоЧем private отличается от protected при наследовании класса?
Чем private отличается от protected при наследовании класса?
protected виден в объявившем классе и во всех производных классах. private виден только внутри объявившего класса, поэтому производный класс не может обратиться к private-свойству родителя и может объявить своё с тем же именем.
Типичные ошибки
- ✗Считать, что производный класс может обратиться к
private-членам родителя - ✗Думать, что
privateзакрывает доступ из другого экземпляра того же класса (это не так) - ✗Полагать, что переобъявленный
privateв наследнике конфликтует с родительским
Уточняющие вопросы
- →Почему один экземпляр может читать
private-свойство другого внутри того же класса? - →Что означает видимость
privateдля метода, разрешаемого поздним статическим связыванием?
JuniorТеорияОчень частоВ чём разница между static-методом и методом экземпляра?
В чём разница между static-методом и методом экземпляра?
static-метод принадлежит классу, вызывается как Foo::bar() без объекта, не имеет $this и работает только со статическими членами. Метод экземпляра вызывается на объекте, получает $this и читает свойства этого объекта.
Типичные ошибки
- ✗Использовать
$thisвнутриstatic-метода - ✗Считать, что
staticпро кэширование или скорость, а не про область видимости - ✗Вызывать метод экземпляра статически и ждать, что PHP создаст объект
Уточняющие вопросы
- →Почему статические вызовы усложняют модульное тестирование по сравнению с внедрёнными зависимостями?
- →Во что разрешается
static::там, гдеself::этого не делает?
JuniorТеорияЧастоКогда стоит использовать константу класса вместо static-свойства?
Когда стоит использовать константу класса вместо static-свойства?
Константа класса неизменна, её нельзя переприсвоить, читается как Foo::NAME. static-свойство — единственный изменяемый слот, общий для всех экземпляров, читается как Foo::$name. Константа — для неизменного значения, static-свойство — разделяемое изменяемое состояние.
Типичные ошибки
- ✗Считать, что константу класса можно переприсвоить в рантайме
- ✗Ждать, что каждый производный класс получит свою копию унаследованного
static-свойства - ✗Читать константу класса через
$this->NAMEвместоself::NAME
Уточняющие вопросы
- →Что произойдёт с
Parent::$count, если наследник пишет вChild::$count, не переобъявив его? - →Как пометка константы класса как
finalменяет возможности наследника?
JuniorТеорияЧастоЧто делает продвижение свойств в конструкторе (property promotion) в PHP 8?
Что делает продвижение свойств в конструкторе (property promotion) в PHP 8?
Модификатор видимости у параметра конструктора — __construct(private Logger $log) — объявляет свойство и присваивает ему аргумент за один шаг, убирая связку из объявления и $this->log = $log. Получается обычное типизированное свойство.
Типичные ошибки
- ✗Считать, что продвинутому параметру всё ещё нужно
$this->log = $logв теле конструктора - ✗Полагать, что продвижение работает только с видимостью
public - ✗Ждать, что продвинутое свойство окажется
staticили общим для экземпляров
Уточняющие вопросы
- →Может ли продвинутое свойство быть ещё и
readonly, и что тогда гарантируется? - →Почему продвижение нельзя применить в методе, отличном от
__construct?
JuniorТеорияЧастоЧем backed enum отличается от чистого enum в PHP?
Чем backed enum отличается от чистого enum в PHP?
Случаи чистого enum — синглтоны, у которых есть только name. backed enum объявляет скалярный тип — enum Status: string — поэтому у случая есть ещё и value, а также from()/tryFrom() для обратного сопоставления. Backed нужен, когда значение сохраняется.
Типичные ошибки
- ✗Ждать, что у случая чистого enum есть
value - ✗Путать
from()(кидает на неизвестном скаляре) иtryFrom()(возвращаетnull) - ✗Пытаться создать случай enum через
new
Уточняющие вопросы
- →Как реализация интерфейса перечислением меняет возможности его случаев?
- →Где хранить человекочитаемую подпись, если у случая есть только
nameиvalue?
JuniorТеорияЧастоПочему класс в PHP реализует много интерфейсов, но наследует только один класс?
Почему класс в PHP реализует много интерфейсов, но наследует только один класс?
В PHP одиночное наследование реализации: класс наследует один родительский класс, поэтому два родителя не могут дать конфликтующие тела методов. Интерфейсы не несут ни тел, ни состояния, поэтому implements B, C лишь объединяет контракты и не конфликтует.
Типичные ошибки
- ✗Считать, что
extends A, B— валидный PHP - ✗Думать, что два интерфейса с одинаковой совместимой сигнатурой конфликтуют
- ✗Брать trait как способ получить второй родительский класс
Уточняющие вопросы
- →Что будет, если два реализуемых интерфейса объявят одноимённый метод с разными сигнатурами?
- →Как trait даёт переиспользование кода, не добавляя второго родительского класса?
JuniorТеорияЧастоЧто гарантирует readonly-свойство после конструирования объекта?
Что гарантирует readonly-свойство после конструирования объекта?
readonly-свойство с типом можно записать ровно один раз изнутри объявившего класса — обычно в конструкторе. Любая последующая запись кидает Error, даже из самого класса. Ограничение поверхностное: ссылка заморожена, но объект по ней изменяем.
Типичные ошибки
- ✗Ждать, что
readonlyглубоко заморозит объект, на который указывает свойство - ✗Считать, что объявивший класс всё ещё может перезаписать свойство после конструирования
- ✗Пытаться инициализировать
readonly-свойство вне области объявившего класса
Уточняющие вопросы
- →Почему
readonly-свойство нельзя инициализировать из конструктора наследника? - →Как получить изменённую копию объекта, все свойства которого
readonly?
JuniorТеорияЧастоКакую задачу решают trait в PHP при одиночном наследовании классов?
Какую задачу решают trait в PHP при одиночном наследовании классов?
trait — набор методов и свойств, копируемый в класс на этапе компиляции, чтобы несвязанные классы делили реализацию без общего родителя. Это горизонтальное переиспользование: trait не является типом, не инстанцируется, и instanceof его не находит.
Типичные ошибки
- ✗Считать trait типом —
instanceofникогда его не находит - ✗Ждать, что метод trait переопределит собственный метод класса (класс всегда побеждает)
- ✗Воспринимать
useкак второйextendsс наследованием в рантайме
Уточняющие вопросы
- →В каком порядке приоритета разрешаются собственный метод класса, метод trait и метод родителя?
- →Что произойдёт, если два trait в одном классе объявят одноимённое свойство?
MiddleТеорияЧастоЧем static:: отличается от self:: внутри унаследованного метода?
Чем static:: отличается от self:: внутри унаследованного метода?
self:: связывается на этапе компиляции с классом, где написан код, поэтому унаследованный метод всегда попадает в родительскую версию. static:: — позднее статическое связывание: имя разрешается в рантайме по классу вызова, поэтому new static() в родителе при Child::create() создаёт Child.
Типичные ошибки
- ✗Использовать
new self()в родительской фабрике и удивляться, чтоChildне возвращается - ✗Считать, что
static::требует, чтобы окружающий метод был объявленstatic - ✗Ждать, что
self::последует за классом, на котором реально сделан вызов
Уточняющие вопросы
- →Что вернёт
get_called_class()внутри родительского метода, вызванного какChild::run()? - →Как позднее статическое связывание делает возможной наследуемую статическую фабрику?
MiddleТеорияЧастоКак приём структурной типизации duck typing соотносится с полиморфизмом через интерфейс?
Как приём структурной типизации duck typing соотносится с полиморфизмом через интерфейс?
PHP разрешает вызов метода в рантайме, поэтому $x->pay() работает на любом объекте, у которого этот метод просто есть, — это и есть duck typing, и полиморфизму большего не нужно. Объявление параметра интерфейсом делает контракт явным и проверяемым на границе, превращая глубинный Error в TypeError у самой двери.
Типичные ошибки
- ✗Считать, что объект обязан реализовать интерфейс, прежде чем его метод вообще можно вызвать
- ✗Воспринимать объявление типа-интерфейса как документацию, которую движок не проверяет
- ✗Ждать, что вызов отсутствующего метода вернёт
null, а не броситError
Уточняющие вопросы
- →Что именно бросает PHP при вызове метода, которого у объекта нет?
- →Когда явный интерфейс стоит добавить вместо опоры на форму объекта?
JuniorКодИногдаСоберите fluent-сборщик запросов с цепочкой вызовов
Соберите fluent-сборщик запросов с цепочкой вызовов
Каждый метод цепочки меняет состояние сборщика и возвращает $this с типом static, чтобы наследник тоже продолжал цепочку, — именно это позволяет связывать вызовы. toSql() — терминальный метод: он возвращает собранную строку, а не сборщик, и цепочка на нём заканчивается.
Типичные ошибки
- ✗Забыть
return $this, из-за чего следующий вызов цепочки приходит наnull - ✗Указать тип возврата
selfвместоstatic, что ломает цепочку в наследнике - ✗Заставить терминальный
toSql()вернуть сборщик вместо собранной строки
Уточняющие вопросы
- →Почему тип возврата
staticсохраняет цепочку в наследнике там, гдеselfеё ломает? - →Как сделать сборщик неизменяемым, чтобы каждый вызов возвращал изменённую копию?
JuniorТеорияИногдаЧто такое магические методы PHP и когда движок вызывает __get или __call?
Что такое магические методы PHP и когда движок вызывает __get или __call?
Магические методы — хуки с префиксом __, которые движок вызывает сам. __get/__set срабатывают, только если свойство не объявлено или недоступно из вызывающей области; __call — только если метода нет. Есть также __toString, __invoke и __clone.
Типичные ошибки
- ✗Считать, что
__getвыполняется при каждом чтении свойства, а не только недоступного или необъявленного - ✗Ждать, что
__callперехватит методы, которые на классе реально есть - ✗Забывать, что
isset()на виртуальном свойстве требует__isset, а не__get
Уточняющие вопросы
- →Почему объявленное, но
privateсвойство всё равно запускает__getпри чтении снаружи класса? - →Что даёт
__callStaticсверх__call?
MiddleТеорияИногдаКогда срабатывает __get и почему объявленные свойства лучше него?
Когда срабатывает __get и почему объявленные свойства лучше него?
__get срабатывает, только если свойство не объявлено, снято через unset или недоступно из вызывающей области: объявленное и инициализированное public-свойство до него не доходит. Магические свойства не видны статическому анализу и IDE, стоят лишний вызов метода и требуют __isset.
Типичные ошибки
- ✗Считать, что
__getвыполняется при каждом чтении свойства, включая объявленные public - ✗Забывать, что
isset()на магическом свойстве требует__isset - ✗Не знать, что
unset()объявленного свойства направляет последующие чтения в__get
Уточняющие вопросы
- →Почему после
unset($obj->declared)последующие чтения этого свойства идут через__get? - →Как property hooks из PHP 8.4 закрывают тот же сценарий без
__get?
MiddleКодИногдаЗащитите порождающий паттерн Singleton от размножения через clone и unserialize
Защитите порождающий паттерн Singleton от размножения через clone и unserialize
Сделайте __construct приватным, чтобы закрыть new, держите экземпляр в private static-слоте и выдавайте его из getInstance(). Затем закройте два чёрных хода: приватный __clone() делает clone фатальным, а __wakeup() обязан бросать: unserialize() собирает объект без конструктора.
Типичные ошибки
- ✗Закрыть
newприватным конструктором, оставивclone $instanceполностью открытым - ✗Забыть, что
unserialize()собирает объект, вообще не вызывая__construct - ✗Считать, что клонирование запрещает именно
finalна классе
Уточняющие вопросы
- →Почему
unserialize()обходит приватный конструктор и что с этим делает__wakeup()? - →Почему синглтон трудно тестировать и что бы вы внедряли вместо него?
MiddleТеорияИногдаКак разрешить конфликт двух trait с одноимённым методом?
Как разрешить конфликт двух trait с одноимённым методом?
Два trait с одноимённым методом — фатальная ошибка, пока конфликт не разрешён в блоке use: insteadof выбирает, чей метод побеждает, а as даёт проигравшему псевдоним, чтобы он остался доступен. as также меняет видимость импортируемого метода.
Типичные ошибки
- ✗Считать, что PHP молча выберет один из двух конфликтующих методов trait
- ✗Думать, что
asвыбирает победителя, аinsteadofпереименовывает (всё наоборот) - ✗Забывать, что
asумеет ещё и менять видимость импортируемого метода
Уточняющие вопросы
- →Что произойдёт, если сам класс объявит метод, который объявлен и в используемом trait?
- →Чем конфликт свойств trait отличается от конфликта методов trait?
MiddleТеорияРедкоЧто даёт свойству асимметричная видимость вида public private(set)?
Что даёт свойству асимметричная видимость вида public private(set)?
Она разделяет видимость чтения и записи: public private(set) int $total читается откуда угодно, но пишется лишь изнутри класса. В отличие от readonly, класс может менять его дальше — ограничен тот, КТО пишет, а не сколько раз. Появилась в PHP 8.4; 8.5 расширил её на static-свойства.
Типичные ошибки
- ✗Путать
private(set)сreadonly— объявивший класс всё ещё может писать в свойство многократно - ✗Читать
public private(set)как ограничение чтения, а не записи - ✗Считать, что асимметричная видимость появилась в PHP 8.5, а не в 8.4
Уточняющие вопросы
- →Когда стоит выбрать
private(set)вместо того, чтобы сделать свойствоreadonly? - →Что
protected(set)разрешает производному классу из того, что запрещаетprivate(set)?
MiddleТеорияРедкоКак в PHP 8.5 скопировать readonly-объект с одним изменённым полем?
Как в PHP 8.5 скопировать readonly-объект с одним изменённым полем?
PHP 8.5 сделал clone ещё и функцией: clone(object $object, array $withProperties = []): object. Вызванная изнутри класса как clone($this, ['statusCode' => $code]), она копирует объект и применяет эти свойства в ходе клонирования — единственный момент, когда readonly-поле переустанавливают.
Типичные ошибки
- ✗Верить в синтаксис
clone $obj with { ... }—cloneстал функцией, а не ключевым словом с секциейwith - ✗Присваивать
readonly-свойству на свежесклонированном объекте снаружи объявившего класса - ✗Ждать, что
readonlyможно переприсвоить изнутри класса в любой момент, а не только при клонировании
Уточняющие вопросы
- →Почему
clone($this, [...])вызывают изнутри класса, а не из кода, держащего объект? - →Во что обходилась изменённая копия
readonly-объекта-значения до появления этого?
MiddleТеорияРедкоЧто меняют property hooks, добавленные в PHP 8.4, в объявленном свойстве?
Что меняют property hooks, добавленные в PHP 8.4, в объявленном свойстве?
Hook привязывает логику get/set к объявленному свойству: чтение $obj->total выполняет код, а член остаётся обычным типизированным свойством. В отличие от __get, свойство реально существует и видно статическому анализу, а hook срабатывает при каждом обращении, а не только при недоступном.
Типичные ошибки
- ✗Считать свойство с hook виртуальным и необъявленным, как свойство через
__get - ✗Ждать, что hook срабатывает, только когда свойство недоступно или снято
- ✗Относить property hooks к PHP 8.5, а не к 8.4, где они появились
Уточняющие вопросы
- →Почему свойство с hook остаётся понятным статическим анализаторам там, где свойство через
__get— нет? - →Может ли свойство с hook участвовать в продвижении конструктора и что получает
set-hook?
SeniorДизайнРедкоВам достаётся PHP-приложение на 60 тысяч строк, написанное процедурно: каталог include-файлов, глобальные функции, суперглобальные массивы читаются напрямую, SQL собирается строками по месту. Тестов нет, код работает в проде, а на переписывание бизнес денег не даст. Задача — сделать его тестируемым и постепенно, без заморозки функций, вести к объектно-ориентированной архитектуре с внедрением зависимостей. Опишите подход: с чего начнёте, как прорежете швы в процедурном коде, как введёте классы и интерфейсы, не сломав существующие места вызова, как запустите первые тесты на коде, который сейчас не тестируется, и что не даст миграции застрять на середине.
Вам достаётся PHP-приложение на 60 тысяч строк, написанное процедурно: каталог include-файлов, глобальные функции, суперглобальные массивы читаются напрямую, SQL собирается строками по месту. Тестов нет, код работает в проде, а на переписывание бизнес денег не даст. Задача — сделать его тестируемым и постепенно, без заморозки функций, вести к объектно-ориентированной архитектуре с внедрением зависимостей. Опишите подход: с чего начнёте, как прорежете швы в процедурном коде, как введёте классы и интерфейсы, не сломав существующие места вызова, как запустите первые тесты на коде, который сейчас не тестируется, и что не даст миграции застрять на середине.
Сначала зафиксируйте поведение: характеризующие тесты через HTTP-вход, чтобы любая поломка была видна. Затем прорежьте швы — уберите каждую глобальную функцию и каждое чтение суперглобального массива за тонкий класс с интерфейсом, а старую функцию оставьте однострочным адаптером, чтобы существующие вызовы работали. Двигайтесь от листьев: выделили сервис, внедрили зависимости, удалили глобальную.
Типичные ошибки
- ✗Предлагать переписывание одним махом за заморозкой, которую бизнес никогда не оплатит
- ✗Превратить глобальные функции в
static-методы и назвать это внедрением зависимостей - ✗Писать тесты только после рефакторинга, так что поведение оригинала ничем не зафиксировано
Уточняющие вопросы
- →Как протестировать функцию, читающую
$_SESSIONнапрямую, не трогая места её вызова? - →Какое правило в CI не даст процедурной поверхности расти, пока идёт миграция?