Lesson 73 of 100
Classes15 min5 checksPHP 8.5

Control Property Visibility in PHP Classes

Learn how public, protected, private and PHP 8.4 asymmetric visibility keep object state behind small, predictable methods.

01 · THE IDEAREAD → TRY → LEARN

Why visibility matters

An object is more than a bag of values. It is a value plus the rules for changing that value. If every property is open to any code, those rules become suggestions. Visibility lets a class decide which of its members are part of its public contract and which are internal details.

PHP has three keywords for properties, methods and constants:

  • public: reachable from anywhere. A member with no keyword is public.
  • protected: reachable inside the class and in inheriting (and parent) classes.
  • private: reachable only inside the class that declares it.

The usual habit is to start with private and widen access only when there is a reason.

Private state behind methods

In the next example the balance-like number is private. Outside code cannot read or write it directly, so it has to go through methods that you control. Trying to touch it from the global scope throws an Error.

Example
<?php
class Wallet
{
    private int $cents = 0;

    public function add(int $amount): void
    {
        $this->cents += $amount;
    }

    public function total(): int
    {
        return $this->cents;
    }
}

$wallet = new Wallet();
$wallet->add(250);
echo $wallet->total(), "\n";

try {
    echo $wallet->cents;
} catch (Error $e) {
    echo $e->getMessage(), "\n";
}
Output
250
Cannot access private property Wallet::$cents

The class can later change how it stores money, or add validation to add(), without breaking any caller. That freedom is the point of encapsulation.

Protected versus private

Use protected when a subclass legitimately needs a member. A subclass can use a protected member but outside code cannot. A private member belongs to the defining class alone. If a child class declares a property with the same name as a parent's private property, it creates a separate property; it does not change the parent's one.

Asymmetric visibility in PHP 8.4

Sometimes you want everyone to read a value but only the class to change it. PHP 8.4 supports this with a separate set visibility, written like public private(set).

Example
<?php
class Book
{
    public function __construct(
        public private(set) string $title,
        public protected(set) string $author,
    ) {}

    public function retitle(string $title): void
    {
        $this->title = $title;
    }
}

$book = new Book('Draft', 'Ada');
$book->retitle('Final');
echo $book->title, "\n";

try {
    $book->title = 'Hack';
} catch (Error $e) {
    echo $e->getMessage(), "\n";
}
Output
Final
Cannot modify private(set) property Book::$title from global scope

Rules to remember:

  • Only typed properties can have a separate set visibility.
  • The set visibility must be the same as the read visibility or stricter. protected public(set) is a syntax error.
  • Write private(set) with no spaces inside the parentheses.
  • A private(set) property is implicitly final, so a child class cannot redeclare it.
  • Taking a reference to a property, or writing into an array property, follows the set visibility, because both can modify the value.

Visibility is per class, not per object

Code inside a class may access private and protected members of other instances of the same class. That is useful for comparisons and copying.

Example
<?php
class Point
{
    private int $x;

    public function __construct(int $x)
    {
        $this->x = $x;
    }

    public function sameAs(Point $other): bool
    {
        return $this->x === $other->x;
    }
}

var_dump((new Point(3))->sameAs(new Point(3)));
var_dump((new Point(3))->sameAs(new Point(4)));
Output
bool(true)
bool(false)

Worked design: an account

Before writing the exercise, plan the class. The state is a balance and a count of rejected transactions. Both are private. A single method, apply(int $amount), owns the rules: an amount of zero is rejected, and a negative amount that would take the balance below zero is rejected. A summary() method exposes a snapshot as an array. Because callers can never assign to the balance directly, the rule the balance never goes negative holds for the lifetime of every object.

Exercise contract

Implement applyTransactions(int $opening, array $transactions): array. Create an object that keeps its balance and rejected count in private properties, then apply each integer in order. Positive numbers are deposits and negative numbers are withdrawals. Reject zero, and reject any withdrawal larger than the current balance. Rejected items do not change the balance. Return ['balance' => int, 'rejected' => int].

Watch the boundaries: an empty list, a withdrawal that empties the account exactly (allowed), and a withdrawal one unit too large (rejected).

Recap

  • Default to private, and expose behaviour through methods.
  • protected is for deliberate extension by subclasses.
  • PHP 8.4 lets you separate read access from write access with private(set) or protected(set).
  • Visibility is checked by class scope, so instances of the same class can reach each other's internals.
Before you start

Functions · Arrays · Types

You’ll practice
  • Encapsulate local object state behind public methods
  • Implement and test a deterministic PHP 8.4 function
Your task

Make applyTransactions() pass all 5 checks. Run your code as often as you like, then check your answer.

exercise.phpSaved in this browserPHP 8.5
CONSOLE & CHECKS● Ready

Ready when you are

Your output will appear here.
Make it work. Then make it pass.

PHP loads on your first run. Each check starts fresh. Your code stays in this browser; clearing site data removes your progress. A check or the task looks wrong? Report a problem with this exercise.