Перейти к основному содержимому

Объектно-ориентированное программирование в PHP

Разработчику Архитектору
Сначала — общие понятия (раздел 4 "Код")

Если ООП для вас новое или вы учите PHP с нуля, сначала пройдите материалы без привязки к синтаксису: парадигмы и уровни абстракции, затем ООП — о разделезачем объекты, введение, абстракция, инкапсуляция, наследование, полиморфизм.

Ниже — как это устроено в PHP.

Теория и синтаксис PHP

Понятие ООПКак выражено в PHPАналог в Java
Класс, АДТ (алгебраический тип данных)class, readonly class (8.2+)class, record
Объект / экземплярnew ClassName()new ClassName()
Конструктор__construct(); promotion свойств (8+)конструктор класса
Свойство$property с типом (7.4+)поле класса
Инкапсуляцияpublic / protected / privateте же модификаторы
Наследование классаextends (один родитель)extends
Реализация контрактаimplements (несколько интерфейсов)implements
Горизонтальное переиспользованиеtraitнет (интерфейсы + default)
Абстракцияinterface, abstract classто же
Полиморфизм подтиповпереопределение, тип параметра@Override, ссылки на супертип
Перегрузканет перегрузки методов по сигнатуреперегрузка разрешена
Сообщения->; магические __call, __getвызовы методов; reflection
Статикаstaticstatic
Копированиеclone, __clone()clone(), copy constructors
Передача объектовпо ссылке (один экземпляр)по ссылке на объект

Определения — раздел 4-08-oop. Синтаксис JVM-языка для сопоставления — ООП в Java. Обзор PHP — раздел PHP.

Кратко для новичка:

  • Классclass User { }; new User() вызывает __construct.
  • public / private / protected — кто видит поле или метод.
  • Наследованиеextends; parent:: — вызов метода родителя.
  • Интерфейс и trait — контракт и переиспользуемый код без второго родителя.
  • Полиморфизм — тип-hint Shape принимает Circle и Rectangle в одной функции.

Создание экземпляра и присваивание переменным

new и __construct

При выполнении $obj = new User("Пётр") PHP:

  1. Выделяет память под объект.
  2. Вызывает __construct() (если определён) или конструктор по умолчанию.
  3. Возвращает ссылку на экземпляр.
class User {
public function __construct(
public string $name,
private string $email = '',
) {}
}

$user = new User('Пётр', 'p@example.com');
$alias = $user; // та же ссылка
$alias->name = 'Петр'; // изменится и $user->name
$other = new User('Анна'); // другой объект

Разбор:

  • Constructor property promotion (PHP 8+) объявляет свойство и параметр в одной строке — аналог compact-конструктора в Java record, но объект остаётся изменяемым (если не readonly).
  • $alias = $user не клонирует объект — обе переменные указывают на один экземпляр.
  • Для независимой копии — clone $user (см. раздел Клонирование объектов ниже).

Цепочка вызова родительского конструктора

class Employee extends User {
public function __construct(
string $name,
public string $department,
) {
parent::__construct($name); // обязательно явно, если свой __construct
}
}

Разбор:

  • В отличие от C#/Kotlin, PHP не вызывает parent::__construct() автоматически, если в дочернем классе есть свой конструктор.
  • Пропуск parent::__construct() оставляет поля родителя неинициализированными.

Фабрики и статические конструкторы

class Money {
private function __construct(
public readonly int $amountCents,
public readonly string $currency,
) {}

public static function rub(int $rubles): self {
return new self($rubles * 100, 'RUB');
}
}

$price = Money::rub(150);

Разбор:

  • private __construct + static фабрика — идиома, когда нужны именованные способы создания (rub, usd).
  • В Java аналог — приватный конструктор + static Money rub(int).

Модификаторы доступа

МодификаторКлассНаследникВнешний кодЗаметка
publicдададаAPI объекта
protectedдаданетдля расширения подклассами
privateдада*нет*доступ к private другого объекта того же класса разрешён
class BankAccount {
private float $balance = 0.0;

public function deposit(float $amount): void {
if ($amount > 0) {
$this->balance += $amount;
}
}

public function transferTo(BankAccount $other, float $amount): void {
if ($amount > 0 && $amount <= $this->balance) {
$this->balance -= $amount;
$other->balance += $amount; // OK: тот же класс
}
}

public function getBalance(): float {
return $this->balance;
}
}

Разбор:

  • private в PHP проверяется на уровне класса, не экземпляра — метод может трогать private другого BankAccount.
  • С PHP 7.4+ свойства типизируются; с 8.0 — readonly для однократной инициализации.
  • Публичные свойства допустимы в DTO, но для доменных сущностей предпочтительны методы с инвариантами.

Четыре столпа ООП в PHP

Интерактивные схемы — общие принципы (псевдокод). Подробнее: абстракция, инкапсуляция, наследование, полиморфизм.

Play ITЗагрузка интерактивного демо…

