====== PHP RFC: Friends ======
* Version: 0.4
* Date: 2026-05-04
* Author: Daniel Scherzer, daniel.e.scherzer@gmail.com
* Status: Under discussion
* Implementation: https://github.com/php/php-src/pull/21937
* Discussion thread: https://news-web.php.net/php.internals/130815
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.
===== Introduction =====
PHP has three visibility levels
* Public: accessible everywhere
* Protected: accessible in the declaring class and other classes in the same hierarchy
* Private: accessible only in the declaring class
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:
* Make things public, and document them as internal
* Make things public, and enforce the internal-ness by examining backtraces
* Make things protected, and have the other internal classes extend the declaring class to have access to those methods, despite not semantically making sense as subclasses
* Don't make things public, and use reflection to access them
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 [[https://packagist.org/packages/dave-liddament/php-language-extensions|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.
===== 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 (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:
==== Examples ====
Friends have access to protected methods (e.g. constructor):
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):
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;
}
?>
==== Details ====
Assuming User has a friend UserBuilder
* Friendship is **not mutual**. UserBuilder can access protected parts of User, but User cannot access the protected parts of UserBuilder (unless the builder also adds a friend declaration)
* Friendship is **not transitive**. If UserBuilder has a friend BuilderFactory, that does not mean that BuilderFactory can access the protected parts of User
* Friendship is **not inherited**. If UserBuilder has a subclass LoggedUserFactory, that subclass cannot access the protected details of User
* Methods defined on UserBuilder 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.
newWithId(3);
var_dump($charlie);
$daniel = $loggedBuilder->newWithName("Daniel");
var_dump($daniel);
?>
Additionally:
* Friendship respects asymmetric visibility - friends can modify class properties only if modifiable from the protected scope
* Friends cannot be declared on traits or interfaces
==== Not a visibility level ====
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".
===== Backward Incompatible Changes =====
* The new ReflectionClass::getFriendNames() method means that any subclass with a method of the same name but an incompatible signature will no longer be allowed
* New global constant T_FRIEND prevents userland constants of the same name when using the tokenizer extension
===== Proposed PHP Version(s) =====
Next minor version (PHP 8.6).
===== RFC Impact =====
==== To the Ecosystem ====
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+.
==== To Existing Extensions ====
Will existing extensions be affected?
* Opcache: caching of class entries is updated to include the list of friends of the class
* Reflection:
* introduction of ReflectionClass::getFriendNames()
* ReflectionProperty::isReadable() and ReflectionClass::isWritable() are updated to account for friendship
* Tokenizer: new token T_FRIEND for the new keyword
==== To SAPIs ====
None
===== Open Issues =====
Make sure there are no open issues when the vote starts!
===== Future Scope =====
* Adding support for friendship allowing access to private class details
* Adding support for inherited friendship ("UserFactory and all of its subclasses are all friends of User")
* Adding support for namespaces as friends ("Any class in \My\Library\Namespace is a friend of User")
* Adding support for friends on traits ("Any class that uses this trait will declare UserFactory as a friend")
===== Voting Choices =====
Please consult [[https://github.com/php/policies/blob/main/feature-proposals.rst#voting-phase|the php/policies repository]] for the current voting guidelines.
----
Primary Vote requiring a 2/3 majority to accept the RFC:
* Yes
* No
* Abstain
===== Patches and Tests =====
https://github.com/php/php-src/pull/21937
===== Implementation =====
After the RFC is implemented, this section should contain:
- the version(s) it was merged into
- a link to the git commit(s)
- a link to the PHP manual entry for the feature
===== References =====
Links to external references, discussions, or RFCs.
* Blog post with feature overview: https://scherzer.dev/Blog/20260309-php-friends
* https://packagist.org/packages/dave-liddament/php-language-extensions
* Pre-RFC discussion on syntax: https://externals.io/message/130710
===== Rejected Features =====
Keep this updated with features that were discussed on the mail lists.
==== Access to private class parts ====
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:
resetUser();
}
}
In isolation, this would work fine. However, if a subclass of the User class declared a method by the same name:
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
* requiring classes with friends to be final
* preventing the shadowing of private properties/methods/constants if the parent class has friends
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.
===== 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.
* v0.1: created
* v0.2: add section explaining why friendship is not a new visibility level, improve example
* v0.3: dropped access to private parts, only allowing protected access
* v0.4: improved discussion of inheritance of friendship with new example