Инкапсуляция в PHP: почему состояние класса должно быть закрытым
Зачем это нужно
Инкапсуляция — первый кит ООП, и её чаще всего понимают неправильно: «это когда есть геттеры и сеттеры». На деле суть другая: данные и методы, которые с ними работают, живут внутри класса, а наружу торчат только «кнопки» — те операции, которые класс разрешает делать со своим состоянием.
Боль из практики: в проекте есть класс заказа со свойством public float $total. Где-то в контроллере прилетает строка кода $order->total = 0; — и скидка применяется к нулевой сумме. Никаких ошибок, логи чистые, а бухгалтерия в панике. Найти виновника — квест на полдня, потому что писать в public-свойство может кто угодно откуда угодно. Если бы $total был private, а изменения шли через метод с проверками — такая бага была бы невозможна по построению.
Как это работает
Модификаторы видимости определяют, кто может трогать свойство или метод:
public— доступен всем;protected— только самому классу и наследникам;private— только самому классу.
Разберём на классике — банковском счёте:
phpclass 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() |
| Хранение вычислимых значений отдельным свойством | Считать на лету в геттере — нечему расходиться |
| Раскиданные по коду проверки перед изменением свойства | Проверки внутри самого класса — их нельзя обойти |
Подводный камень
- Сеттеры как костыль. Если у класса на каждое свойство есть сеттер — инкапсуляции нет, есть формальность. Спросите себя: «какие операции этот класс вообще позволяет делать с состоянием?» Их и оставляйте.
- Обход через наследование.
protectedоткрывает доступ наследникам — если класс разрастётся в иерархию, контроль ослабнет. Сомневаетесь — делайтеprivate. - Проверки «вокруг класса». Если валидация баланса живёт в контроллере, а не в классе — новый вызывающий код её скопирует? А десятый? Инвариант должен охранять сам класс.
- Массив
$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'];
}
Три вывода
- Инкапсуляция — это не геттеры и сеттеры, а контроль каждого изменения состояния внутри самого класса.
- Если значение можно вычислить из состояния — не храните его отдельным свойством, вычисляйте в геттере.
- Проверки, охраняющие состояние, живут в классе, а не вокруг него: обойти их должно быть невозможно.