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.

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.

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?

-Daniel

[1] I'm excluding static properties and methods since those are normally
accessed as User::$prop and User::$staticMethod(), same with constants

Reply via email to