Play ITЗагрузка интерактивного демо…

Play ITЗагрузка интерактивного демо…

Play ITЗагрузка интерактивного демо…

Абстракция

Скрытие деталей за контрактом. В PHP:

  • interface Logger { public function log(string $message): void; } — только обязательные методы.
  • abstract class AbstractRepository { abstract public function find(string $id): ?Entity; public function mustFind(string $id): Entity { /* шаблон */ } } — общий код + обязательный шаг в потомке.
interface PaymentGateway {
public function charge(int $amountCents): string;
}

final class StripeGateway implements PaymentGateway {
public function charge(int $amountCents): string {
// детали HTTP к Stripe
return 'pi_123';
}
}

Разбор:

  • Сервис заказов зависит от PaymentGateway, не от Stripe.
  • final class запрещает наследование — аналог final в Java для классов-реализаций.

Инкапсуляция

Состояние меняется через методы с правилами (deposit, withdraw). Подробный разбор — в разделе Инкапсуляция ниже.

Наследование

extends переносит поведение и защищённые члены. Используйте только для отношения является (is-a); для миксинов — trait.

Полиморфизм

Тип параметра Animal $a принимает Dog и Cat; вызывается переопределённый метод. Второй путь — несколько реализаций одного interface (предпочтительнее для слабой связности).


ООП в PHP

PHP отвечает на три прикладных вопроса веб-приложения:

  • Где хранится состояние сущности (User, Order, Invoice) и кто имеет право его менять.
  • Где живут бизнес-правила и как гарантировать их выполнение в каждом сценарии.
  • Как заменить реализацию (логгер, репозиторий, платёжный шлюз) без переписывания всего приложения.

Полноценная поддержка ООП — с PHP 5.0 (2004); версии 7.x и 8.x добавили типы, readonly, enum и promotion свойств в конструкторе. В Laravel и Symfony объектный стиль — фактический стандарт для доменной логики.

КЛАСС Кот
поля: имя, возраст
метод мяукнуть()
КОНЕЦ

объект barsik := новый Кот(имя="Барсик", возраст=3)
barsik.мяукнуть()

Разбор:

  • Блок показывает ООП-идею в псевдокоде без привязки к синтаксису конкретного языка.
  • Класс задает структуру данных (имя, возраст) и поведение (мяукнуть).
  • Объект barsik — конкретный экземпляр класса с собственным состоянием.
  • Вызов barsik.мяукнуть() демонстрирует отправку сообщения объекту через метод.
  • Такой формат помогает сначала понять модель, а потом переходить к PHP-реализации.

Пример класса

Справочно на PHP

Код ITЗагрузка примера кода…

Разбор:

  • class Unit — шаблон игровой сущности: свойства и методы в одном типе.
  • $this в методах ссылается на текущий объект и его состояние.
  • getDamage() — вычисляемое значение урона по характеристикам и уровню.
  • attack($target) — взаимодействие объектов: инициатор меняет health цели через метод.
  • new Unit() создаёт экземпляр; -> — доступ к полям и методам объекта.
  • <?php открывает блок PHP; в файлах только с кодом закрывающий ?> часто опускают.
  • Запуск: php filename.php — интерпретатор выполняет инструкции и выводит результат в консоль.

Интерактивная схема — класс и объект (псевдокод, подходит для любого ООП-языка). Полный разбор принципов: ООП в разделе "Код и разработка".

Play ITЗагрузка интерактивного демо…


Объектно-ориентированное программирование

ООП — парадигма, в которой программа строится вокруг объектов (экземпляров классов). Класс задаёт структуру данных и поведение; объект хранит конкретные значения и отвечает на вызовы методов.

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

Практический порядок для новичка:

  1. Спроектировать одну сущность с правилами изменения состояния.
  2. Вынести общий контракт в interface.
  3. Добавлять extends или trait только когда связь — отношение является.

Класс и объект — единица абстракции и её воплощение

В основе объектно-ориентированного подхода лежит понятие класса — это шаблон или чертёж, описывающий структуру и поведение некоторой сущности. Класс определяет, какие данные могут храниться в объекте (их называют свойствами, полями или атрибутами), и какие действия объект может выполнять (методы). Важно подчеркнуть: класс сам по себе не содержит данных и не выполняет код — он лишь задаёт форму.

Объект — это конкретный экземпляр класса. При создании объекта в памяти выделяется пространство под его свойства, инициализируются значения, вызываются конструкторы. Один и тот же класс может порождать неограниченное количество объектов, каждый из которых обладает независимым состоянием.

Рассмотрим базовый пример:

Код ITЗагрузка примера кода…

Разбор:

  • __construct($name) автоматически вызывается при new User("Петр").
  • $this->name = $name сохраняет входное значение в состояние конкретного объекта.
  • sayHello() формирует ответ на основе внутреннего свойства $name.
  • echo $user->sayHello() вызывает метод и выводит его результат.
  • Код демонстрирует связку "класс -> объект -> метод" в минимальном виде.

