# [RFC] [Discussion] Friends

- Thread: https://externals.io/message/130815
- Original archive: https://news-web.php.net/php.internals/130815
- Started by Daniel Scherzer on 2026-05-10T16:28:26+00:00
- Messages: 10
- Reply by email to internals@lists.php.net
- More for agents: search with https://externals.io/search.md?q={query} · JSON API at https://externals.io/api · documentation at https://externals.io/llms.txt

---

## #130815 — Daniel Scherzer — 2026-05-10T16:28:26+00:00

URL: https://externals.io/message/130815#130815

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

---

## #130817 — Rob Landers — 2026-05-10T20:10:42+00:00

URL: https://externals.io/message/130815#130817 · In reply to: #130815

> 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

---

## #130827 — Daniel Scherzer — 2026-05-11T14:58:51+00:00

URL: https://externals.io/message/130815#130827 · In reply to: #130817

> 
>
> 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

---

## #130828 — Rob Landers — 2026-05-11T15:33:53+00:00

URL: https://externals.io/message/130815#130828 · In reply to: #130827

> 
> 
> > […]
> 
> 
> 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

---

## #130831 — Larry Garfield — 2026-05-11T16:31:08+00:00

URL: https://externals.io/message/130815#130831 · In reply to: #130828

> 

> > […]
>
> 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

Honestly, this seems like the easiest model to explain to people.  A friend class has the same access as a child class, ie, it gets protected but not private access.  That's a very simple rule to document and explain, and seems like it would side-step a number of issues.  (We had a similar conversation for aviz, IIRC, which is how we ended up on private => final and readonly => protected(set) simplifying a lot of issues.)

That said, I am still skeptical.  This seems simple enough, but I worry that it's still attacking the general problem in too specific a way (compared to modules or similar).

--Larry Garfield

---

## #130882 — Daniel Scherzer — 2026-05-14T19:29:27+00:00

URL: https://externals.io/message/130815#130882 · In reply to: #130815

On Sun, May 10, 2026 at 9:28 AM Daniel Scherzer <daniel.e.scherzer@gmail.com>
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
>


After feedback from Rob and Larry, I'm planning to switch friendship to
just allow access to protected properties/methods/constants, unless anyone
has any other suggestions for avoiding the LSP issues.

-Daniel

---

## #131128 — Daniel Scherzer — 2026-06-03T23:13:28+00:00

URL: https://externals.io/message/130815#131128 · In reply to: #130882

On Thu, May 14, 2026 at 12:29 PM Daniel Scherzer <
daniel.e.scherzer@gmail.com> wrote:

>
>
> On Sun, May 10, 2026 at 9:28 AM Daniel Scherzer <
> daniel.e.scherzer@gmail.com> wrote:
>
> > […]
>
>
> After feedback from Rob and Larry, I'm planning to switch friendship to
> just allow access to protected properties/methods/constants, unless anyone
> has any other suggestions for avoiding the LSP issues.
>
>
Seeing no further feedback, I have adjusted the RFC and the implementation
to only allow access to protected properties/methods/constants.
This qualifies as a "major change" and triggers a 14 day cooldown period.

 -Daniel

---

## #131166 — tim@bastelstu.be — 2026-06-08T09:19:43+00:00

URL: https://externals.io/message/130815#131166 · In reply to: #131128

Hi

Am 2026-06-04 01:13, schrieb Daniel Scherzer:
> Seeing no further feedback, I have adjusted the RFC and the 
> implementation
> to only allow access to protected properties/methods/constants.
> This qualifies as a "major change" and triggers a 14 day cooldown 
> period.

I've given the RFC another read and stumbled over:

> Friendship is not inherited. If UserFactory has a subclass 
> NamedUserFactory, that subclass cannot access the protected details of 
> User

 From what I see this would also be in violation of the LSP, because 
subclasses are not fully interchangeable with the top-level class. It 
would also need clarification what the relevant “authority” for 
accessibility is: Is it the class name of the actual class or is it the 
defining class of the method in question?

i.e.

     class User { friend UserFactory; protected function __construct() { 
} }

     class UserFactory {
         public function foo() { new User(); }
     }

     class ChildUserFactory extends UserFactory { }

     $f = new ChildUserFactory();
     $f->foo(); // Is this legal? ChildUserFactory is not a friend, but 
foo() is defined in UserFactory.

Best regards
Tim Düsterhus

---

## #131172 — Daniel Scherzer — 2026-06-08T18:42:00+00:00

URL: https://externals.io/message/130815#131172 · In reply to: #131166

> Hi
>
> Am 2026-06-04 01:13, schrieb Daniel Scherzer:
> > […]
>
> I've given the RFC another read and stumbled over:
>
> > […]
>
>  From what I see this would also be in violation of the LSP, because
> subclasses are not fully interchangeable with the top-level class. It
> would also need clarification what the relevant “authority” for
> accessibility is: Is it the class name of the actual class or is it the
> defining class of the method in question?
>
> i.e.
>
>      class User { friend UserFactory; protected function __construct() {
> } }
>
>      class UserFactory {
>          public function foo() { new User(); }
>      }
>
>      class ChildUserFactory extends UserFactory { }
>
>      $f = new ChildUserFactory();
>      $f->foo(); // Is this legal? ChildUserFactory is not a friend, but
> foo() is defined in UserFactory.
>
> Best regards
> Tim Düsterhus
>


Since `foo()` is defined in `UserFactory` rather than `ChildUserFactory`,
the method call still works - I was going to go add a test for this, but I
already have one, see
https://github.com/DanielEScherzer/php-src/blob/9f3c8dcac5032f11cf8b8c0ee6a4a2611a2a3933/Zend/tests/friends/not_inherited.phpt

Specifically, friendship is checked based on where the caller is *defined*,
i.e. for inherited methods (here ChildUserFactory::foo(), in my test
Baz::testFooAccess()) the parent class which defines the function is what
needs to be a friend.

I've added this clarification to the RFC text with a clearer example (and
updated the implementation with the new example test) - even when `foo()`
is overridden, using `parent::foo()` would work. As long as the actual
access is done from code defined in a friend, things work.

Let me know if the updated RFC text is still unclear.

-Daniel

---

## #131203 — tim@bastelstu.be — 2026-06-11T12:58:07+00:00

URL: https://externals.io/message/130815#131203 · In reply to: #131172

Hi

Am 2026-06-08 20:42, schrieb Daniel Scherzer:
> Let me know if the updated RFC text is still unclear.

No, that's clear now, thank you. I'm not yet sure if I like the 
proposal, but I don't see any more obvious technical / procedural 
issues.

Best regards
Tim Düsterhus
