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.
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, ) {} } ?>
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 {} } ?>
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; } ?>
Assuming User has a friend UserFactory
UserFactory can access private details of User, but User cannot access the private details of UserFactory (unless the factory also adds a friend declaration)UserFactory has a friend ServiceFactory, that does not mean that ServiceFactory can access the private details of UserUserFactory has a subclass NamedUserFactory, that subclass cannot access the private details of UserAdditionally:
ReflectionClass::getFriendNames() method means that any subclass with a method of the same name but an incompatible signature will no longer be allowedT_FRIEND prevents userland constants of the same name when using the tokenizer extensionNext minor version (PHP 8.6).
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+.
Will existing extensions be affected?
ReflectionClass::getFriendNamesReflectionProperty::isReadable and ReflectionClass::isWritable are updated to account for friendshipT_FRIEND for the new keyworkNone
Make sure there are no open issues when the vote starts!
Please consult the php/policies repository for the current voting guidelines.
Primary Vote requiring a 2/3 majority to accept the RFC:
After the RFC is implemented, this section should contain:
Links to external references, discussions, or RFCs.
Keep this updated with features that were discussed on the mail lists.
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.