Класс User объявляется с помощью ключевого слова class. Внутри него определено одно публичное свойство $name и два метода: __construct() и sayHello(). Метод __construct() — это конструктор: специальный метод, который автоматически вызывается при создании нового объекта с помощью оператора new. Конструктор принимает аргумент $name, который присваивается свойству $this->name. Здесь $this — это псевдопеременная, ссылающаяся на текущий объект, то есть на тот экземпляр, в контексте которого выполняется метод. Без $this обращение к свойству было бы неоднозначным: интерпретатор не смог бы отличить локальную переменную от свойства класса.

Метод sayHello() — обычный метод экземпляра: он не принимает параметров, но использует значение свойства $name текущего объекта для формирования строки. Обращение к методу производится через оператор ->, что характерно для PHP при работе с объектами.

После объявления класса выполняется инструкция $user = new User("Петр");. Эта инструкция создаёт объект — экземпляр класса User, передавая в конструктор строку "Петр". В результате свойство $name этого объекта получает значение "Петр". Далее вызов $user->sayHello() приводит к выполнению метода sayHello() в контексте именно этого объекта, и возвращаемая строка содержит имя "Петр".

Стоит отметить: в PHP объекты передаются и присваиваются по ссылке (начиная с версии 5.0). Это означает, что при выполнении $anotherUser = $user; обе переменные будут ссылаться на один и тот же объект в памяти. Изменение свойств через одну переменную отразится и при обращении через другую. Такое поведение отличается от передачи примитивных типов (строк, чисел), которые копируются по значению. В случаях, когда требуется создать независимую копию объекта, используется клонирование через clone.

Также следует упомянуть, что в PHP до версии 8.0 не существовало строгой типизации параметров конструктора по умолчанию, хотя начиная с PHP 7.0 появилась возможность указывать объявление типов (type declarations) — в частности, скалярные типы (string, int, bool, float) и ? для обозначения допустимости null. В версии 8.0 был введён механизм конструкторного повышения (constructor property promotion), позволяющий объединить объявление свойства, параметра и присваивание в одну строку:

class User {
public function __construct(
public string $name
) {}
}

Разбор:

  • Это constructor property promotion из PHP 8+, сокращающий шаблонный код.
  • public string $name одновременно объявляет свойство и параметр конструктора.
  • Переданное значение автоматически присваивается полю без ручного $this->name = $name.
  • Подход ускоряет разработку простых DTO и value object.

Этот синтаксис создаёт публичное свойство $name и автоматически присваивает ему значение переданного аргумента. Он не меняет семантики, но сокращает шаблонный код.


Наследование — иерархия и расширение поведения

Интерактивная схема — наследование (псевдокод). Подробнее: Наследование.

Play ITЗагрузка интерактивного демо…

Наследование — один из фундаментальных механизмов ООП, позволяющий создавать новые классы на основе существующих. Класс, от которого наследуют, называется родительским (или базовым, суперклассом), а класс, который наследует — дочерним (или производным, подклассом). Дочерний класс автоматически получает все не приватные свойства и методы родительского класса и может расширять или изменять их поведение.

В PHP наследование реализуется с помощью ключевого слова extends. Оно поддерживает только одиночное наследование — то есть дочерний класс может иметь лишь одного непосредственного родителя. Это ограничение осознанно и направлено на упрощение модели взаимодействия и избежание проблем множественного наследования (например, ромбовидной зависимости). Вместо множественного наследования PHP предлагает использовать трейты и интерфейсы, о чём будет сказано далее.

Рассмотрим пример:

class Admin extends User {
public function role() {
return "Администратор";
}
}

Разбор:

  • extends User создает отношение наследования "Admin является User".
  • Класс Admin получает доступ к публичным и защищенным членам User.
  • Метод role() добавляет специализированное поведение дочернего класса.
  • Пример показывает расширение базового контракта без копирования кода.

Здесь класс Admin объявляется как расширение класса User. Это означает, что любой объект класса Admin будет обладать всеми свойствами и методами, определёнными в User, включая $name, __construct() и sayHello(), а также дополнительным методом role(), специфичным для администратора.

С точки зрения памяти и инициализации, при создании объекта new Admin("Анна") порядок выполнения следующий:

  1. Вызывается конструктор Admin. Поскольку в приведённом примере явного конструктора в Admin нет, PHP автоматически вызывает конструктор родительского класса User, если он определён и совместим по сигнатуре.
  2. В конструкторе User происходит присваивание $this->name = "Анна".
  3. Объект инициализирован и готов к использованию.

Если дочерний класс определяет собственный конструктор, то вызов родительского конструктора не происходит автоматически. Это важное отличие от некоторых других языков, где вызов super() может быть неявным. В PHP при необходимости явно вызывается родительский конструктор через parent::__construct(...). Например:

class Moderator extends User {
private $permissions;

public function __construct($name, array $permissions) {
parent::__construct($name); // явный вызов конструктора родителя
$this->permissions = $permissions;
}
}

