On Mon, May 11, 2026, at 16:58, Daniel Scherzer wrote:
>
> On Sun, May 10, 2026 at 1:59 PM Rob Landers <[email protected]> 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.
>>>
>>> * RFC: https://wiki.php.net/rfc/friends
>>> * Implementation: https://github.com/php/php-src/pull/21937
>>>
>>> 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