PHP: What is the difference between public, private and protected?
You find the table for this in every tutorial, and it is correct. But it
suggests one thing that is wrong: that private applies to the individual
object. It does not, and that is a good thing.
The basics
<?php
class Basis {
public string $oeffentlich = 'public';
protected string $geschuetzt = 'protected';
private string $privat = 'private';
}
?>
From outside:
$b->oeffentlich : 'public'
$b->geschuetzt : Error: Cannot access protected property Basis::$geschuetzt
$b->privat : Error: Cannot access private property Basis::$privat
From inside the class everything works:
vonInnen() : ["public","protected","private"]
Note that this is an Error, not a warning. So the access aborts — unlike
with a property that does not exist at all.
In a derived class
And here it already gets more interesting:
Warning: Undefined property: Kind::$privat
ausDerAbleitung() : {"oeffentlich":"public","geschuetzt":"protected","privat":null}
protected works, private does not. So far as expected. What is interesting
is the kind of failure: it is not an access error but a
Warning: Undefined property.
The difference matters. private is not inherited — the property simply does
not exist in Kind. So you do not get an Error that stops your script, but a
warning and a null that gets happily carried forward.
That is the more dangerous of the two cases.
Now for the part that surprises
private applies to the class, not to the object. Which means: an object
may access the private properties of another object of the same class.
<?php
class Konto {
public function __construct(private int $stand) {}
public function vergleiche(Konto $anderes): string {
return $this->stand . ' gegen ' . $anderes->stand; // works!
}
}
?>
$a1->vergleiche($a2) : 100 gegen 250
$anderes->stand is a private property of a foreign object, and the
access is permitted.
Seeing this for the first time it looks like a hole in the encapsulation. But
it is the precondition for being able to write sensible classes at all. Every
equals() method, every comparison, every add() method that combines two
objects of the same class needs exactly this:
<?php
class Money {
public function __construct(private int $cents) {}
public function plus(Money $other): self {
return new self($this->cents + $other->cents);
}
public function equals(Money $other): bool {
return $this->cents === $other->cents;
}
}
?>
Without this rule Money would need a public getter for $cents — and then
the encapsulation would be gone for good.
The same applies to protected, all the way up to the common base class:
LinksKind reads Eltern instance : 'ererbt'
LinksKind reads RechtsKind instance : 'ererbt'
So a class may also access protected properties of a sibling class, as long
as both descend from the same base.
The case worth knowing about: __get()
This is the subtlest point in this article.
<?php
class Magisch {
private array $daten = ['a' => 1];
public function __get(string $n): mixed {
return $this->daten[$n] ?? null;
}
}
?>
$m->a : 1 [__get('a') was called]
$m->daten : NULL [__get('daten') was called]
$m->daten is private. Normally access from outside would be an Error.
But because a __get() exists, PHP calls that instead — and here it
returns null.
A loud error turns into a quiet wrong value. This is exactly why magic methods
are so unpleasant when debugging: a typo in a property name does not lead to a
crash but to a null that causes trouble somewhere further down.
If you use __get(), have it throw an exception for unknown names rather than
returning null:
<?php
public function __get(string $n): mixed {
if (!array_key_exists($n, $this->data)) {
throw new InvalidArgumentException("Unknown property: $n");
}
return $this->data[$n];
}
?>
Widening yes, narrowing no
When overriding, visibility may become wider but not narrower:
<?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
That makes sense: whoever receives a Basis object relies on machWas() being
callable. If narrowing were permitted, a derived class could break that
promise.
The other way round works: protected may become public in a derived class.
What constructor promotion gives you
Since PHP 8.0 all of this can be shortened into the constructor:
<?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 is the interesting addition here: the property is readable from
outside but cannot be changed after the constructor. So for immutable values
you no longer need a getter — public readonly is enough.
What is visible from outside
To check when you are unsure:
get_object_vars from outside : ["oeffentlich"]
get_object_vars from inside : ["oeffentlich","geschuetzt","privat"]
get_object_vars() shows what is reachable from the respective place in the
code. The same applies to json_encode(), which only takes the public
properties along — something that regularly surprises people when an API
response is suddenly empty.
What I do in practice
- Properties
privateby default. If a derived class needs one, it becomesprotected— but only then. - Methods
publicwhen they belong to the interface, otherwiseprivate. public readonlyinstead of getters for immutable values.protectedsparingly. It is a promise to every future derived class and therefore almost as binding aspublic.
The rule of thumb behind it: anything that is not private you cannot easily
change later. So you start narrow and open up when it becomes necessary.
Summary
publiceverywhere,protectedin the class and its derived classes,privateonly in the class itself.privateis class-scoped, not object-scoped: an object sees the private properties of other instances of the same class.privateis not inherited — access from a derived class is a warning withnull, not an error.- An existing
__get()replaces the access error with a silent return value. - Visibility can only be widened when overriding.
public readonlyreplaces the classic getter.
About Netcup (advertisement)
The German host Netcup offers, among other things, affordable and powerful web hosting packages, KVM-based root servers and dedicated servers. With our voucher codes you can save even more (6€ off your first order, 30% off all KVM-based root servers, ...).