Разбор:

  • Дочерний класс объявляет собственный конструктор с дополнительным параметром permissions.
  • parent::__construct($name) явно вызывает инициализацию родительской части объекта.
  • Приватное свойство $permissions хранит расширенное состояние только этого класса.
  • Такой паттерн важен, когда наследник дополняет, а не заменяет базовый контракт.

Пропуск parent::__construct() в таком случае приведёт к тому, что свойства родительского класса (в данном случае $name) останутся неинициализированными, что может вызвать ошибки или неопределённое поведение.

Наследование тесно связано с принципом подстановки Барбары Лисков: объект дочернего класса должен быть взаимозаменяемым с объектом родительского класса без нарушения корректности программы. Это означает, что дочерний класс не должен ослаблять предусловия, усиливать постусловия или изменять контракт методов (например, возвращать значения иных типов, если это не разрешено ковариантностью или контравариантностью). В PHP начиная с версии 7.4 реализована ковариантность возвращаемых типов и контравариантность типов параметров при переопределении методов, что делает соблюдение этого принципа более строго контролируемым:

Код ITЗагрузка примера кода…

Разбор:

  • Базовый класс Shape задает общий метод getArea() с типом float.
  • Rectangle переопределяет реализацию, сохраняя совместимый контракт возврата.
  • Конструктор принимает размеры и сохраняет их в типизированных свойствах.
  • Полиморфный вызов getArea() работает единообразно через ссылку на Shape.
  • Пример закрепляет идею корректного override без нарушения сигнатуры.

Здесь переопределение getArea() корректно: возвращаемый тип совпадает, а реализация изменена. Если бы дочерний класс возвращал, скажем, строку — это вызвало бы фатальную ошибку на этапе компиляции (в runtime, но до выполнения основного кода).

Следует избегать чрезмерного углубления иерархий наследования. В PHP, как и в большинстве языков, сложные древовидные структуры (A extends B extends C extends D…) затрудняют анализ кода, усложняют тестирование и повышают связанность. Предпочтительнее проектировать "плоские" иерархии, в которых наследование используется только для выражения отношения "является" (is-a), а не для повторного использования кода как такового.


Инкапсуляция — управление доступом и сокрытие реализации

Интерактивная схема — инкапсуляция (псевдокод). Подробнее: Инкапсуляция.

Play ITЗагрузка интерактивного демо…

Инкапсуляция — это принцип, согласно которому внутреннее состояние объекта (его свойства) защищается от прямого внешнего вмешательства, а взаимодействие с объектом осуществляется только через чётко определённый интерфейс — его публичные методы. Цель — обеспечить целостность данных и уменьшить связанность между компонентами системы.

В PHP инкапсуляция реализуется с помощью модификаторов доступаpublic, protected, private.

  • public — элемент доступен из любого контекста — внутри класса, извне, из дочерних классов.
  • protected — элемент доступен только внутри класса и его дочерних классов, но не извне.
  • private — элемент доступен только внутри того класса, в котором он объявлен; даже дочерние классы не могут получить к нему доступ.

Пример:

class BankAccount {
private float $balance = 0.0;

public function deposit(float $amount): void {
if ($amount > 0) {
$this->balance += $amount;
}
}

public function getBalance(): float {
return $this->balance;
}
}

Разбор:

  • private float $balance скрывает внутреннее состояние от прямой записи извне.
  • deposit(float $amount) реализует контролируемую точку изменения баланса.
  • Валидация if ($amount > 0) защищает объект от некорректного ввода.
  • getBalance() дает только чтение и сохраняет инварианты класса.
  • Блок показывает практическую инкапсуляцию через публичный API.

Свойство $balance объявлено как private, что исключает прямое присваивание вида $account->balance = -1000. Вместо этого изменение баланса возможно только через метод deposit(), который включает валидацию: отрицательные суммы отклоняются. Метод getBalance() предоставляет только чтение текущего значения — клиентский код не может изменить баланс, минуя бизнес-логику.

Важно: в PHP до версии 7.4 не было поддержки типизации свойств, и их объявление выглядело как private $balance;. Начиная с PHP 7.4, появилась возможность объявлять типы прямым синтаксисом (private float $balance); в PHP 8.0 — инициализировать свойства в месте объявления, включая выражения (private DateTime $createdAt = new DateTime();), что сокращает необходимость в конструкторе для тривиальных инициализаций.

Модификаторы доступа действуют на уровне класса. Это означает, что метод одного объекта класса A может обращаться к private-свойствам другого объекта того же класса A. Такое поведение соответствует спецификации языка и отличается от, например, C++ или Java, где доступ проверяется на уровне экземпляра. Это редко вызывает проблемы на практике, но важно понимать при проектировании систем, чувствительных к инкапсуляции на уровне объектов.


Полиморфизм — единообразие в разнообразии

Интерактивная схема — полиморфизм (псевдокод). Подробнее: Полиморфизм.

