Table of Contents

PHP RFC: Friends

This RFC proposes adding support for friendship in PHP, allowing classes to specify other classes as friends. Those friends would then be able to access protected and private properties, constants, and methods of the declaring class.

Introduction

PHP has three visibility levels

Classes will occasionally have property or methods that need to be exposed to one or more other classes, but should not be public to everyone. These are generally internal implementation details that are exposed for use in other parts of the same library, like factory classes, but should not be used by external code.

Currently, the options to do so are limited:

The use of reflection for bypassing visibility restrictions in non-test code is generally frowned upon, and should not be recommended. Thus, the primary approach currently taken is to make things public, and to require, via documentation, tooling, and/or runtime backtrace checks, that the internal aspect of the class is not being accessed improperly. One tool, that inspired this RFC, is the #[Friend] attribute from the dave-liddament/php-language-extensions library, which adds an attribute for friendship that is enforced via static analysis.

This RFC proposes an approach based on C++'s concept of friends. A class (e.g. User) can declare another class (e.g. UserFactory) as a friend, allowing that friend to access internal details.

<?php
 
class User {
    // The UserFactory class is allowed to use the private constructor
    friend UserFactory;
 
    // Private constructor - user information must come from a trusted source
    private function __construct(
        public readonly int $userId,
        public readonly string $username,
    ) {}
}
 
?>

Proposal

Add support for PHP classes (including enums) to declare friends based on class name, following the same namespace handling rules as normal (e.g. use statements are applied). The named friend need not exist at the time of declaration, and is not autoloaded. This allows for potential friendship with external classes.

Friends (defined as classes where the fully qualified class name matches the declared friend name) are then allowed to access protected and private parts of the declaring class.

For protected parts that are inherited by subclasses, when not overridden the friend can still access them. In other words, friendship checks are based on the class where a property/method/constant is defined.

To allow checking what friends a class has, a new method is added to ReflectionClass returning an array of the friend names:

<?php
 
class ReflectionClass implements Reflector
 
    // ...existing methods
 
    public function getFriendNames(): array {}
 
}
 
?>

Examples

Friends have access to private methods (e.g. constructor):

<?php
 
class User {
    friend UserFactory;
 
    // Private constructor - user information must come from a trusted source
    private function __construct(
        public readonly int $userId,
        public readonly string $username,
    ) {}
}
 
class UserFactory {
 
    public function newFromId(int $userId): ?User {
        // In reality this would query a database or something
        return match ($userId) {
            1 => new User(1, "Alice"),
            2 => new User(2, "Bob"),
            default => null,
        };
    }
}
 
$factory = new UserFactory;
 
$alice = $factory->newFromId(1);
var_dump($alice);
 
$bob = $factory->newFromId(2);
var_dump($bob);
 
// Creation outside of the factory fails
try {
    $unknown = new User(3, "Camille");
} catch (Error $e) {
    echo $e;
}
 
?>

Friends also have access to change private properties (including those with asymmetric visibility):

<?php
 
class User {
    friend UserBuilder;
 
    public private(set) ?int $userId = null;
    public private(set) ?string $username = null;
 
    // Private constructor - use the UserBuilder
    private function __construct() {}
}
 
class UserBuilder {
 
    public function newWithId(int $userId): User {
        $u = new User();
        $u->userId = $userId;
        return $u;
    }
 
    public function newWithName(string $username): User {
        $u = new User();
        $u->username = $username;
        return $u;
    }
}
 
$builder = new UserBuilder();
 
$alice = $builder->newWithId(1);
var_dump($alice);
 
$bob = $builder->newWithName("Bob");
var_dump($bob);
 
// Creation outside of the builder fails
try {
    $unknown = new User();
} catch (Error $e) {
    echo $e;
}
 
?>

Details

Assuming User has a friend UserFactory

Additionally:

Backward Incompatible Changes

Proposed PHP Version(s)

Next minor version (PHP 8.6).

RFC Impact

To the Ecosystem

Static Analyzers will want to suppress warnings about accessing private/protected class members when the access is coming from a friend.

Linters and IDEs will need to update for the new syntax.

Userland PHP libraries that have internal details exposed to other parts of the library via public methods may want to migrate to using friends once they require PHP 8.6+.

To Existing Extensions

Will existing extensions be affected?

To SAPIs

None

Open Issues

Make sure there are no open issues when the vote starts!

Future Scope

Voting Choices

Please consult the php/policies repository for the current voting guidelines.


Primary Vote requiring a 2/3 majority to accept the RFC:

Implement friends as outlined in the RFC?
Real name Yes No Abstain
Final result: 0 0 0
This poll has been closed.

Patches and Tests

https://github.com/php/php-src/pull/21937

Implementation

After the RFC is implemented, this section should contain:

  1. the version(s) it was merged into
  2. a link to the git commit(s)
  3. a link to the PHP manual entry for the feature

References

Links to external references, discussions, or RFCs.

Rejected Features

Keep this updated with features that were discussed on the mail lists.

Changelog

If there are major changes to the initial proposal, please include a short summary with a date or a link to the mailing list announcement here, as not everyone has access to the wikis' version history.