Sometimes a class has members that exactly one other class needs, and nobody else should touch. A factory that needs a protected constructor is the common case. Today you either make those members public and mark them "internal" in the docs, or you reach for reflection or a fake subclass. This RFC from Daniel Scherzer adds friend classes, an idea borrowed from C++: a class names its friends, and those friends can access its protected members.

class User {
    // The UserFactory class is allowed to use the protected constructor
    friend UserFactory;

    protected function __construct(
        public readonly int $userId,
        public readonly string $username,
    ) {}
}

class UserFactory {
    public function newFromId(int $userId): ?User {
        return new User($userId, "Alice");
    }
}

UserFactory can call new User(...), while code anywhere else gets an Error. The idea came from the #[Friend] attribute in the dave-liddament/php-language-extensions package, which enforces the same rule through static analysis.

The rules

  • Friends get protected access only, not private. An early draft allowed private access too, but that was dropped after problems with subclasses came up on the mailing list.
  • Friends can modify protected properties, but only when asymmetric visibility allows writes from protected scope.
  • Friendship goes one way. Being User's friend doesn't make User your friend.
  • It doesn't chain. A friend of your friend isn't your friend.
  • It isn't inherited. A subclass of UserFactory isn't a friend, although methods it inherits from UserFactory unchanged still work, and so do parent:: calls.
  • The friend class doesn't need to exist yet, and declaring it doesn't trigger the autoloader.
  • Classes and enums can declare friends, but traits and interfaces can't.

Friendship isn't a new visibility level. Instead, protected now means "classes in the same hierarchy, plus friends." A new ReflectionClass::getFriendNames() method returns the list of declared friends.

What it means for existing code

There are two small backward compatibility breaks. A subclass of ReflectionClass that defines its own getFriendNames() method may clash if the signature doesn't match. And when the tokenizer extension is loaded, the new T_FRIEND token constant blocks any constant of your own with that name.

Static analysis tools will also need to stop warning about protected access from a friend.

Where it stands

The RFC is under discussion, with an implementation up for review. It targets PHP 8.6 and needs a two-thirds vote. The RFC lists private access, namespace-level friends and inherited friendship as possible follow-ups.