Play ITЗагрузка интерактивного демо…

Полиморфизм — способность объектов разных классов обрабатываться единообразно при условии, что они реализуют общий интерфейс или наследуют от общего предка. Это позволяет писать код, не зависящий от конкретных типов, а опирающийся на абстракции.

В PHP полиморфизм проявляется двумя основными способами:

  1. Через наследование и переопределение методов:

Код ITЗагрузка примера кода…

Разбор:

  • Animal задает общий интерфейс поведения через метод makeSound().

  • Dog и Cat переопределяют метод своей конкретной реализацией.

  • Функция announce(Animal $animal) зависит от абстракции, а не от конкретного класса.

  • При вызове PHP выбирает нужную реализацию динамически по реальному типу объекта.

  • Это классический пример runtime-полиморфизма.

    Здесь функция announce() принимает параметр типа Animal, но работает корректно с любым подклассом, поскольку каждый из них переопределяет makeSound() в соответствии со своей спецификой. Выбор реализации происходит динамически, во время выполнения, на основе реального типа объекта.

  1. Через интерфейсы, что является более гибким и предпочтительным подходом (см. ниже).

Полиморфизм лежит в основе таких практик, как внедрение зависимостей (dependency injection), использование фабрик, стратегий и других шаблонов проектирования. Он позволяет разделять интерфейс и реализацию, повышая тестируемость (через моки и заглушки) и расширяемость (через добавление новых реализаций без изменения клиентского кода).


Интерфейсы — контракты без реализации

Интерфейс — это особый вид абстракции, который задаёт контракт — набор методов, которые обязан реализовать любой класс, заявивший о его поддержке. В отличие от класса, интерфейс не содержит реализации методов (до PHP 8.0 — вообще никакой; начиная с PHP 8.0 — может содержать методы по умолчанию, но это исключение из правила и требует осторожного применения).

Интерфейс объявляется ключевым словом interface, а его реализация — implements. Класс может реализовывать несколько интерфейсов, что компенсирует отсутствие множественного наследования.

Рассмотрим пример:

Код ITЗагрузка примера кода…

Разбор:

  • interface Logger определяет контракт метода log() без реализации.
  • implements Logger обязывает классы реализовать метод с совместимой сигнатурой.
  • FileLogger и ConsoleLogger реализуют один и тот же интерфейс разными способами.
  • Клиентский код может подставлять любую реализацию без изменения вызывающей логики.
  • Такой подход уменьшает связность и упрощает тестирование через моки.

Интерфейс Logger определяет единый метод log(), принимающий строку. Ни один класс не знает, как именно будет происходить логирование — важен только факт, что вызов log() возможен и соответствует сигнатуре. Это позволяет писать клиентский код, не привязанный к конкретной реализации:

Код ITЗагрузка примера кода…

Разбор:

  • private Logger $logger хранит зависимость на уровне интерфейса.
  • Конструкторная инъекция передает реализацию извне и поддерживает DI.
  • createUser() вызывает log() без знания конкретного способа логирования.
  • Замена FileLogger на ConsoleLogger выполняется одной строкой конфигурации.
  • Блок демонстрирует инверсию зависимостей в прикладном коде.

Интерфейсы особенно ценны в крупных системах, где важна заменяемость компонентов. Они позволяют явно выразить зависимости на уровне типов и поддерживаются инструментами статического анализа (например, PHPStan, Psalm), что повышает надёжность кода.

Начиная с PHP 8.0, интерфейсы могут содержать методы по умолчанию (default interface methods), заимствованные из Java. Это позволяет добавлять новые методы в существующие интерфейсы без нарушения обратной совместимости — классы, уже реализующие интерфейс, могут не переопределять новый метод, если его реализация по умолчанию приемлема. Однако такая практика требует осторожности: чрезмерное использование методов по умолчанию может привести к "размыванию" контракта и снижению явности.

Сравнение с таблицей и примерами — Абстракция в ООП. В PHP:

Критерийinterfaceabstract class
РольКонтракт способности (can-do)Частичная реализация семейства (is-a)
Ключевые словаimplements, несколько штукextends, один родитель
СостояниеКонстанты; с PHP 8.0+ — методы по умолчаниюСвойства и реализованные методы
Общий код без иерархииЧасто интерфейс + trait

Абстрактный класс (abstract class) подходит, когда наследники — одно семейство и им нужны общие поля или конструктор.

Интерфейс подходит, когда важен только контракт для полиморфного кода. Если нужна ещё и готовая реализация, но типы не родственники, в PHP обычно подключают trait, а не наследуют абстрактный класс ради одного метода.


Трейты — повторное использование кода без наследования

Трейт (trait) — это механизм горизонтального повторного использования кода, введённый в PHP 5.4. Он позволяет внедрять набор методов (и, начиная с PHP 8.0, даже свойств) в класс без установления иерархической связи "родитель–потомок". Трейты особенно полезны в условиях одиночного наследования, когда требуется совместить поведение из нескольких независимых источников.

