ООП в PHP
Объектная модель PHP — это одиночное наследование реализации плюс диспетчеризация по имени в рантайме: вызывая $obj->pay(), движок ищет метод в таблице класса в момент вызова, а не сверяется с заранее построенной таблицей типов. Отсюда и гибкость языка, и все его ловушки: объект с нужным методом подойдёт куда угодно, даже если он не реализует ни одного интерфейса, а объявление типа-интерфейса нужно не чтобы вызов состоялся, а чтобы контракт проверялся на границе, а не проваливался где-то глубоко внутри.
Главный водораздел темы — когда разрешается имя. Часть механизмов работает на этапе компиляции класса: self:: намертво привязывается к классу, где написан код; use SomeTrait физически копирует тела методов и свойства в использующий класс (поэтому два trait с одноимённым методом — фатальная ошибка, и поэтому существуют insteadof и as); вариантность сигнатур проверяет движок при линковке класса. Другая часть — в рантайме: static:: (позднее статическое связывание) разрешается по классу, на котором реально сделан вызов; __get/__set срабатывают только для недоступных свойств и ценой того, что статический анализ и автодополнение IDE их не видят; instanceof обходит всю иерархию. Отдельно стоит запомнить версии: property hooks и асимметричная видимость — это PHP 8.4 (8.5 лишь распространил асимметричную видимость на static-свойства), а readonly запрещает переприсваивание даже изнутри класса — именно эту проблему решает функция clone() из PHP 8.5. Разбор по слоям — ниже.
Карта темы
- Видимость и асимметричная видимость — почему
privateпроверяется по классу, а не по объекту, чем он отличается отprotectedпри наследовании и что даётpublic private(set)из PHP 8.4. - Объявление свойств — типизированные свойства и состояние uninitialized, продвижение параметров конструктора и property hooks, срабатывающие при каждом обращении.
- Интерфейсы и абстрактные классы — контракт без состояния против частичной базы с состоянием, почему интерфейсов можно много, а родитель один, и как
instanceofобходит иерархию. - Trait и разрешение конфликтов — trait как копирование в класс на этапе компиляции, порядок приоритета «класс > trait > родитель» и разбор конфликта через
insteadofиas. - Статические члены и позднее статическое связывание — константа против
static-свойства, общий слот у наследников и главное различие темы —self::противstatic::. - Магические методы — что значит «недоступное свойство», почему
unset()открывает дорогу в__getи чем магия расплачивается за удобство. - readonly и неизменяемость — запись ровно один раз из области объявившего класса, поверхностность заморозки и функция
clone()из PHP 8.5. - Перечисления — чистый и backed enum,
nameпротивvalue,from()/tryFrom()на границе и почему случаи — синглтоны. - Паттерны и полиморфизм — duck typing против явного контракта, fluent-цепочка на
staticи Singleton с закрытыми чёрными ходамиcloneиunserialize.
Частые ошибки и ловушки
| Ошибка | Последствие |
|---|---|
Считать, что private закрывает доступ из другого экземпляра того же класса | Видимость проверяется по классу, а не по объекту — $other->amount внутри метода работает |
Путать private(set) с readonly | Асимметричная видимость ограничивает, кто пишет, а не сколько раз — класс может писать многократно |
| Относить property hooks и асимметричную видимость к PHP 8.5 | Обе появились в 8.4; 8.5 лишь расширил асимметричную видимость на static-свойства |
Считать trait вторым extends с наследованием в рантайме | trait копируется в класс на этапе компиляции; он не тип, и instanceof его не находит |
| Ждать, что метод trait переопределит собственный метод класса | Приоритет: собственный метод класса > метод trait > унаследованный — класс всегда побеждает |
| Класть два trait с одноимённым методом и ждать, что PHP выберет сам | Фатальная ошибка на этапе компиляции, пока конфликт не разобран через insteadof / as |
Писать new self() в родительской фабрике | self:: связан на этапе компиляции с родителем — Child::create() вернёт родителя, а не Child |
Ждать, что у наследника будет своя копия унаследованного static-свойства | Это один слот на всю иерархию: Child::$count++ меняет и Base::$count |
Читать константу класса как $this->NAME | $this->NAME ищет свойство; константа читается как self::NAME / Foo::NAME |
Считать, что __get срабатывает при каждом чтении свойства | Только для недоступного — необъявленного, снятого unset() или закрытого видимостью |
Забывать, что isset() на виртуальном свойстве требует __isset | isset($obj->virtual) вернёт false, хотя __get отдаёт значение |
Ждать от readonly глубокой заморозки | Заморожена ссылка: объект по ней по-прежнему изменяем |
Верить в синтаксис clone $obj with { ... } | Такого синтаксиса не существует ни в одной версии PHP — в 8.5 clone стал функцией |
Ждать value у случая чистого enum | value есть только у backed enum (enum Status: string); у чистого — только name |
Путать from() и tryFrom() | from() бросает ValueError на неизвестном скаляре, tryFrom() возвращает null |
Указать self вместо static возвращаемым типом fluent-метода | Цепочка ломается в наследнике: следующий вызов идёт по типу родителя |
| Защитить Singleton только приватным конструктором | Остаются два чёрных хода: clone $instance и unserialize(), который не зовёт конструктор |
Значение для собеседований
ООП в PHP — это раздел, где джуна отделяют от middle одним вопросом: «чем self:: отличается от static::?». Правильный ответ — не «одно новее другого», а «одно связывается на этапе компиляции с классом, где написан код, другое разрешается в рантайме по классу вызова». Тот же приём применяют ко всей теме: интервьюер проверяет не знание слов, а модель исполнения — что физически происходит с классом, когда в него подмешивают trait, что именно движок считает «недоступным» свойством и в какой ровно момент readonly-полю разрешено получить значение.
Что обычно проверяют:
- Разницу
privateиprotectedпри наследовании и то, что видимость — свойство класса, а не объекта. - Что делает продвижение свойств в конструкторе и чем property hooks отличаются от
__get. - Когда брать интерфейс, а когда абстрактный класс, и почему интерфейсов может быть много.
- Что такое trait на самом деле и как разрешается конфликт двух одноимённых методов.
self::противstatic::и что вернётnew static()в родительской фабрике.- Условия срабатывания
__get/__callи чем магия расплачивается. - Гарантии
readonly, его поверхностность и как в 8.5 копируют объект с изменённым полем. - Чистый и backed enum,
from()противtryFrom(). - Fluent-цепочку и защиту Singleton от
cloneиunserialize.
Типичный неверный ответ: «trait — это как интерфейс, только с реализацией». Он и приводит кандидата к следующей ошибке: раз это интерфейс, то instanceof должен его находить, а конфликт имён движок как-нибудь разрулит. На деле trait — копирование в класс на этапе компиляции: его члены становятся собственными членами класса, instanceof о нём не знает, а два одинаковых метода дают фатальную ошибку. Второй классический провал — обещание неизменяемости через readonly с попыткой сделать «withX()» через clone и присваивание: свойство после клонирования остаётся инициализированным, и присваивание бросает Error. Ровно ради этого сценария в PHP 8.5 появилась функция clone($this, ['statusCode' => $code]).