Объектно-ориентированное программирование на PHP — это способ строить программу из объектов: каждый объект хранит свои данные (свойства) и умеет с ними работать (методы), а описывается классом. Ниже — рабочие примеры на одном сюжете (товары и заказ интернет-магазина): класс и объект, конструктор, области видимости, наследование, абстрактные классы, интерфейсы, трейты, readonly и enum.
Содержание
Весь код проверен 30 сентября 2026 года на PHP 8.4.25 и на текущей ветке PHP 8.5 (8.5.11, CLI) — вывод одинаковый. Возможности, которых нет в более старых версиях, подписаны номером версии.
Четыре термина, которые легко спутать:
- класс — описание: какие свойства и методы будут у объектов;
- объект (экземпляр) — конкретное значение, созданное по классу через
new; - свойство — переменная внутри объекта;
- метод — функция внутри класса, внутри нее текущий объект доступен как
$this.
Класс и объект: минимальный рабочий пример
Класс похож на бланк накладной, объект — на заполненную накладную. Сохраните код в product.php и запустите php product.php.
<?php
declare(strict_types=1);
class Product
{
public function __construct(
public readonly string $title,
private int $priceKopecks,
) {
if ($priceKopecks < 0) {
throw new InvalidArgumentException('Цена не может быть отрицательной');
}
}
public function price(): int
{
return $this->priceKopecks;
}
public function discount(int $percent): void
{
if ($percent < 0 || $percent > 100) {
throw new InvalidArgumentException('Скидка - от 0 до 100 процентов');
}
$this->priceKopecks = intdiv($this->priceKopecks * (100 - $percent), 100);
}
}
$book = new Product('Книга по PHP', 150000);
$pen = new Product('Ручка', 9900);
$book->discount(10);
echo $book->title, ': ', $book->price() / 100, " руб.\n";
echo $pen->title, ': ', $pen->price() / 100, " руб.\n";
В консоли появятся две строки: скидка изменила только книгу, потому что у каждого объекта свой набор значений.
Книга по PHP: 1350 руб.
Ручка: 99 руб.
Цена хранится целым числом копеек: дробные рубли во float дают ошибки округления.
Конструктор и сокращенная запись свойств
__construct — метод, который PHP вызывает сам при new Product(...). Его задача — привести объект в корректное состояние, поэтому проверка цены стоит именно здесь: объект с отрицательной ценой создать нельзя.
Модификатор перед параметром конструктора (public, private, protected) одновременно объявляет свойство и записывает в него аргумент. Это constructor property promotion, доступно с PHP 8.0. Без него пришлось бы отдельно объявить свойство и написать $this->title = $title;.
Области видимости и readonly
| Модификатор | Откуда доступно |
|---|---|
public |
из любого кода |
protected |
из самого класса и его наследников |
private |
только из класса, где объявлено |
readonly (PHP 8.1) разрешает записать свойство один раз, обычно в конструкторе. Снаружи класса это сделать нельзя; до PHP 8.3 включительно инициализировать свойство мог только класс, где оно объявлено, с PHP 8.4 — и его наследник. Граница: запрет касается присваивания свойству. Если в readonly-свойстве лежит другой объект, его внутреннее состояние менять по-прежнему можно.
Что будет при нарушении правил — три попытки после кода выше:
$book->priceKopecks = 1;
// Error: Cannot access private property Product::$priceKopecks
$book->title = 'Другая книга';
// Error: Cannot modify readonly property Product::$title
new Product('Ручка', '99');
// TypeError: Product::__construct(): Argument #2 ($priceKopecks)
// must be of type int, string given
Каждая строка бросает ошибку и останавливает скрипт, если ее не перехватить. Error и TypeError не наследуют Exception, поэтому catch (Exception $e) их не поймает — нужен catch (Error $e) или общий catch (Throwable $e). Исправление — работать через публичные методы (price(), discount()) и передавать значения нужного типа. Третья ошибка возникает из-за declare(strict_types=1): без этой директивы PHP молча превратил бы строку '99' в число.
Скрытие данных за методами называют инкапсуляцией: правило «цена не отрицательна, скидка от 0 до 100» живет в одном месте, и обойти его снаружи нельзя.
Объект не копируется при присваивании
$same = $book; // вторая переменная для того же объекта
$copy = clone $book; // отдельная копия
$same->discount(50);
echo $book->price(), ' ', $copy->price(), "\n"; // 67500 135000
Переменная хранит не сам объект, а его идентификатор. Поэтому объект, переданный в функцию, меняется и для вызывающего кода. clone делает поверхностную копию: вложенные объекты остаются общими, пока вы не опишете их копирование в методе __clone.
Наследование, абстрактные классы и интерфейсы
Наследование (extends) — это отношение «является разновидностью»: дочерний класс получает свойства и методы родителя и может их дополнить или переопределить. У класса в PHP только один родитель.
<?php
declare(strict_types=1);
interface HasPrice
{
public function price(): int;
}
abstract class Item implements HasPrice
{
public function __construct(
protected string $title,
protected int $basePrice,
) {
}
abstract public function delivery(): int;
public function price(): int
{
return $this->basePrice + $this->delivery();
}
public function label(): string
{
return static::class . ' «' . $this->title . '»';
}
}
class PaperBook extends Item
{
public function delivery(): int
{
return 30000;
}
}
class EBook extends Item
{
public function delivery(): int
{
return 0;
}
#[\Override]
public function label(): string
{
return parent::label() . ' (файл)';
}
}
final class GiftCard implements HasPrice
{
public function __construct(private int $amount)
{
}
public function price(): int
{
return $this->amount;
}
}
function total(HasPrice ...$items): int
{
$sum = 0;
foreach ($items as $item) {
$sum += $item->price();
}
return $sum;
}
$paper = new PaperBook('PHP 8', 150000);
$ebook = new EBook('PHP 8', 90000);
echo $paper->label(), ' - ', $paper->price(), "\n";
echo $ebook->label(), ' - ', $ebook->price(), "\n";
echo 'Итого: ', total($paper, $ebook, new GiftCard(50000)), "\n";
PaperBook «PHP 8» - 180000
EBook «PHP 8» (файл) - 90000
Итого: 320000
Что здесь происходит:
Item— абстрактный класс: общий код (price(),label()) плюс методdelivery()без тела, который обязан написать каждый наследник. Создать самItemнельзя:new Item('x', 1)даетError: Cannot instantiate abstract class Item.HasPrice— интерфейс: только договор «у объекта естьprice(): int».GiftCardне товар и не наследуетItem, но договор выполняет.total()принимает любые объекты с этим интерфейсом и не знает их классов. Один вызов$item->price()исполняет разный код — это и есть полиморфизм.parent::label()вызывает версию родителя,static::classдает имя класса реального объекта.#[\Override](PHP 8.3) просит PHP проверить, что метод действительно переопределяет родительский. При опечаткеlable()скрипт не запустится:Fatal error: B::lable() has #[\Override] attribute, but no matching parent method exists.finalзапрещает наследовать класс.
Перегрузки методов по набору параметров, как в Java или C#, в PHP нет: два метода с одним именем в классе объявить нельзя. Вместо нее используют параметры по умолчанию, объединенные типы (int|string) или отдельные методы с говорящими именами.
Трейты, enum и readonly-классы
Третий пример собирает заказ и показывает возможности PHP 8.1-8.4.
<?php
declare(strict_types=1);
trait HasHistory
{
private array $history = [];
protected function remember(string $event): void
{
$this->history[] = $event;
}
public function history(): array
{
return $this->history;
}
}
enum OrderStatus: string
{
case New = 'new';
case Paid = 'paid';
case Shipped = 'shipped';
public function label(): string
{
return match ($this) {
self::New => 'Новый',
self::Paid => 'Оплачен',
self::Shipped => 'Отправлен',
};
}
}
final readonly class Money
{
public function __construct(
public int $kopecks,
public string $currency = 'RUB',
) {
}
public function add(Money $other): self
{
if ($other->currency !== $this->currency) {
throw new InvalidArgumentException('Разные валюты');
}
return new self($this->kopecks + $other->kopecks, $this->currency);
}
}
final class Order
{
use HasHistory;
public private(set) OrderStatus $status = OrderStatus::New;
public function __construct(public readonly Money $total)
{
}
public function pay(): void
{
if ($this->status !== OrderStatus::New) {
throw new LogicException('Оплатить можно только новый заказ');
}
$this->status = OrderStatus::Paid;
$this->remember('Оплата');
}
}
$sum = (new Money(150000))->add(new Money(9900));
$order = new Order($sum);
$order->pay();
echo $order->status->label(), ', ', $order->total->kopecks / 100, " руб.\n";
echo implode(', ', $order->history()), "\n";
echo OrderStatus::from('shipped')->label(), "\n";
var_dump(OrderStatus::tryFrom('lost'));
Оплачен, 1599 руб.
Оплата
Отправлен
NULL
- Трейт (
trait+use) — готовый кусок кода, который PHP вставляет в класс. Он не создает отношения «является» и типом не служит:$order instanceof HasHistoryвернетfalse. - Enum (PHP 8.1) — тип с закрытым списком значений. Свойство
$statusне может содержать ничего, кроме трех вариантов, а опечатка в имени варианта — ошибка, а не «тихая» неверная строка.from()превращает значение из базы или формы в вариант и бросаетValueError: "lost" is not a valid backing value for enum OrderStatus;tryFrom()в той же ситуации возвращаетnull. - readonly-класс (PHP 8.2) делает все свойства
readonly.Money— объект-значение:add()не меняет сумму, а возвращает новую. Попытка$sum->kopecks = 0даетError: Cannot modify readonly property Money::$kopecks. public private(set)(PHP 8.4) — читать свойство можно отовсюду, записывать только внутри класса. Строка$order->status = OrderStatus::Shipped;снаружи даетError: Cannot modify private(set) property Order::$status from global scope. Повторный$order->pay()броситLogicException.
Что выбрать: абстрактный класс, интерфейс или трейт
| Абстрактный класс | Интерфейс | Трейт | |
|---|---|---|---|
| Сколько можно подключить | один родитель | несколько | несколько |
| Содержит реализацию | да | нет (сигнатуры методов и константы, с PHP 8.4 еще объявления свойств без хранения данных) | да |
| Хранит свойства с данными | да | нет | да |
| Служит типом параметра | да | да | нет |
| Когда брать | у близких классов общий код и состояние | нужен договор для несвязанных классов | одинаковый код в классах без общего родителя |
Практическое правило для начала: тип параметра функции — интерфейс, общий код родственных классов — абстрактный класс, трейт — когда первых двух не хватает. Глубокие цепочки наследования трудно менять, поэтому объект часто получает другой объект в конструкторе (композиция), как Order получает Money.
Выводы
- Класс описывает свойства и методы, объект создается через
new; переменная хранит идентификатор объекта, копию делаетclone. - Конструктор с promotion (PHP 8.0) объявляет и заполняет свойства в одной записи; проверки в нем не дают создать некорректный объект.
private,protected,readonlyиprivate(set)закрывают данные от внешнего кода: изменить состояние можно только через методы класса.- Наследование одно (
extends), интерфейсов и трейтов может быть несколько; полиморфизм — вызов одного метода интерфейса у объектов разных классов. enumзаменяет строковые константы статусов закрытым типом,readonly classподходит для объектов-значений.
Где применяется / связь с практикой
На классах построены современные PHP-фреймворки и библиотеки: контроллер, модель, сервис отправки почты, запрос и ответ HTTP — это объекты, а контейнер зависимостей связывает их через интерфейсы. Без уверенного владения ООП код фреймворка прочитать трудно, а тест на сервис не написать: подмена зависимости в тесте работает именно за счет интерфейса.
Освойте тему на практике
Проектирование классов, паттерны, тестирование и работа с фреймворками разбираются на курсе «PHP-разработчик. Продвинутый уровень». Познакомиться с форматом занятий можно на открытых уроках Otus.
Смежные темы: принципы и особенности ООП, что нового в PHP 8.
FAQ
С какой версии в PHP есть ООП?
Классы были уже в PHP 3 и 4, но в урезанном виде. Области видимости, интерфейсы, абстрактные классы и исключения появились в PHP 5 (2004 год), трейты — в PHP 5.4.
Чем self отличается от static?
self указывает на класс, в котором написан код, static — на класс, у которого метод реально вызван (позднее статическое связывание). В примере static::class в Item вернул PaperBook и EBook, а self::class вернул бы Item.
Можно ли обойтись без геттеров и сеттеров?
Часто да: public readonly закрывает чтение без изменения, private(set) в PHP 8.4 — чтение снаружи и запись внутри. Метод нужен там, где изменение сопровождается проверкой или действием, как pay() у заказа.



