PHP: Was ist der Unterschied zwischen public, private und protected?

Die Tabelle dazu findet man in jedem Tutorial, und sie stimmt auch. Sie legt aber eine Sache nahe, die falsch ist: dass private sich auf das einzelne Objekt bezieht. Das tut es nicht, und das ist gut so.

Die Grundlagen

<?php
class Basis {
  public    string $oeffentlich = 'public';
  protected string $geschuetzt  = 'protected';
  private   string $privat      = 'private';
}
?>

Von außen:

$b->oeffentlich : 'public'
$b->geschuetzt  : Error: Cannot access protected property Basis::$geschuetzt
$b->privat      : Error: Cannot access private property Basis::$privat

Aus der Klasse selbst heraus geht alles:

vonInnen() : ["public","protected","private"]

Beachte, dass es sich um einen Error handelt, nicht um eine Warnung. Der Zugriff bricht also ab — anders als bei einer Eigenschaft, die es gar nicht gibt.

In einer abgeleiteten Klasse

Und hier wird es schon interessanter:

Warning: Undefined property: Kind::$privat
ausDerAbleitung() : {"oeffentlich":"public","geschuetzt":"protected","privat":null}

protected funktioniert, private nicht. So weit erwartbar. Interessant ist aber die Art des Fehlers: Es ist kein Zugriffsfehler, sondern eine Warning: Undefined property.

Der Unterschied ist wichtig. private wird nicht vererbt — die Eigenschaft existiert in Kind schlicht nicht. Deshalb bekommst du keinen Error, der dein Skript anhält, sondern eine Warning und ein null, mit dem munter weitergerechnet wird.

Das ist der gefährlichere von beiden Fällen.

Jetzt der Teil, der überrascht

private bezieht sich auf die Klasse, nicht auf das Objekt. Das heißt: Ein Objekt darf auf die privaten Eigenschaften eines anderen Objekts derselben Klasse zugreifen.

<?php
class Konto {
  public function __construct(private int $stand) {}

  public function vergleiche(Konto $anderes): string {
    return $this->stand . ' gegen ' . $anderes->stand;   // funktioniert!
  }
}
?>
$a1->vergleiche($a2) : 100 gegen 250

$anderes->stand ist eine private Eigenschaft eines fremden Objekts, und der Zugriff ist erlaubt.

Wenn man das zum ersten Mal sieht, wirkt es wie ein Loch in der Kapselung. Es ist aber die Voraussetzung dafür, dass man überhaupt vernünftige Klassen schreiben kann. Jede equals()-Methode, jeder Vergleich, jede add()-Methode, die zwei Objekte derselben Klasse verrechnet, braucht genau das:

<?php
class Geldbetrag {
  public function __construct(private int $cent) {}

  public function plus(Geldbetrag $anderer): self {
    return new self($this->cent + $anderer->cent);
  }

  public function gleich(Geldbetrag $anderer): bool {
    return $this->cent === $anderer->cent;
  }
}
?>

Ohne diese Regel müsste Geldbetrag einen öffentlichen Getter für $cent haben — und dann wäre die Kapselung erst recht dahin.

Bei protected gilt dasselbe, und zwar bis hinauf zur gemeinsamen Basisklasse:

LinksKind liest Eltern-Instanz     : 'ererbt'
LinksKind liest RechtsKind-Instanz : 'ererbt'

Eine Klasse darf also auch auf protected Eigenschaften einer Geschwisterklasse zugreifen, solange beide von derselben Basis abstammen.

Der Fall, den man kennen sollte: __get()

Das ist der subtilste Punkt in diesem Artikel.

<?php
class Magisch {
  private array $daten = ['a' => 1];

  public function __get(string $n): mixed {
    return $this->daten[$n] ?? null;
  }
}
?>
$m->a     : 1        [__get('a') wurde aufgerufen]
$m->daten : NULL     [__get('daten') wurde aufgerufen]

$m->daten ist private. Normalerweise wäre der Zugriff von außen ein Error. Weil aber ein __get() existiert, ruft PHP stattdessen dieses auf — und das liefert hier null.