Трейт объявляется ключевым словом trait, а подключается в класс с помощью use. В отличие от интерфейсов, трейты содержат реализацию, но, в отличие от классов, не могут быть инстанцированы напрямую.

Пример:

Код ITЗагрузка примера кода…

Разбор:

  • trait Loggable инкапсулирует переиспользуемый метод log.
  • use Loggable; внедряет этот метод в класс без наследования.
  • Метод process() использует трейт как обычный метод экземпляра.
  • Трейты полезны для кросс-срезной технической логики.
  • При этом доменные зависимости обычно лучше передавать через композицию.

После подключения трейта Loggable в класс Service, метод log() становится доступен как обычный метод экземпляра, будто он был объявлен непосредственно в теле класса.

Трейты могут содержать как публичные, так и защищённые методы и свойства. Начиная с PHP 8.2, трейты получили поддержку свойств, включая типизированные и абстрактные свойства — это позволяет выносить в трейты поведение и часть состояния.


Разрешение конфликтов

Если два или более трейта предоставляют метод с одинаковым именем, PHP не может разрешить конфликт автоматически и генерирует фатальную ошибку. Для явного управления используется конструкция insteadof и as:

Код ITЗагрузка примера кода…

Разбор:

  • Оба трейта содержат метод с одинаковым именем greet, что создает конфликт.
  • A::greet insteadof B выбирает реализацию из трейта A.
  • B::greet as greetFromB сохраняет вторую реализацию под алиасом.
  • Механизм позволяет явно и безопасно разрешать коллизии методов.
  • Такой контроль делает смешивание нескольких трейтов предсказуемым.

Здесь greet() вызовет реализацию из трейта A, а greetFromB() — из B. Это позволяет сохранить обе реализации при необходимости.


Влияние на инкапсуляцию и иерархию

Трейты нарушают строгую иерархическую модель, но не нарушают инкапсуляцию — методы трейта выполняются в контексте того класса, в который они подключены, и имеют доступ к $this, включая приватные и защищённые члены хост-класса. Обратное неверно: хост-класс не имеет прямого доступа к приватным членам трейта, но может вызывать его публичные и защищённые методы.

Трейты не следует использовать как замену композиции. Если поведение логически независимо и может быть представлено как отдельный объект (например, Logger, Validator), предпочтительнее внедрять его через зависимость. Трейты оправданы, когда речь идёт о техническом кросс-куттинг-концерне (cross-cutting concern) — например, ведение лога, измерение времени выполнения, кэширование вызовов, — где внедрение отдельного объекта избыточно или неудобно.


Статические члены — поведение и данные на уровне класса

В дополнение к методам и свойствам экземпляра, PHP поддерживает статические члены — они принадлежат самому классу. Они инициализируются один раз при первом обращении к классу и существуют в единственном экземпляре на всё время выполнения скрипта.

Статическое свойство объявляется с модификатором static, доступ к нему осуществляется через self::, static:: или имя класса (ClassName::$property). Аналогично — статические методы.

Код ITЗагрузка примера кода…

Разбор:

  • private static int $count принадлежит классу, а не экземпляру.
  • Конструктор увеличивает общий счетчик при каждом new Counter().
  • public static function getCount() читает состояние без создания объекта.
  • Counter::getCount() вызывается через оператор ::.
  • Пример показывает модель общего состояния на уровне класса.

Здесь $count — общий счётчик для всех экземпляров. Каждое создание объекта увеличивает его значение, а getCount() позволяет получить текущее состояние без необходимости создавать объект.

Ключевое различие между self:: и static::позднее статическое связывание (late static binding), введённое в PHP 5.3. self:: всегда ссылается на класс, в котором метод объявлен, тогда как static:: ссылается на класс, с которым был вызван метод (возможно, дочерний). Это критично при наследовании статических членов.

Код ITЗагрузка примера кода…

Разбор:

  • self:: привязывается к классу, где метод объявлен (Base).
  • static:: использует позднее статическое связывание и учитывает класс вызова.
  • В Child::who() возвращается Base, потому что метод использует self.
  • В Child::who2() возвращается Child, потому что метод использует static.
  • Разница критична при проектировании расширяемых статических API.

Статические члены удобны для фабрик, реестров, утилитарных классов, но их чрезмерное применение ведёт к глобальному состоянию, что затрудняет тестирование и нарушает принцип единственной ответственности. В современных приложениях статические методы стараются минимизировать в пользу внедрения зависимостей и инверсии управления.


Магические методы — управление поведением объекта на уровне языка

PHP предоставляет набор магических методов — специальных методов с зарезервированными именами, которые вызываются автоматически при выполнении определённых операций с объектом. Они позволяют переопределить стандартное поведение — доступ к несуществующим свойствам, вызов несуществующих методов, сериализацию, строковое представление и др.

