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 (but not 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. Using the potential combination of protected + subclasses for access, though possible, is also frowned upon as abusing the protected functionality. 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.
After LSP issues were raised on the mailing list, the friend only has access to protected parts of the class, rather than also private parts. See the rejected features below for details.
<?php class User { // The UserFactory class is allowed to use the protected constructor friend UserFactory; // Protected constructor - user information must come from a trusted source protected 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 (but not 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 protected methods (e.g. constructor):
<?php class User { friend UserFactory; // Protected constructor - user information must come from a trusted source protected 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 protected properties (including those with asymmetric visibility):
<?php class User { friend UserBuilder; public protected(set) ?int $userId = null; public protected(set) ?string $username = null; // Protected constructor - use the UserBuilder protected 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); // Manipulation outside of the builder fails try { $bob->userId = 2; } catch (Error $e) { echo $e; } ?>
Assuming User has a friend UserBuilder
UserBuilder can access protected parts of User, but User cannot access the protected parts of UserBuilder (unless the builder also adds a friend declaration)UserBuilder has a friend BuilderFactory, that does not mean that BuilderFactory can access the protected parts of UserUserBuilder has a subclass LoggedUserFactory, that subclass cannot access the protected details of UserUserBuilder that are inherited by LoggedUserFactory and not overridden can still access the protected details of User. If the method is overridden, code within that method has no extra access, but the parent method (e.g. invoked with parent::) still works fine.<?php // Assuming definition of User and UserBuilder classes from the example above class LoggedUserBuilder extends UserBuilder { public function newWithId(int $userId): User { echo "Creating user with id: $userId\n"; return parent::newWithId($userId); } public function newWithName(string $username): User { echo "Creating user with name: $username\n"; return parent::newWithName($username); } } // LoggedUserBuilder doesn't actually access anything protected, that is all // done in the parent:: calls, this works fine $loggedBuilder = new LoggedUserBuilder(); $charlie = $loggedBuilder->newWithId(3); var_dump($charlie); $daniel = $loggedBuilder->newWithName("Daniel"); var_dump($daniel); ?>
Additionally:
Friendship is added orthogonally to existing visibility levels, rather than adding a new level. Friends of a class have the same level of access as a subclass would, but without the semantic implications of being a subclass.
This also presents a simple mental model - protected visibility now means “other classes in the same hierarchy, and also friends”, rather than just “other classes in the same hierarchy”.
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 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::getFriendNames()ReflectionProperty::isReadable() and ReflectionClass::isWritable() are updated to account for friendshipT_FRIEND for the new keywordNone
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.
A previous version of this RFC specified that friends have access to both protected and private parts of a class. However, this presents an LSP challenge. Specifically, consider code like the following:
<?php class User { friend UserService; private function resetUser() { echo "User::resetUser()\n"; } } class UserService { public static function resetAUser(User $u) { $u->resetUser(); } }
In isolation, this would work fine. However, if a subclass of the User class declared a method by the same name:
<?php class OtherUser extends User { private function resetUser() { echo "OtherUser::resetUser()\n"; } }
then substituting an instance of OtherUser in a call to UserService::resetAUser() would fail, since UserService is not listed as a friend of OtherUser. The same issue occurs with properties and constants.
In the absence of a casting system to allow making it clear that the original property/method/constant on the User class is the intended target, trying to support access to private members would have required one of
To avoid needing to deal with this complication, this RFC was updated to only focus on protected access, and not allow private access. Private access may be worked on in a follow-up RFC.
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.