Aus einem lauten Fehler wird ein stiller falscher Wert. Genau deshalb sind magische Methoden beim Debuggen so unangenehm: Ein Tippfehler im Eigenschaftsnamen führt nicht zu einem Absturz, sondern zu einem null, das irgendwo weiter unten Ärger macht.

Wenn du __get() verwendest, lass es bei unbekannten Namen eine Exception werfen statt null zurückzugeben:

<?php
  public function __get(string $n): mixed {
    if (!array_key_exists($n, $this->daten)) {
      throw new InvalidArgumentException("Unbekannte Eigenschaft: $n");
    }
    return $this->daten[$n];
  }
?>

Lockern ja, verschärfen nein

Beim Überschreiben darf die Sichtbarkeit weiter werden, aber nicht enger:

<?php
class Basis { public function machWas(): void {} }
class Kind extends Basis {
  protected function machWas(): void {}   // Fatal Error
}
?>
Fatal error: Access level to Kind::machWas() must be public
(as in class Basis) in /verstoss.php on line 8

Das ergibt Sinn: Wer ein Basis-Objekt entgegennimmt, verlässt sich darauf, dass machWas() aufrufbar ist. Wäre die Verschärfung erlaubt, könnte eine Ableitung diese Zusage brechen.

Umgekehrt geht es: Aus protected darf in der Ableitung public werden.

Was man mit Konstruktor-Promotion bekommt

Seit PHP 8.0 lässt sich das alles im Konstruktor abkürzen:

<?php
class Server {
  public function __construct(
    public readonly string $name,
    private int $portNr = 22,
  ) {}

  public function port(): int { return $this->portNr; }
}
?>
$s->name   : web01
$s->port() : 22
$s->portNr : Error: Cannot access private property Server::$portNr
$s->name = "neu" : Error: Cannot modify readonly property Server::$name

readonly ist dabei die interessante Ergänzung: Die Eigenschaft ist von außen lesbar, aber nach dem Konstruktor nicht mehr änderbar. Damit braucht man für unveränderliche Werte keinen Getter mehr — public readonly reicht.

Was von außen sichtbar ist

Zum Nachsehen, wenn man unsicher ist:

get_object_vars von aussen : ["oeffentlich"]
get_object_vars von innen  : ["oeffentlich","geschuetzt","privat"]

get_object_vars() zeigt, was an der jeweiligen Stelle im Code erreichbar ist. Dasselbe gilt für json_encode(), das nur die öffentlichen Eigenschaften mitnimmt — was einen regelmäßig überrascht, wenn eine API-Antwort plötzlich leer ist.

Was ich in der Praxis mache

  • Eigenschaften grundsätzlich private. Wenn eine Ableitung sie braucht, wird sie zu protected — aber erst dann.
  • Methoden public, wenn sie zur Schnittstelle gehören, sonst private.
  • Für unveränderliche Werte public readonly statt Getter.
  • protected sparsam. Es ist eine Zusage an alle künftigen Ableitungen und damit fast so bindend wie public.

Die Faustregel dahinter: Alles, was nicht private ist, kannst du später nicht mehr ohne Weiteres ändern. Deshalb fängt man eng an und öffnet, wenn es nötig wird.

Zusammenfassung

  • public überall, protected in der Klasse und ihren Ableitungen, private nur in der Klasse selbst.
  • private ist klassenbezogen, nicht objektbezogen: Ein Objekt sieht die privaten Eigenschaften anderer Instanzen derselben Klasse.
  • private wird nicht vererbt — der Zugriff in einer Ableitung ist eine Warning mit null, kein Error.
  • Ein vorhandenes __get() ersetzt den Zugriffsfehler durch einen stillen Rückgabewert.
  • Die Sichtbarkeit lässt sich beim Überschreiben nur lockern.
  • public readonly ersetzt den klassischen Getter.

Hinweis zu Netcup (Werbung)

Der deutsche Hoster Netcup bietet unter anderem günstige und zugleich leistungsstarke Webhosting Pakete, KVM-basierte Root Server und dezidierte Server an. Mit unseren Gutscheincodes kannst du noch mehr Geld sparen (6€ bei deiner ersten Bestellung, 30% Rabatt auf alle KVM-basierten Root Server, ...).