5.07. ООП
ООП
Объектно-ориентированное программирование (ООП) представляет собой парадигму разработки, в которой программа строится вокруг объектов — экземпляров абстракций, называемых классами. Эта парадигма призвана повысить модульность, переиспользуемость и сопровождаемость кода за счёт явного выделения сущностей предметной области и их поведения. В PHP поддержка ООП стала полноценной начиная с версии 5.0 (2004 год), а последующие версии, особенно 5.3, 5.4, 7.0 и 8.0, существенно расширили её возможности и устранили ограничения ранних реализаций.
PHP — язык с динамической типизацией и мультипарадигмальной архитектурой, что означает: объектно-ориентированный стиль не является обязательным, но при разработке сложных приложений, особенно в рамках фреймворков (Symfony, Laravel, Yii и др.), он становится фактически стандартом. Это обусловлено как техническими преимуществами (инкапсуляция, наследование, полиморфизм), так и экосистемными — большинство современных инструментов PHP-разработки проектируются с учётом ООП-подхода.
Для понимания ООП в PHP необходимо последовательно рассмотреть ключевые концепции: класс и объект, наследование, инкапсуляция, полиморфизм, интерфейсы, абстрактные классы, трейты, а также особенности реализации этих механизмов именно в PHP — с учётом его исторического развития, семантики и ограничений платформы.
Класс и объект: единица абстракции и её воплощение
В основе объектно-ориентированного подхода лежит понятие класса — это шаблон или чертёж, описывающий структуру и поведение некоторой сущности. Класс определяет, какие данные могут храниться в объекте (их называют свойствами, полями или атрибутами), и какие действия объект может выполнять (методы). Важно подчеркнуть: класс сам по себе не содержит данных и не выполняет код — он лишь задаёт форму.
Объект — это конкретный экземпляр класса. При создании объекта в памяти выделяется пространство под его свойства, инициализируются значения, вызываются конструкторы. Один и тот же класс может порождать неограниченное количество объектов, каждый из которых обладает независимым состоянием.
Рассмотрим базовый пример:
class User {
public $name;
public function __construct($name) {
$this->name = $name;
}
public function sayHello() {
return "Привет, " . $this->name;
}
}
$user = new User("Петр");
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
) {}
}
Этот синтаксис создаёт публичное свойство $name и автоматически присваивает ему значение переданного аргумента. Он не меняет семантики, но сокращает шаблонный код.
Наследование: иерархия и расширение поведения
Наследование — один из фундаментальных механизмов ООП, позволяющий создавать новые классы на основе существующих. Класс, от которого наследуют, называется родительским (или базовым, суперклассом), а класс, который наследует — дочерним (или производным, подклассом). Дочерний класс автоматически получает все не приватные свойства и методы родительского класса и может расширять или изменять их поведение.
В PHP наследование реализуется с помощью ключевого слова extends. Оно поддерживает только одиночное наследование — то есть дочерний класс может иметь лишь одного непосредственного родителя. Это ограничение осознанно и направлено на упрощение модели взаимодействия и избежание проблем множественного наследования (например, ромбовидной зависимости). Вместо множественного наследования PHP предлагает использовать трейты и интерфейсы, о чём будет сказано далее.
Рассмотрим пример:
class Admin extends User {
public function role() {
return "Администратор";
}
}
Здесь класс Admin объявляется как расширение класса User. Это означает, что любой объект класса Admin будет обладать всеми свойствами и методами, определёнными в User, включая $name, __construct() и sayHello(), а также дополнительным методом role(), специфичным для администратора.
С точки зрения памяти и инициализации, при создании объекта new Admin("Анна") порядок выполнения следующий:
- Вызывается конструктор
Admin. Поскольку в приведённом примере явного конструктора вAdminнет, PHP автоматически вызывает конструктор родительского классаUser, если он определён и совместим по сигнатуре. - В конструкторе
Userпроисходит присваивание$this->name = "Анна". - Объект инициализирован и готов к использованию.
Если дочерний класс определяет собственный конструктор, то вызов родительского конструктора не происходит автоматически. Это важное отличие от некоторых других языков, где вызов super() может быть неявным. В PHP при необходимости явно вызывается родительский конструктор через parent::__construct(...). Например:
class Moderator extends User {
private $permissions;
public function __construct($name, array $permissions) {
parent::__construct($name); // явный вызов конструктора родителя
$this->permissions = $permissions;
}
}
Пропуск parent::__construct() в таком случае приведёт к тому, что свойства родительского класса (в данном случае $name) останутся неинициализированными, что может вызвать ошибки или неопределённое поведение.
Наследование тесно связано с принципом подстановки Барбары Лисков: объект дочернего класса должен быть взаимозаменяемым с объектом родительского класса без нарушения корректности программы. Это означает, что дочерний класс не должен ослаблять предусловия, усиливать постусловия или изменять контракт методов (например, возвращать значения иных типов, если это не разрешено ковариантностью или контравариантностью). В PHP начиная с версии 7.4 реализована ковариантность возвращаемых типов и контравариантность типов параметров при переопределении методов, что делает соблюдение этого принципа более строго контролируемым:
class Shape {
public function getArea(): float {
return 0.0;
}
}
class Rectangle extends Shape {
private float $width;
private float $height;
public function __construct(float $width, float $height) {
$this->width = $width;
$this->height = $height;
}
// Ковариантность: float → float (допустимо, тип не сужен)
public function getArea(): float {
return $this->width * $this->height;
}
}
Здесь переопределение getArea() корректно: возвращаемый тип совпадает, а реализация изменена. Если бы дочерний класс возвращал, скажем, строку — это вызвало бы фатальную ошибку на этапе компиляции (в runtime, но до выполнения основного кода).
Следует избегать чрезмерного углубления иерархий наследования. В PHP, как и в большинстве языков, сложные древовидные структуры (A extends B extends C extends D…) затрудняют анализ кода, усложняют тестирование и повышают связанность. Предпочтительнее проектировать «плоские» иерархии, в которых наследование используется только для выражения отношения «является» (is-a), а не для повторного использования кода как такового.
Инкапсуляция: управление доступом и сокрытие реализации
Инкапсуляция — это принцип, согласно которому внутреннее состояние объекта (его свойства) защищается от прямого внешнего вмешательства, а взаимодействие с объектом осуществляется только через чётко определённый интерфейс — его публичные методы. Цель — обеспечить целостность данных и уменьшить связанность между компонентами системы.
В 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;
}
}
Свойство $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, где доступ проверяется на уровне экземпляра. Это редко вызывает проблемы на практике, но важно понимать при проектировании систем, чувствительных к инкапсуляции на уровне объектов.
Полиморфизм: единообразие в разнообразии
Полиморфизм — способность объектов разных классов обрабатываться единообразно при условии, что они реализуют общий интерфейс или наследуют от общего предка. Это позволяет писать код, не зависящий от конкретных типов, а опирающийся на абстракции.
В PHP полиморфизм проявляется двумя основными способами:
-
Через наследование и переопределение методов:
class Animal {
public function makeSound(): string {
return "…";
}
}
class Dog extends Animal {
public function makeSound(): string {
return "Гав";
}
}
class Cat extends Animal {
public function makeSound(): string {
return "Мяу";
}
}
function announce(Animal $animal): void {
echo $animal->makeSound();
}
announce(new Dog()); // → "Гав"
announce(new Cat()); // → "Мяу"Здесь функция
announce()принимает параметр типаAnimal, но работает корректно с любым подклассом, поскольку каждый из них переопределяетmakeSound()в соответствии со своей спецификой. Выбор реализации происходит динамически, во время выполнения, на основе реального типа объекта. -
Через интерфейсы, что является более гибким и предпочтительным подходом (см. ниже).
Полиморфизм лежит в основе таких практик, как внедрение зависимостей (dependency injection), использование фабрик, стратегий и других шаблонов проектирования. Он позволяет разделять интерфейс и реализацию, повышая тестируемость (через моки и заглушки) и расширяемость (через добавление новых реализаций без изменения клиентского кода).
Интерфейсы: контракты без реализации
Интерфейс — это особый вид абстракции, который задаёт контракт: набор методов, которые обязан реализовать любой класс, заявивший о его поддержке. В отличие от класса, интерфейс не содержит реализации методов (до PHP 8.0 — вообще никакой; начиная с PHP 8.0 — может содержать методы по умолчанию, но это исключение из правила и требует осторожного применения).
Интерфейс объявляется ключевым словом interface, а его реализация — implements. Класс может реализовывать несколько интерфейсов, что компенсирует отсутствие множественного наследования.
Рассмотрим пример:
interface Logger {
public function log(string $message): void;
}
class FileLogger implements Logger {
public function log(string $message): void {
file_put_contents('log.txt', $message . PHP_EOL, FILE_APPEND);
}
}
class ConsoleLogger implements Logger {
public function log(string $message): void {
echo "[LOG] $message\n";
}
}
Интерфейс Logger определяет единый метод log(), принимающий строку. Ни один класс не знает, как именно будет происходить логирование — важен только факт, что вызов log() возможен и соответствует сигнатуре. Это позволяет писать клиентский код, не привязанный к конкретной реализации:
class UserManager {
private Logger $logger;
public function __construct(Logger $logger) {
$this->logger = $logger;
}
public function createUser(string $name): void {
// … логика создания
$this->logger->log("Создан пользователь: $name");
}
}
// Гибкая компоновка:
$manager = new UserManager(new FileLogger());
// или
$manager = new UserManager(new ConsoleLogger());
Интерфейсы особенно ценны в крупных системах, где важна заменяемость компонентов. Они позволяют явно выразить зависимости на уровне типов и поддерживаются инструментами статического анализа (например, PHPStan, Psalm), что повышает надёжность кода.
Начиная с PHP 8.0, интерфейсы могут содержать методы по умолчанию (default interface methods), заимствованные из Java. Это позволяет добавлять новые методы в существующие интерфейсы без нарушения обратной совместимости — классы, уже реализующие интерфейс, могут не переопределять новый метод, если его реализация по умолчанию приемлема. Однако такая практика требует осторожности: чрезмерное использование методов по умолчанию может привести к «размыванию» контракта и снижению явности.
Важно не путать интерфейсы с абстрактными классами. Абстрактный класс (abstract class) может содержать как абстрактные методы (без реализации), так и конкретные (с реализацией), а также свойства. Он предназначен для частичной реализации поведения, тогда как интерфейс — только для объявления контракта. Предпочтение следует отдавать интерфейсам, если нет необходимости в общей реализации или защищённых данных.
Трейты: повторное использование кода без наследования
Трейт (trait) — это механизм горизонтального повторного использования кода, введённый в PHP 5.4. Он позволяет внедрять набор методов (и, начиная с PHP 8.0, даже свойств) в класс без установления иерархической связи «родитель–потомок». Трейты особенно полезны в условиях одиночного наследования, когда требуется совместить поведение из нескольких независимых источников.
Трейт объявляется ключевым словом trait, а подключается в класс с помощью use. В отличие от интерфейсов, трейты содержат реализацию, но, в отличие от классов, не могут быть инстанцированы напрямую.
Пример:
trait Loggable {
public function log(string $msg): void {
error_log("[LOG] $msg");
}
}
class Service {
use Loggable;
public function process(): void {
$this->log("Начата обработка");
// … основная логика
$this->log("Обработка завершена");
}
}
После подключения трейта Loggable в класс Service, метод log() становится доступен как обычный метод экземпляра, будто он был объявлен непосредственно в теле класса.
Трейты могут содержать как публичные, так и защищённые методы и свойства. Начиная с PHP 8.2, трейты получили поддержку свойств, включая типизированные и абстрактные свойства — это позволяет выносить в трейты поведение и часть состояния.
Разрешение конфликтов
Если два или более трейта предоставляют метод с одинаковым именем, PHP не может разрешить конфликт автоматически и генерирует фатальную ошибку. Для явного управления используется конструкция insteadof и as:
trait A {
public function greet() { return "Привет от A"; }
}
trait B {
public function greet() { return "Привет от B"; }
}
class C {
use A, B {
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). Аналогично — статические методы.
class Counter {
private static int $count = 0;
public function __construct() {
self::$count++;
}
public static function getCount(): int {
return self::$count;
}
}
new Counter();
new Counter();
echo Counter::getCount(); // → 2
Здесь $count — общий счётчик для всех экземпляров. Каждое создание объекта увеличивает его значение, а getCount() позволяет получить текущее состояние без необходимости создавать объект.
Ключевое различие между self:: и static:: — позднее статическое связывание (late static binding), введённое в PHP 5.3. self:: всегда ссылается на класс, в котором метод объявлен, тогда как static:: ссылается на класс, с которым был вызван метод (возможно, дочерний). Это критично при наследовании статических членов.
class Base {
protected static string $name = 'Base';
public static function who(): string {
return self::$name;
}
public static function who2(): string {
return static::$name;
}
}
class Child extends Base {
protected static string $name = 'Child';
}
echo Base::who(); // → "Base"
echo Child::who(); // → "Base" (self → Base)
echo Child::who2(); // → "Child" (static → Child)
Статические члены удобны для фабрик, реестров, утилитарных классов, но их чрезмерное применение ведёт к глобальному состоянию, что затрудняет тестирование и нарушает принцип единственной ответственности. В современных приложениях статические методы стараются минимизировать в пользу внедрения зависимостей и инверсии управления.
Магические методы: управление поведением объекта на уровне языка
PHP предоставляет набор магических методов — специальных методов с зарезервированными именами, которые вызываются автоматически при выполнении определённых операций с объектом. Они позволяют переопределить стандартное поведение: доступ к несуществующим свойствам, вызов несуществующих методов, сериализацию, строковое представление и др.
Наиболее часто используемые:
__construct()— конструктор (уже рассмотрен).__destruct()— деструктор; вызывается при уничтожении объекта (например, при выходе из области видимости или явномunset). Используется для освобождения ресурсов (файловые дескрипторы, соединения).__get($name)и__set($name, $value)— вызываются при чтении и записи недоступного (например, несуществующего или приватного) свойства. Позволяют реализовать динамические свойства, lazy loading, прокси.__call($name, $arguments)и__callStatic($name, $arguments)— вызываются при вызове несуществующего метода экземпляра или статического метода соответственно. Основа для реализации прокси, фасадов, Fluent Interface.__toString()— вызывается при попытке использовать объект как строку (например, вecho). Должен возвращать строку, иначе — фатальная ошибка.__invoke()— позволяет вызывать объект как функцию:$obj().
Пример динамического доступа:
class Config {
private array $data = [];
public function __set(string $key, $value): void {
$this->data[$key] = $value;
}
public function __get(string $key) {
return $this->data[$key] ?? null;
}
}
$config = new Config();
$config->database = 'mysql'; // → вызов __set('database', 'mysql')
echo $config->database; // → вызов __get('database')
Магические методы мощны, но их следует использовать с осторожностью: они делают поведение объекта менее прозрачным, затрудняют статический анализ и могут скрывать ошибки (например, опечатка в имени свойства не вызовет ошибку, а создаст новое динамическое поле). В 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);
Анонимный класс может наследовать от другого класса, реализовывать интерфейсы, использовать трейты и содержать свойства и методы — как обычный класс. Отличие лишь в том, что он не имеет имени и объявляется 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() оба объекта ($user и clone $user) имели бы общую ссылку на один объект DateTime, и изменение времени у одного повлияло бы на другого.
Сериализация и десериализация
Сериализация — преобразование объекта в строку (обычно для хранения или передачи), десериализация — обратный процесс. В PHP это делается функциями serialize() и unserialize().
По умолчанию сериализуются все нестатические свойства, включая приватные (но с модификатором класса в имени для избежания конфликтов). Однако такое поведение не всегда приемлемо: например, ресурсы (соединения с БД) нельзя сериализовать.
Для контроля процесса используются магические методы:
__sleep()— вызывается перед сериализацией; должен вернуть массив имён свойств, подлежащих сохранению. Может использоваться для закрытия ресурсов.__wakeup()— вызывается после десериализации; восстанавливает состояние (например, переоткрывает соединения).
class DatabaseConnection {
private $pdo;
private string $dsn;
public function __construct(string $dsn) {
$this->dsn = $dsn;
$this->pdo = new PDO($dsn);
}
public function __sleep(): array {
return ['dsn']; // сохраняем только DSN
}
public function __wakeup(): void {
$this->pdo = new PDO($this->dsn); // восстанавливаем соединение
}
}
Начиная с 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);
}
Эти методы не подвержены проблемам с именованием приватных свойств и более совместимы с будущими версиями.