ООП в PHP: классы, объекты, интерфейсы и композиция

PHP / Архитектура

ООП в PHP: классы, объекты, интерфейсы и композиция

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

Опубликовано 04.09.2026 5 минут чтения
Содержание статьи

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

Не каждую функцию нужно превращать в класс. Простая чистая операция часто понятнее как функция. Класс оправдан, когда он выражает сущность, сервис или устойчивый контракт.

Первый класс

PHP
<?php​declare(strict_types=1);​final class Money{    public function __construct(        private int $amountInKopecks,        private string $currency = 'RUB',    ) {        if ($amountInKopecks < 0) {            throw new InvalidArgumentException('Сумма не может быть отрицательной.');        }​        if ($currency === '') {            throw new InvalidArgumentException('Укажите валюту.');        }    }​    public function amountInKopecks(): int    {        return $this->amountInKopecks;    }​    public function currency(): string    {        return $this->currency;    }​    public function add(self $other): self    {        if ($this->currency !== $other->currency) {            throw new DomainException('Нельзя складывать разные валюты.');        }​        return new self(            $this->amountInKopecks + $other->amountInKopecks,            $this->currency,        );    }}​$subtotal = new Money(10_000);$delivery = new Money(500);$total = $subtotal->add($delivery);​

Конструктор проверяет инварианты, типизированные свойства ограничивают форму состояния, private не позволяет менять сумму снаружи. Метод add() возвращает новый объект, поэтому исходные значения не изменяются.

final запрещает наследование, которое класс не проектировал. Это разумный default для небольшого доменного объекта.

Видимость

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

Публичное свойство легко перевести в недопустимое состояние:

PHP
final class Account{    public int $balance = 0;}​$account = new Account();$account->balance = -1_000_000;​

Лучше предоставить операции:

PHP
final class Account{    private int $balance = 0;​    public function deposit(int $amount): void    {        if ($amount <= 0) {            throw new InvalidArgumentException('Сумма пополнения должна быть положительной.');        }​        $this->balance += $amount;    }​    public function balance(): int    {        return $this->balance;    }}​

Инкапсуляция — не сокрытие ради сокрытия, а защита правил объекта.

Интерфейс

Интерфейс задаёт контракт без конкретной реализации:

PHP
interface Notifier{    public function send(string $recipient, string $message): void;}​

PHP
final class EmailNotifier implements Notifier{    public function send(string $recipient, string $message): void    {        // Здесь вызывается почтовый транспорт.    }}​

Сервис зависит от контракта:

PHP
final class PasswordResetService{    public function __construct(private Notifier $notifier)    {    }​    public function request(string $email): void    {        $message = 'Инструкция по восстановлению доступа.';        $this->notifier->send($email, $message);    }}​

Зависимость передаётся через конструктор. Это проще тестировать и менять, чем создание new EmailNotifier() внутри каждого метода.

Композиция

Композиция строит объект из других объектов. В примере PasswordResetService имеет Notifier. Такое отношение обычно гибче наследования.

PHP
$notifier = new EmailNotifier();$service = new PasswordResetService($notifier);​

Для теста можно передать реализацию, которая сохраняет сообщения в память:

PHP
final class InMemoryNotifier implements Notifier{    /** @var list<array{recipient: string, message: string}> */    public array $messages = [];​    public function send(string $recipient, string $message): void    {        $this->messages[] = [            'recipient' => $recipient,            'message' => $message,        ];    }}​

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

Наследование выражает отношение «является» и должно сохранять контракт родителя.

PHP
abstract class ReportExporter{    abstract public function contentType(): string;​    /** @param list<array<string, scalar|null>> $rows */    abstract public function export(array $rows): string;}​final class JsonReportExporter extends ReportExporter{    public function contentType(): string    {        return 'application/json; charset=UTF-8';    }​    public function export(array $rows): string    {        return json_encode(            $rows,            JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR,        );    }}​

Не создавайте SportsCar extends Car только ради демонстрации. В приложении наследование полезно, когда есть реальное общее поведение и взаимозаменяемость. Иначе интерфейс и композиция обычно яснее.

readonly

Для объекта, свойства которого задаются один раз:

PHP
readonly class ArticleId{    public function __construct(public int $value)    {        if ($value < 1) {            throw new InvalidArgumentException('ID должен быть положительным.');        }    }}​

readonly не делает вложенный объект глубоко неизменяемым: он запрещает переназначать свойства самого объекта.

Пространства имён и автозагрузка

PHP
namespace App\Service;​final class ArticlePublisher{}​

Пространства имён предотвращают конфликты. В большом проекте классы загружаются автозагрузчиком по соглашению о путях. Не подключайте десятки файлов вручную в случайных контроллерах; держите bootstrap как единую точку сборки приложения.

Частые ошибки

  • класс содержит только getters/setters и не защищает правила;
  • всё состояние объявлено public;
  • сервис сам создаёт конкретные зависимости;
  • наследование используют ради переиспользования пары строк;
  • в одном «God object» смешаны HTTP, SQL, письмо и шаблон;
  • исключение применяется для обычной ветки, которую проще выразить результатом;
  • DTO, сущность и запись базы считаются одним и тем же объектом без анализа.

Что запомнить

Хороший класс защищает допустимое состояние и имеет небольшую ответственность. Используйте типы, private, конструкторную проверку и явные методы. Зависите от интерфейсов там, где действительно нужна смена реализации, и предпочитайте композицию наследованию без отношения «является».

Сообщить об ошибке в статье

Рекомендации редакции

Другие публикации из этого раздела.

Новые материалы

Получайте практические разборы без информационного шума

Одна общая рассылка о новых статьях и инструментах. Адрес будет активироваться только после подтверждения.

Подписка появится после подключения сервиса рассылок и Yandex SmartCaptcha.