Methods and Visibility

public members are reachable everywhere, protected ones from the class and its subclasses, and private ones from the declaring class only; methods default to public. Visibility is checked per class, not per object, so one Money may read another's private $cents. Violations throw a catchable Error.

Private state, a private helper and access checks from outsidePHP
<?php
class Money {
  public function __construct(private int $cents, private string $cur = 'USD') {}
  public function add(Money $o): static {
    if ($o->cur !== $this->cur) { $this->fail("Cannot add $o->cur to $this->cur"); }
    return new static($this->cents + $o->cents, $this->cur);   // reads $o's privates
  }
  public function text(): string { return sprintf('%s %.2f', $this->cur, $this->cents / 100); }
  private function fail(string $why): never { throw new InvalidArgumentException($why); }
}
$total = new Money(1999)->add(new Money(501));
echo $total->text(), "\n";
foreach ([fn() => $total->cents, fn() => $total->fail('x'),
          fn() => $total->add(new Money(1, 'EUR'))] as $f) {
  try { $f(); } catch (Throwable $e) { echo get_class($e), ': ', $e->getMessage(), "\n"; }
}
Output
USD 25.00
Error: Cannot access private property Money::$cents
Error: Call to private method Money::fail() from global scope
InvalidArgumentException: Cannot add EUR to USD

Keep state private and expose behavior. When outside code must read a value, PHP 8.4's public private(set) makes it readable everywhere but writable only inside (Subsection 4.11.10).