Reader Stacks

Access Modifiers in PHP: public, protected, and private

The three visibility keywords control where a property or method can actually be accessed from — the distinction between protected and private specifically comes down to whether a subclass gets access too.

Every property and method in a PHP class carries a visibility level — public, protected, or private — controlling where in the codebase that member can actually be accessed from. Choosing the right one deliberately, rather than defaulting everything to public, is what keeps a class's internal implementation genuinely separate from its external interface.

1. public — accessible from anywhere

class Product
{
    public string $name;

    public function getDisplayPrice(): string
    {
        return '$' . number_format($this->price, 2);
    }
}
$product = new Product();
$product->name = 'Widget'; // works fine, from outside the class
echo $product->getDisplayPrice(); // works fine, from outside the class

public members form a class's actual external interface — the properties and methods other code is meant to interact with directly. This should generally be limited to what genuinely needs to be exposed, not applied by default to everything out of convenience.

2. private — accessible only within the exact same class

class Product
{
    private float $costBasis;

    private function calculateMargin(): float
    {
        return $this->price - $this->costBasis;
    }
}
$product = new Product();
$product->calculateMargin(); // Error: cannot access private method from outside the class

private is the strictest visibility — accessible only from code inside that exact class definition, not even from a subclass extending it. This is the right default for internal implementation details that genuinely have no business being touched or called from outside the class, including by a subclass.

3. protected — accessible within the class and its subclasses

class Model
{
    protected array $attributes = [];

    protected function setAttribute(string $key, mixed $value): void
    {
        $this->attributes[$key] = $value;
    }
}
class Product extends Model
{
    public function setName(string $name): void
    {
        $this->setAttribute('name', $name); // works — protected is accessible from a subclass
    }
}
$product = new Product();
$product->setAttribute('name', 'Widget'); // Error: still not accessible from OUTSIDE the class hierarchy

This is the key distinction from private: protected members are accessible from within the defining class and any class that extends it, but still not from outside code calling in from elsewhere — the middle ground between "fully open" (public) and "locked to exactly this one class" (private).

4. A practical rule for choosing between them

  • public — the class's actual intended external API; what other code is meant to call.
  • protected — internal implementation details that a subclass legitimately needs to build on or override, as part of designed extension points.
  • private — internal implementation details that even a subclass has no business touching directly — the safest, most restrictive default when there's no specific reason to open access further.

5. Why defaulting everything to public causes real problems later

A class with every property marked public has no way to enforce its own internal invariants — any external code can set any property to any value, bypassing whatever validation or business logic the class's own methods might otherwise enforce. Restricting direct access with private or protected, and exposing controlled access only through specific public methods, is what actually lets a class guarantee its own internal consistency.

6. Constructor property promotion and visibility

class Product
{
    public function __construct(
        public string $name,
        private float $costBasis,
        protected int $stockLevel = 0,
    ) {}
}

PHP 8's constructor property promotion lets a visibility modifier be declared directly on a constructor parameter, which both declares the property and assigns it from the matching argument in one line — the same three visibility levels apply exactly as they would on a manually declared property, just with considerably less boilerplate.

Topics: Developer Productivity