On Mon, May 11, 2026, at 16:58, Daniel Scherzer wrote:
On Sun, May 10, 2026 at 1:59 PM Rob Landers rob@bottled.codes wrote:
On Sun, May 10, 2026, at 18:28, Daniel Scherzer wrote:
Hi internals,
I’d like to start the discussion for a new RFC about adding friendship in PHP. This is a follow-up to a pre-RFC discussion thread https://externals.io/message/130710.
Thanks,
-Daniel
Hi Daniel,
I worked on the namespace visibility RFC before running out of time, and life isn’t slowing down anytime soon; thus I wish you the best of luck with this one.
First of all … the RFC doesn’t address several inheritance/override interactions worth working through. I’d invite you to read the thread here: https://externals.io/message/129147 – you’re going to run into a lot of the same issues in this RFC (and especially the follow-up namespace one), and you have some of the same problems.
Take this example:
class P {
friend F;
private int $x = 0;
}
class C extends P {
protected int $x = 0;
}
class F {
static function set(P $p, int $v) { $p->x = $v; }
}
F::set(new C, 5); // fatal
The friend grant on P creates a non-local invariant that subclass authors of P can break without realizing. C’s author, adding x for their own internal reasons, doesn’t know they’ve broken some F that depends on the parent contract. C’s tests pass. F’s tests pass. Integration breaks at runtime in production. private(namespace) had the identical pathology.
— Rob
Ah, okay, I think I understand what the issue is. Using an example of User and UserFactory, the issue is that
- When
User marks UserFactory as a friend
- and
User is not marked as final
- and
User has private non-static properties or methods[1]
- and
UserFactory has some expression that accesses a private property or method
then
- subclasses of
User could have properties/methods of the same name that shadow the property/method in User
- if the subclass property/method is not public, and does not list
UserFactory as a friend, then the UserFactory access would trigger visibility errors
That… is an interesting scenario - and I’m not quite sure that friendship is to blame. If this is replacing code that used ReflectionObject to change the property, or ReflectionMethod built from the object, then it was already targeting the shadowing version from the child class.
It’s basically a violation of LSP since LSP guarantees we can use a subclass in place of the parent class. A friendship is basically a visibility modifier on the entire class, making the entire class “public” to the friend. Your RFC says it isn’t visibility, but it is.
Basically, with friends, the developer is making a logic error and assuming that $p instanceof P means that get_class($p) === P::class rather than a subclass.
I don’t think that is a valid argument in OOP. Framing the friend’s call as a developer logic error rejects LSP itself: the whole point of polymorphism is that callers programmed against the parent’s contract can be handed subclass instances. If those instances are allowed to silently violate the contract, the bug isn’t in the calling code; it’s in the language.
Somehow, existing code within the User class doesn’t have this problem
class User {
private $id;
public static function setId(User $u, $value) {
var_dump($u);
$u->id = $value;
var_dump($u);
}
}
class Child extends User { protected $id; }
$c = new Child();
User::setId($c, 123);
I guess there are a few options to address this
- require classes with friends to be final (but then PHPUnit can’t mock them)
- prohibit shadowing of private properties/methods if a class has friends (exposes some implementation details to subclasses, but hopefully not too many)
- make
$u->id always refer to the base property in a friend the same way that it does within the user class itself (but that means that properties/methods that intentionally shadow and are public or protected and visible to the friend that previously referred to the subclass shadow now refer to the parent class)
- add some kind of upcasting to make it clear which property is being referenced,
<$u as User>->id = ...
I actually think that upcasting might be the simplest and cleanest, especially if we restrict it to only places where it is required for friendship - it can only be used as a temporary way to access properties or methods, i.e. no $u2 = $u as User;, and can only be used to cast subclasses into their parent class, when the calling code is a friend of that parent class.
Surprisingly I think adding an entirely new syntax would actually result in the fewest breaking changes when userland classes add friends, because there is no ambiguity. $u->id always refers to the id on whatever class $u happens to be, including applying any shadowing; if you want to be sure to access the base User::$id, use <$u as User>->id.
Before I dive in and actually add that, what do people think?
By the end of it, I basically arrived at a calculus that makes a sorta sense. Visibility levels are sets of callers, partial-ordered by inclusion. An override is admissible iff the child’s caller set is a superset of the parent’s at the call site. P’s caller set for $x with friend F is {P, F}; C’s shadowing with protected $x gives {C ∪ descendants(C)}; incomparable, so the override violates LSP.
Through that lens, we can look at your options you identified.
Option 1 basically kills the entire feature.
Option 3 breaks polymorphism. It throws away legitimate overrides.
Option 4 is an escape hatch, not a solution. There’s already RFCs for “as” in-progress, so you’d step on some toes there. Heh, I think I have even proposed “as” before and … from experience, competing with someone’s in-progress RFC without discussion with them beforehand is a great way to have people get mad at you on this list.
Option 2 is probably the closest “right” answer. It prevents there from being invalid caller sets and can provide meaningful error messages at compile time: “name collides with friended private variable; must friend with class F or change the name x”.
Another option to consider is not allowing private variables/methods to be accessed by friends. Only protected variables/methods. This allows inheritance to work as normal and guarantees overrides are compatible.
— Rob