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 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
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,
-DanielHi 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); // fatalThe 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
UsermarksUserFactoryas a friend - and
Useris not marked as final - and
Userhas private non-static properties or methods[1] - and
UserFactoryhas some expression that accesses a private property or
method
then
- subclasses of
Usercould have properties/methods of the same name that
shadow the property/method inUser - if the subclass property/method is not public, and does not list
UserFactoryas a friend, then theUserFactoryaccess 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->idalways 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
__
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,
-DanielHi 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); // fatalThe friend grant on P creates a non-local invariant that subclass authors of P can break without realizing. C's author, adding
xfor 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
UsermarksUserFactoryas a friend- and
Useris not marked as final- and
Userhas private non-static properties or methods[1]- and
UserFactoryhas some expression that accesses a private property or methodthen
- subclasses of
Usercould have properties/methods of the same name that shadow the property/method inUser- if the subclass property/method is not public, and does not list
UserFactoryas a friend, then theUserFactoryaccess would trigger visibility errorsThat... 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 Pmeans thatget_class($p) === P::classrather 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
Userclass doesn't have this problemclass 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->idalways 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->idalways refers to theidon whatever class$uhappens to be, including applying any shadowing; if you want to be sure to access the baseUser::$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
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->idalways refers to theidon whatever class$uhappens to be, including applying any shadowing; if you want to be sure to access the baseUser::$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$xwith
friend Fis {P, F}; C's shadowing withprotected $xgives {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
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
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: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,
-DanielAfter 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
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
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
UserFrom 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, butfoo() 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
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