Наиболее часто используемые:

  • __construct() — конструктор (уже рассмотрен).
  • __destruct() — деструктор; вызывается при уничтожении объекта (например, при выходе из области видимости или явном unset). Используется для освобождения ресурсов (файловые дескрипторы, соединения).
  • __get($name) и __set($name, $value) — вызываются при чтении и записи недоступного (например, несуществующего или приватного) свойства. Позволяют реализовать динамические свойства, lazy loading, прокси.
  • __call($name, $arguments) и __callStatic($name, $arguments) — вызываются при вызове несуществующего метода экземпляра или статического метода соответственно. Основа для реализации прокси, фасадов, Fluent Interface.
  • __toString() — вызывается при попытке использовать объект как строку (например, в echo). Должен возвращать строку, иначе — фатальная ошибка.
  • __invoke() — позволяет вызывать объект как функцию: $obj().

Пример динамического доступа:

Код ITЗагрузка примера кода…

Разбор:

  • __set перехватывает запись в недоступные или несуществующие свойства.
  • __get перехватывает чтение и возвращает значение из внутреннего массива.
  • Такой шаблон реализует динамическую конфигурацию и lazy-доступ.
  • Магические методы повышают гибкость, но скрывают явные контракты полей.
  • Для production-кода важно ограничивать такие точки динамики.

Магические методы мощны, но их следует использовать с осторожностью — они делают поведение объекта менее прозрачным, затрудняют статический анализ и могут скрывать ошибки (например, опечатка в имени свойства не вызовет ошибку, а создаст новое динамическое поле). В production-коде рекомендуется ограничивать их применение чётко обозначенными паттернами (например, только __toString() и __invoke() — в DTO и коллбэках).


Анонимные классы — локальная реализация интерфейсов и адаптеров

Начиная с PHP 7.0, доступны анонимные классы — объявление и инстанцирование класса в одном выражении. Они полезны для создания одноразовых реализаций интерфейсов, моков в тестах или адаптеров без загрязнения глобального пространства имён.

$logger = new class implements Logger {
public function log(string $message): void {
syslog(LOG_INFO, $message);
}
};

$manager = new UserManager($logger);

Разбор:

  • new class ... создает анонимный класс прямо в месте использования.
  • implements Logger гарантирует совместимость с зависимостью UserManager.
  • Метод log() задает одноразовую реализацию без отдельного именованного класса.
  • Прием удобен в тестах и локальных адаптерах.
  • Код уменьшает шум в проекте, когда реализация нужна в одном месте.

Анонимный класс может наследовать от другого класса, реализовывать интерфейсы, использовать трейты и содержать свойства и методы — как обычный класс. Отличие лишь в том, что он не имеет имени и объявляется inline.

Синтаксически анонимный класс похож на функцию: new class (аргументы) extends … implements … { … }. Аргументы передаются в конструктор, если он определён.

Анонимные классы часто применяются в тестировании (PHPUnit поддерживает их напрямую), при создании замыканий с состоянием или адаптации сторонних библиотек под собственный интерфейс без создания именованного класса.


Клонирование объектов

Как уже отмечалось, присваивание объекта в PHP создаёт новую ссылку на тот же экземпляр. Для получения независимой копии используется оператор clone.

По умолчанию clone выполняет поверхностное копирование — все свойства копируются по значению, но если свойство содержит ссылку на другой объект, то копируется лишь сама ссылка — оба объекта (оригинал и клон) будут указывать на один и тот же вложенный объект.

Для управления процессом клонирования класс может определить магический метод __clone(). Он вызывается автоматически после создания копии и может содержать логику глубокого копирования или сброса состояния:

class User {
public string $name;
public DateTime $createdAt;

public function __construct(string $name) {
$this->name = $name;
$this->createdAt = new DateTime();
}

public function __clone() {
$this->createdAt = clone $this->createdAt; // глубокое копирование DateTime
}
}

Разбор:

  • Оператор clone по умолчанию делает поверхностное копирование объекта.
  • __clone() позволяет вручную скорректировать поведение после клонирования.
  • Повторный clone $this->createdAt создает независимый объект времени.
  • Это устраняет общий mutable-ссылочный объект между оригиналом и копией.
  • Прием важен для корректной изоляции состояния.

Без __clone() оба объекта ($user и clone $user) имели бы общую ссылку на один объект DateTime, и изменение времени у одного повлияло бы на другого.


Сериализация и десериализация

Сериализация — преобразование объекта в строку (обычно для хранения или передачи), десериализация — обратный процесс. В PHP это делается функциями serialize() и unserialize().

По умолчанию сериализуются все нестатические свойства, включая приватные (но с модификатором класса в имени для избежания конфликтов). Однако такое поведение не всегда приемлемо: например, ресурсы (соединения с БД) нельзя сериализовать.

Для контроля процесса используются магические методы:

  • __sleep() — вызывается перед сериализацией; должен вернуть массив имён свойств, подлежащих сохранению. Может использоваться для закрытия ресурсов.
  • __wakeup() — вызывается после десериализации; восстанавливает состояние (например, переоткрывает соединения).

