Skip to content

Visibility

Visibility determines where code may access a Property or Method. It helps define a Class’s public API and keeps implementation details behind a boundary.

Visibility External callers Declaring Class Child Classes
public Yes Yes Yes
protected No Yes Yes
private No Yes No direct access

The same access rules apply to Properties and Methods.

Private access is based on the Class containing the code, not the identity of the receiving Object. A Wallet Method may read a private Property of another Wallet instance:

final class Wallet
{
public function __construct(private int $balance) {}
public function hasMoreThan(Wallet $other): bool
{
return $this->balance > $other->balance;
}
}
$first = new Wallet(100);
$second = new Wallet(50);
var_dump($first->hasMoreThan($second)); // true

External code and an unrelated WalletService cannot access $balance directly. They must use an appropriate public operation or query.

Protected Members are accessible from the declaring Class and its Child Classes, but not directly from external callers:

class Wallet
{
protected int $balance = 100;
}
class PremiumWallet extends Wallet
{
public function balance(): int
{
return $this->balance;
}
}
$wallet = new PremiumWallet();
echo $wallet->balance(); // 100
// echo $wallet->balance; // Error: protected Property

Inheritance will be covered separately. Its existence alone does not justify protected State: a Child may use operations that preserve the Parent’s rules instead of editing its data directly.

Keep the balance private, expose withdrawal and reading as public operations, and keep internal validation private:

<?php
declare(strict_types=1);
final class Wallet
{
public function __construct(private int $balance)
{
if ($balance < 0) {
throw new InvalidArgumentException('Negative initial balance.');
}
}
public function withdraw(int $amount): void
{
$this->ensureCanWithdraw($amount);
$this->balance -= $amount;
}
private function ensureCanWithdraw(int $amount): void
{
if ($amount <= 0) {
throw new InvalidArgumentException('Amount must be positive.');
}
if ($amount > $this->balance) {
throw new LogicException('Insufficient balance.');
}
}
public function balance(): int
{
return $this->balance;
}
}

The helper either returns normally or throws; returning true adds no value when the caller ignores it. Its name describes both positive-amount and available-balance checks. Amounts represent a single currency’s smallest unit.

Extracting a helper organizes implementation. Making it private hides that detail from callers. The same invariant could also be protected by checks written directly inside withdraw().

  • Making every Member public by default expands the API callers depend on.
  • Assuming private means only the current instance may access the Member.
  • Making State protected only because a Child Class exists.
  • Naming a helper after only one of several checks it performs.
  • Treating visibility as a substitute for validating State transitions.

Use the narrowest access level that supports the intended collaboration. Public operations are necessary for callers; private details are easier to change without affecting them. Protected access creates a dependency for Child Classes, so introduce it deliberately.

Public exposes a Member to callers. Protected also supports access through the inheritance family. Private restricts access to code in the declaring Class, including access to other instances of that Class.

  1. Why can hasMoreThan() read another Wallet’s private balance?
  2. Can an external caller invoke ensureCanWithdraw()?
  3. Why does a protected Property remain inaccessible from outside the Class?
  4. Why is a small public API easier to maintain?