Инкапсуляция в PHP: почему состояние класса должно быть закрытым

Зачем это нужно

Инкапсуляция — первый кит ООП, и её чаще всего понимают неправильно: «это когда есть геттеры и сеттеры». На деле суть другая: данные и методы, которые с ними работают, живут внутри класса, а наружу торчат только «кнопки» — те операции, которые класс разрешает делать со своим состоянием.

Боль из практики: в проекте есть класс заказа со свойством public float $total. Где-то в контроллере прилетает строка кода $order->total = 0; — и скидка применяется к нулевой сумме. Никаких ошибок, логи чистые, а бухгалтерия в панике. Найти виновника — квест на полдня, потому что писать в public-свойство может кто угодно откуда угодно. Если бы $total был private, а изменения шли через метод с проверками — такая бага была бы невозможна по построению.

Как это работает

Модификаторы видимости определяют, кто может трогать свойство или метод:

  • public — доступен всем;
  • protected — только самому классу и наследникам;
  • private — только самому классу.

Разберём на классике — банковском счёте:

php
class BankAccount
{
    private float $balance = 0;

    public function deposit(float $amount): void
    {
        if ($amount <= 0) {
            throw new InvalidArgumentException('Сумма должна быть положительной');
        }
        $this->balance += $amount;
    }

    public function withdraw(float $amount): void
    {
        if ($amount <= 0) {
            throw new InvalidArgumentException('Сумма должна быть положительной');
        }
        if ($amount > $this->balance) {
            throw new DomainException('Недостаточно средств');
        }
        $this->balance -= $amount;
    }

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

$account = new BankAccount();
$account->deposit(100);
echo $account->getBalance(); // 100

// $account->balance = -9999; // Fatal error: cannot access private property

Счёт не может уйти в минус — не потому что кто-то «постарался и проверил это везде», а потому что другой возможности изменить баланс просто нет. Единственный вход — методы deposit и withdraw, и они проверяют данные сами.

Геттеры и сеттеры — не автоматом

Ошибочный рефлекс: «private + геттер + сеттер на каждое свойство = инкапсуляция». Нет. Сеттер без проверок — это то же public-свойство, только многословнее.

php
// ПЛОХО: сеттер-пустышка, никакой защиты
class Order
{
    private float $total;

    public function setTotal(float $total): void
    {
        $this->total = $total; // любое значение — даже -500
    }
}

// ХОРОШО: осмысленные операции домена вместо сырого сеттера
class Order
{
    private array $items = [];

    public function addItem(Product $product, int $qty): void
    {
        if ($qty <= 0) {
            throw new InvalidArgumentException('Количество должно быть больше нуля');
        }
        $this->items[] = ['product' => $product, 'qty' => $qty];
    }

    public function total(): float
    {
        // сумма считается из состояния, а не хранится отдельно —
        // значит, не может разойтись с реальным содержимым заказа
        return array_sum(array_map(
            fn ($i) => $i['product']->price * $i['qty'],
            $this->items
        ));
    }
}

Обратите внимание: total() — геттер по форме, но по сути это вычисление. Сумма заказа не хранится отдельным свойством, а считается из позиций. Исчезает целый класс багов «сумма разошлась с составом».

Как это выглядит в Laravel

Eloquent хранит данные модели в недоступном напрямую массиве $attributes, а доступ идёт через «магию» — и это готовый пример инкапсуляции:

php
// Мутатор: при записи данные приводятся к нужному виду
class User extends Model
{
    protected function casts(): array
    {
        return [
            'password' => 'hashed', // при присвоении автоматически хэшируется
            'is_admin' => 'boolean',
        ];
    }
}

$user->password = 'plain-text'; // в базу уйдёт хэш, а не открытый пароль

Присвоили строку — получили хэш. Модель сама контролирует своё состояние, и нам не нужно помнить «а хэшируем ли мы пароль перед сохранением». Аксессоры и мутаторы — это те самые «кнопки» для чтения и записи, только декларативные.

Плохо / Хорошо

ПлохоХорошо
public свойства, которые меняются откуда угодноprivate + методы-операции с проверками
Сеттер-пустышка setTotal() без валидацииОсмысленные методы домена: addItem(), applyDiscount()
Хранение вычислимых значений отдельным свойствомСчитать на лету в геттере — нечему расходиться
Раскиданные по коду проверки перед изменением свойстваПроверки внутри самого класса — их нельзя обойти

Подводный камень

  1. Сеттеры как костыль. Если у класса на каждое свойство есть сеттер — инкапсуляции нет, есть формальность. Спросите себя: «какие операции этот класс вообще позволяет делать с состоянием?» Их и оставляйте.
  2. Обход через наследование. protected открывает доступ наследникам — если класс разрастётся в иерархию, контроль ослабнет. Сомневаетесь — делайте private.
  3. Проверки «вокруг класса». Если валидация баланса живёт в контроллере, а не в классе — новый вызывающий код её скопирует? А десятый? Инвариант должен охранять сам класс.
  4. Массив $attributes как дыра. В Eloquent можно залезть в $model->attributes или использовать forceFill() — это легальные выходы для фреймворка, но не повод тянуть их в бизнес-код.

Мини-шпаргалка

php
// Закрытое состояние + операции с проверками
private float $balance = 0;

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

// Вычислимое значение — не хранить
public function total(): float { /* считается из items */ }

// Модификаторы
public    // всем
protected // классу и наследникам
private   // только классу

// Eloquent: контроль состояния через мутаторы
protected function casts(): array
{
    return ['password' => 'hashed'];
}

Три вывода

  1. Инкапсуляция — это не геттеры и сеттеры, а контроль каждого изменения состояния внутри самого класса.
  2. Если значение можно вычислить из состояния — не храните его отдельным свойством, вычисляйте в геттере.
  3. Проверки, охраняющие состояние, живут в классе, а не вокруг него: обойти их должно быть невозможно.