PHPVisibility (public, protected, private)

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

public

Anywhere — inside the class, subclasses, and outside code.

protected

Inside the class and any subclass, but not from outside code.

private

Only inside the exact class where it is declared — not even subclasses.

Product with all three visibility levels

PHP
<?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
<?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 below
Wireless 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
<?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
<?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%
private is invisible to subclasses too
It is easy to assume `private` behaves like `protected` but "a little stricter." It is stricter in a specific way that matters: `protected` members are visible to subclasses, but `private` members are not — not even to a class that extends the one declaring them. A subclass that needs to reuse a parent's internal state should have that state declared `protected`, not `private`.
  • 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 private or protected member from outside its allowed scope throws a fatal Error, not a warning.

  • Getters and setters are public methods that expose controlled, validated access to otherwise private state.

Defaulting to private
A common rule of thumb: declare properties `private` by default, and only widen to `protected` or `public` when you have an actual reason to. It is much easier to loosen a restriction later than to tighten one that other code has already come to depend on.
Tip
Write a setter even when it currently does nothing but assign the value. The day you need to add validation, logging, or a computed side effect, you add it in one place — the setter — instead of hunting down every spot in the codebase that once wrote to the property directly.