Код ITЗагрузка примера кода…

Разбор:

  • __sleep() определяет, какие свойства попадут в сериализованное представление.
  • Здесь сохраняется только dsn, а несериализуемый ресурс PDO исключается.
  • __wakeup() пересоздает соединение после unserialize.
  • Шаблон помогает безопасно переносить объект между процессами или кешами.
  • Важно явно восстанавливать все ресурсы, которые не живут в строковом дампе.

Начиная с PHP 7.4, рекомендуется использовать механизм магических методов сериализации (__serialize() и __unserialize()), который безопаснее и явнее:

public function __serialize(): array {
return ['dsn' => $this->dsn];
}

public function __unserialize(array $data): void {
$this->dsn = $data['dsn'];
$this->pdo = new PDO($this->dsn);
}

Разбор:

  • __serialize() возвращает точный массив данных для сохранения состояния.
  • __unserialize() получает этот массив и восстанавливает объект вручную.
  • Механизм современнее и надежнее пары __sleep/__wakeup.
  • Такой подход делает сериализацию явной и проще контролируется при рефакторинге.
  • Пример показывает безопасный цикл восстановления PDO по сохраненному dsn.

Эти методы не подвержены проблемам с именованием приватных свойств и более совместимы с будущими версиями.


Сравнение PHP с Java

ТемаPHPJava
Модель типовдинамическая + опциональные типы (7+)статическая
Конструктор__construct, promotion (8+)имя класса / compact record
Перегрузка методовнет (последнее объявление побеждает)да
Множественное наследование классовнетнет
Примесиtrait с insteadof / asинтерфейсы с default methods
Магические методы__get, __call, __toStringограниченный набор (toString, equals)
Передача объектовпо ссылкепо ссылке на объект
nullдопустим везде без ? (до strict)Optional, nullable types
Namespacenamespace, PSR-4 autoloadpackage, module system
Анонимные классыnew class { }локальные классы (ограниченно)
Сериализацияserialize / __sleepSerializable, JSON APIs

PHP в веб-стеке (Laravel, Symfony) использует те же идеи DI и интерфейсов, что Java Spring — но рантайм остаётся интерпретируемым и динамическим.


Типичные ошибки

ОшибкаПоследствиеЧто делать
Публичные свойства без инвариантовнекорректное состояние ($user->balance = -1)private + методы; readonly для DTO
Забыли parent::__construct()неинициализированные поля родителяявный вызов в каждом конструкторе наследника
Трейты вместо композициикласс с десятком use и размытой ответственностьювнедрять зависимости через конструктор
Злоупотребление __get/__setопечатки не ловятся, IDE не подсказываетявные свойства и методы
Ожидание перегрузки как в Javaвторой метод с тем же именем перезапишет первыйразные имена или optional parameters
$a = $b для копииоба изменяют один объектclone + __clone для глубокого копирования
Интерфейс с логикой в default methods без нуждыразмытый контрактdefault methods — только для обратной совместимости
Статические синглтоны вездеглобальное состояние, сложные тестыDI-контейнер фреймворка

Чеклист проектирования класса в PHP

  1. Инварианты — все обязательные поля заданы в __construct; недопустимые значения отклоняются.
  2. Видимость — минимум public; состояние — private или protected.
  3. Контракт — общие зависимости через interface, не через конкретный класс.
  4. Наследование — только отношение является; иначе композиция или trait для технического кросс-ката.
  5. Магия__call/__get только в инфраструктуре (ORM, прокси), не в домене.
  6. Типы — аргументы и возврат с declare(strict_types=1); в новых файлах.
  7. Тест — PHPUnit с моками интерфейсов, не конкретных HTTP-клиентов.

См. также


Учебные примеры ООП

Небольшие самодостаточные программы, которые показывают классы, объекты, инкапсуляцию, наследование и взаимодействие нескольких типов на одной предметной области.

Класс и объект

Чертёж класса Figure и конкретные объекты — круг и квадрат.

Код ITЗагрузка примера кода…


Банковский счёт

Инкапсуляция: скрытое поле баланса и методы deposit/withdraw.

Код ITЗагрузка примера кода…


Наследование

Родитель Animal и дочерние Cat и Dog с общим eat() и своим speak().

Код ITЗагрузка примера кода…


Смартфон

Состояние объекта: заряд батареи, звонки и подзарядка.

Код ITЗагрузка примера кода…


Студент

Список оценок, средний балл и проходной порог.

Код ITЗагрузка примера кода…


Корзина покупок

Взаимодействие Product, Cart и Order при оформлении заказа.

Код ITЗагрузка примера кода…


Автомобиль

Пробег, расход топлива и напоминание о техобслуживании.

Код ITЗагрузка примера кода…


Пользователь

Скрытый пароль, вход в систему и публикация сообщений.

Код ITЗагрузка примера кода…

Содержание