Visibility (public, protected, private)
Every property and method in a PHP class carries a visibility keyword — public, protected, or private — that controls where it can be accessed from. This is PHP's mechanism for encapsulation: deciding which parts of an object are the class's own business and which parts are meant to be used by the rest of the program. Getting visibility right is what keeps a class's internal details free to change later without breaking everything that uses it.
The three levels
Visibility | Accessible from |
|---|---|
| Anywhere — inside the class, subclasses, and outside code. |
| Inside the class and any subclass, but not from outside code. |
| Only inside the exact class where it is declared — not even subclasses. |
Product with all three visibility levels
<?php
class Product
{
public string $name; // anyone can read/write this
protected float $taxRate; // only Product and its subclasses
private float $price; // only Product itself
public function __construct(string $name, float $price, float $taxRate = 0.08)
{
$this->name = $name;
$this->price = $price;
$this->taxRate = $taxRate;
}
}Why restrict access at all
A public property can be set to anything, from anywhere, at any
time, with zero opportunity for the class to react or validate. If
$price were public, any code anywhere in the application could do
$product->price = -50;, and Product would have no say in the
matter. Marking $price private means the only code allowed to
change it is code written inside Product itself — which means the
class's own methods can validate, log, or otherwise guard every
change, and outside code is forced to go through them.
Accessing a private property from outside fails
Trying to read or write a private property from outside its class is not a warning or a quiet no-op — it is a fatal Error that stops the script.
Reaching for a private property directly
<?php
class Product
{
public string $name;
private float $price;
public function __construct(string $name, float $price)
{
$this->name = $name;
$this->price = $price;
}
}
$product = new Product('Wireless Mouse', 25.00);
echo $product->name . "\n"; // fine, $name is public
echo $product->price; // fatal error, see belowWireless Mouse PHP Fatal error: Uncaught Error: Cannot access private property Product::$price
PHP names the exact class and property in the error message, which makes this one of the easier fatal errors to diagnose: it is always telling you that some code outside the class tried to reach in past a private (or protected) boundary.
The getter/setter pattern
Since outside code cannot touch $price directly, Product exposes controlled access through ordinary public methods — conventionally called a getter (reads the value) and a setter (writes the value, usually after validating it). This is the standard way to let outside code work with private state without ever handing over unrestricted access to it.
getPrice() and setPrice() control access to $price
<?php
class Product
{
public string $name;
private float $price;
public function __construct(string $name, float $price)
{
$this->name = $name;
$this->setPrice($price);
}
public function getPrice(): float
{
return $this->price;
}
public function setPrice(float $price): void
{
if ($price < 0) {
throw new InvalidArgumentException('Price cannot be negative.');
}
$this->price = $price;
}
}
$product = new Product('Wireless Mouse', 25.00);
echo $product->getPrice() . "\n";
$product->setPrice(19.99);
echo $product->getPrice() . "\n";
try {
$product->setPrice(-5);
} catch (InvalidArgumentException $e) {
echo 'Rejected: ' . $e->getMessage() . "\n";
}25 19.99 Rejected: Price cannot be negative.
setPrice() is the single gatekeeper for changing $price, so the negative-price rule lives in exactly one place and applies no matter who calls it — the constructor even routes through setPrice() itself, so a negative price is rejected at creation time too, not just on later updates.
protected: sharing with subclasses only
protected sits between the other two: a subclass can reach a protected member as freely as if it were its own, but outside code still cannot touch it. This is useful for details a class wants to share with things that extend it, without exposing them to the whole program.
A subclass reading a protected property
<?php
class Product
{
protected float $taxRate = 0.08;
}
class DigitalProduct extends Product
{
public function describeTaxRate(): string
{
// allowed: DigitalProduct extends Product, so protected is visible
return 'Tax rate: ' . ($this->taxRate * 100) . '%';
}
}
$ebook = new DigitalProduct();
echo $ebook->describeTaxRate();Tax rate: 8%
public— accessible from anywhere; use it for the intentional, stable interface of a class.protected— accessible inside the class and its subclasses only; use it for internals meant to be shared with subclasses.private— accessible only inside the exact declaring class; use it for details nothing outside should ever touch directly.Accessing a
privateorprotectedmember from outside its allowed scope throws a fatalError, not a warning.Getters and setters are
publicmethods that expose controlled, validated access to otherwiseprivatestate.