Encapsulation — hiding internal state behind a public interface
Concept
Encapsulation — bundling data and the methods that operate on that data together in one unit (a class), and controlling access to the internals via visibility modifiers.
What it is: The object hides its internal state. External code cannot directly read or modify the internals. All interaction goes through a defined public interface.
Why it matters:
- Internal representation can change freely: If
BankAccountstores balance in cents internally but exposes it in dollars publicly, you can change the storage format without breaking any code that uses the class. - Invariants are maintained: The class can validate all modifications to its state.
withdraw($amount)can check that balance doesn't go negative. Direct$account->balance -= $amountbypasses the check. - Reduces coupling: Code that depends on a class's public interface doesn't break when you change private implementation.
Visibility in PHP:
public: Accessible from anywhere.protected: Accessible from the class and its subclasses.private: Accessible only from within the class. Tightest encapsulation.
PHP 8.1+ readonly: readonly properties can only be set once (in the constructor). Enforces immutability — a stronger form of encapsulation.
Getters and setters: A naive approach that adds encapsulation syntax without the benefit. getBalance() + setBalance($x) is worse than just public $balance. Prefer behavior methods: deposit($amount), withdraw($amount) — they maintain invariants.
Law of Demeter ("tell, don't ask"): Don't reach into an object's internals — tell it what to do. $order->confirm() not $order->getStatus()->setConfirmed(true).
Code Example
<?php
// ❌ No encapsulation — public fields, no invariant protection
class BankAccountBad
{
public float $balance = 0;
// Anyone can do: $account->balance = -9999; — no protection!
}
// ✅ Encapsulated — private state, public behavior
class BankAccount
{
private float $balanceCents; // stored in cents internally
private array $transactions = [];
public function __construct(float $initialBalance = 0.0)
{
if ($initialBalance < 0) throw new \InvalidArgumentException('Initial balance cannot be negative');
$this->balanceCents = (int) ($initialBalance * 100);
}
public function deposit(float $amount): void
{
if ($amount <= 0) throw new \InvalidArgumentException('Deposit amount must be positive');
$this->balanceCents += (int) ($amount * 100);
$this->transactions[] = ['type' => 'deposit', 'amount' => $amount, 'at' => now()];
}
public function withdraw(float $amount): void
{
if ($amount <= 0) throw new \InvalidArgumentException('Amount must be positive');
if ($amount * 100 > $this->balanceCents) throw new \DomainException('Insufficient funds');
$this->balanceCents -= (int) ($amount * 100);
$this->transactions[] = ['type' => 'withdrawal', 'amount' => $amount, 'at' => now()];
}
// Getter — read-only view of internal state, in dollars
public function getBalance(): float { return $this->balanceCents / 100; }
// Read-only view of history — returns a COPY, not the internal array
public function getTransactions(): array { return $this->transactions; }
// Behavior method — tells the account to do something
public function transfer(BankAccount $to, float $amount): void
{
$this->withdraw($amount); // validates here
$to->deposit($amount);
}
}
// External code can only interact through the public interface
$account = new BankAccount(100.00);
$account->deposit(50);
$account->withdraw(30);
echo $account->getBalance(); // 120.00
// $account->balanceCents = -9999; // Fatal error — private!
// $account->transactions[] = [...]; // Fatal error — private!
// PHP 8.1 readonly — encapsulated via immutability
class Money
{
public function __construct(
public readonly int $amount, // cannot be changed after construction
public readonly string $currency,
) {}
}
$price = new Money(100, 'USD');
// $price->amount = 200; // Fatal error: Cannot modify readonly property