[PHP-DEV] RFC: short and inner classes

Am 20.03.2025 um 15:51 schrieb Rob Landers rob@bottled.codes:

On Wed, Mar 19, 2025, at 21:09, Bob Weinand wrote:

On 19.3.2025 16:04:06, Rob Landers wrote:

On Tue, Mar 18, 2025, at 03:37, Bob Weinand wrote:

Okay, I see the point with LSP. I’m not sure whether we need to preserve LSP for that specific scenario, but neither can I say that we should ignore it.

(Effectively implementing LSP would mean that there’s an implicit interface matching all public method signatures of the parent class, for child classes - which is doable, but possibly too much for the initial RFC.)

I would however ask, should we not implement LSP compatible inner classes, to enforce that no child class may name a class the same than any non-private inner class declared by any of its parents, until we resolve this question (in possibly a future version of PHP).

I do not think we should bar ourselves from allowing this in the future.

I’m not sure I understand what you are asking. But I think you are saying the following should be illegal?

class ParentOuter {

class ParentInner {}

}

class ChildOuter extends ParentOuter {

class ParentInner {} // not allowed

}

Precisely.

And not pretending starts with using a different symbol than a backslash.

I have been thinking about this for a couple of days now… When thinking through the ramifications of my decision to use :> over ::, this will also affect generics, most likely – whenever that happens. This is because if this RFC passes, generics will want to be consistent with whatever exists currently.

If we were to use :>, then generics would probably look something like this to be consistent:

class Box {

public function add(self:>T $item) {}

}

The same thing would also probably apply to ::

class Box {

public function add(self::T $item) {}

}

With \, it nearly follows exactly what you would expect-ish:

use \Box\T as T;

class Box {

public function add(T $item) {}

// or without use

public function add(Box\T $item) {}

}

With \, we can also just automatically check the current class as part of namespace resolution when compiling:

class Box {

public function add(T $item) {}

}

This would also make it easier to user inner classes:

class Outer {

public class Inner {}

public function foo(Inner $bar) {}

}

The other syntax options do not allow this; at least, I don’t think so. I’m very heavily leaning towards \ as the separator.

— Rob

I’m failing to understand why you’d think this would be related at all?

If we get generics,

class Box {

public function add(T $item) {}

}

would just work, without any use or such. There will not be a symbol Box::T or Box\T, just all mentions of T within the Box class will be taken as “this is the generic placeholder” and the compiler takes care.

It’s not like that T will be directly accessible from outside of the class or actually a proper type, unlike inner classes.

A generic is not an inner class nor will it look like it. Also, there’s no accessing of a parents generic - you write class Child extends ParentClass - or something along these lines, getting the T available for your class.

Bob

Yes, that is the question right? It might not affect anything there, but there would probably be an argument to keep it consistent with inner classes. In PHP, a class is a type; thus an inner class is an inner type, and generic types are also an inner type that only exist in the scope of their enclosing class, just like private inner classes.

If my logic is incorrect, let me know.

— Rob

The difference is that inner classes are a backed type; there’s actually a class/interface/enum/… behind it.

Generic placeholders are placeholders for another type, which is strictly local to the compilation of the class.

Bob

On Thu, Mar 20, 2025, at 16:12, Bob Weinand wrote:

Am 20.03.2025 um 15:51 schrieb Rob Landers rob@bottled.codes:

On Wed, Mar 19, 2025, at 21:09, Bob Weinand wrote:

On 19.3.2025 16:04:06, Rob Landers wrote:

On Tue, Mar 18, 2025, at 03:37, Bob Weinand wrote:

Okay, I see the point with LSP. I’m not sure whether we need to preserve LSP for that specific scenario, but neither can I say that we should ignore it.

(Effectively implementing LSP would mean that there’s an implicit interface matching all public method signatures of the parent class, for child classes - which is doable, but possibly too much for the initial RFC.)

I would however ask, should we not implement LSP compatible inner classes, to enforce that no child class may name a class the same than any non-private inner class declared by any of its parents, until we resolve this question (in possibly a future version of PHP).

I do not think we should bar ourselves from allowing this in the future.

I’m not sure I understand what you are asking. But I think you are saying the following should be illegal?

class ParentOuter {

class ParentInner {}

}

class ChildOuter extends ParentOuter {

class ParentInner {} // not allowed

}

Precisely.

And not pretending starts with using a different symbol than a backslash.

I have been thinking about this for a couple of days now… When thinking through the ramifications of my decision to use :> over ::, this will also affect generics, most likely – whenever that happens. This is because if this RFC passes, generics will want to be consistent with whatever exists currently.

If we were to use :>, then generics would probably look something like this to be consistent:

class Box {

public function add(self:>T $item) {}

}

The same thing would also probably apply to ::

class Box {

public function add(self::T $item) {}

}

With \, it nearly follows exactly what you would expect-ish:

use \Box\T as T;

class Box {

public function add(T $item) {}

// or without use

public function add(Box\T $item) {}

}

With \, we can also just automatically check the current class as part of namespace resolution when compiling:

class Box {

public function add(T $item) {}

}

This would also make it easier to user inner classes:

class Outer {

public class Inner {}

public function foo(Inner $bar) {}

}

The other syntax options do not allow this; at least, I don’t think so. I’m very heavily leaning towards \ as the separator.

— Rob

I’m failing to understand why you’d think this would be related at all?

If we get generics,

class Box {

public function add(T $item) {}

}

would just work, without any use or such. There will not be a symbol Box::T or Box\T, just all mentions of T within the Box class will be taken as “this is the generic placeholder” and the compiler takes care.

It’s not like that T will be directly accessible from outside of the class or actually a proper type, unlike inner classes.

A generic is not an inner class nor will it look like it. Also, there’s no accessing of a parents generic - you write class Child extends ParentClass - or something along these lines, getting the T available for your class.

Bob

Yes, that is the question right? It might not affect anything there, but there would probably be an argument to keep it consistent with inner classes. In PHP, a class is a type; thus an inner class is an inner type, and generic types are also an inner type that only exist in the scope of their enclosing class, just like private inner classes.

If my logic is incorrect, let me know.

— Rob

The difference is that inner classes are a backed type; there’s actually a class/interface/enum/… behind it.

Generic placeholders are placeholders for another type, which is strictly local to the compilation of the class.

Bob

If that were the case, then this would be an error:

function add(T $item) {

if ($item instanceof T) {}

}

because T isn’t a backed type. I don’t think they are the same thing, but during runtime, T is most definitely a backed-type; not an empty box. You still have to refer to T in some way, and I’m of the opinion that self:>T is not ideal (I think we can probably all agree on that?). Even in inner classes, it seems to make more sense to just treat it like you would any other type in the current namespace.

I think Rowan makes a good argument about other languages, where naming collisions are not an issue. Ilija has also made some good arguments. I have some time next week to implement this and see how it feels.

— Rob

Am 20.03.2025 um 16:28 schrieb Rob Landers rob@bottled.codes:

On Thu, Mar 20, 2025, at 16:12, Bob Weinand wrote:

Am 20.03.2025 um 15:51 schrieb Rob Landers rob@bottled.codes:

On Wed, Mar 19, 2025, at 21:09, Bob Weinand wrote:

On 19.3.2025 16:04:06, Rob Landers wrote:

On Tue, Mar 18, 2025, at 03:37, Bob Weinand wrote:

Okay, I see the point with LSP. I’m not sure whether we need to preserve LSP for that specific scenario, but neither can I say that we should ignore it.

(Effectively implementing LSP would mean that there’s an implicit interface matching all public method signatures of the parent class, for child classes - which is doable, but possibly too much for the initial RFC.)

I would however ask, should we not implement LSP compatible inner classes, to enforce that no child class may name a class the same than any non-private inner class declared by any of its parents, until we resolve this question (in possibly a future version of PHP).

I do not think we should bar ourselves from allowing this in the future.

I’m not sure I understand what you are asking. But I think you are saying the following should be illegal?

class ParentOuter {

class ParentInner {}

}

class ChildOuter extends ParentOuter {

class ParentInner {} // not allowed

}

Precisely.

And not pretending starts with using a different symbol than a backslash.

I have been thinking about this for a couple of days now… When thinking through the ramifications of my decision to use :> over ::, this will also affect generics, most likely – whenever that happens. This is because if this RFC passes, generics will want to be consistent with whatever exists currently.

If we were to use :>, then generics would probably look something like this to be consistent:

class Box {

public function add(self:>T $item) {}

}

The same thing would also probably apply to ::

class Box {

public function add(self::T $item) {}

}

With \, it nearly follows exactly what you would expect-ish:

use \Box\T as T;

class Box {

public function add(T $item) {}

// or without use

public function add(Box\T $item) {}

}

With \, we can also just automatically check the current class as part of namespace resolution when compiling:

class Box {

public function add(T $item) {}

}

This would also make it easier to user inner classes:

class Outer {

public class Inner {}

public function foo(Inner $bar) {}

}

The other syntax options do not allow this; at least, I don’t think so. I’m very heavily leaning towards \ as the separator.

— Rob

I’m failing to understand why you’d think this would be related at all?

If we get generics,

class Box {

public function add(T $item) {}

}

would just work, without any use or such. There will not be a symbol Box::T or Box\T, just all mentions of T within the Box class will be taken as “this is the generic placeholder” and the compiler takes care.

It’s not like that T will be directly accessible from outside of the class or actually a proper type, unlike inner classes.

A generic is not an inner class nor will it look like it. Also, there’s no accessing of a parents generic - you write class Child extends ParentClass - or something along these lines, getting the T available for your class.

Bob

Yes, that is the question right? It might not affect anything there, but there would probably be an argument to keep it consistent with inner classes. In PHP, a class is a type; thus an inner class is an inner type, and generic types are also an inner type that only exist in the scope of their enclosing class, just like private inner classes.

If my logic is incorrect, let me know.

— Rob

The difference is that inner classes are a backed type; there’s actually a class/interface/enum/… behind it.

Generic placeholders are placeholders for another type, which is strictly local to the compilation of the class.

Bob

If that were the case, then this would be an error:

function add(T $item) {

if ($item instanceof T) {}

}

because T isn’t a backed type. I don’t think they are the same thing, but during runtime, T is most definitely a backed-type; not an empty box. You still have to refer to T in some way, and I’m of the opinion that self:>T is not ideal (I think we can probably all agree on that?). Even in inner classes, it seems to make more sense to just treat it like you would any other type in the current namespace.

It’s a placeholder for a backed type.
At runtime (or during monomorphization), the T will be replaced by the actual type.

Whatever name T refers to is the actual backed type.

Bob

On Thu, Mar 20, 2025, at 16:41, Bob Weinand wrote:

Am 20.03.2025 um 16:28 schrieb Rob Landers rob@bottled.codes:

On Thu, Mar 20, 2025, at 16:12, Bob Weinand wrote:

Am 20.03.2025 um 15:51 schrieb Rob Landers rob@bottled.codes:

On Wed, Mar 19, 2025, at 21:09, Bob Weinand wrote:

On 19.3.2025 16:04:06, Rob Landers wrote:

On Tue, Mar 18, 2025, at 03:37, Bob Weinand wrote:

Okay, I see the point with LSP. I’m not sure whether we need to preserve LSP for that specific scenario, but neither can I say that we should ignore it.

(Effectively implementing LSP would mean that there’s an implicit interface matching all public method signatures of the parent class, for child classes - which is doable, but possibly too much for the initial RFC.)

I would however ask, should we not implement LSP compatible inner classes, to enforce that no child class may name a class the same than any non-private inner class declared by any of its parents, until we resolve this question (in possibly a future version of PHP).

I do not think we should bar ourselves from allowing this in the future.

I’m not sure I understand what you are asking. But I think you are saying the following should be illegal?

class ParentOuter {

class ParentInner {}

}

class ChildOuter extends ParentOuter {

class ParentInner {} // not allowed

}

Precisely.

And not pretending starts with using a different symbol than a backslash.

I have been thinking about this for a couple of days now… When thinking through the ramifications of my decision to use :> over ::, this will also affect generics, most likely – whenever that happens. This is because if this RFC passes, generics will want to be consistent with whatever exists currently.

If we were to use :>, then generics would probably look something like this to be consistent:

class Box {

public function add(self:>T $item) {}

}

The same thing would also probably apply to ::

class Box {

public function add(self::T $item) {}

}

With \, it nearly follows exactly what you would expect-ish:

use \Box\T as T;

class Box {

public function add(T $item) {}

// or without use

public function add(Box\T $item) {}

}

With \, we can also just automatically check the current class as part of namespace resolution when compiling:

class Box {

public function add(T $item) {}

}

This would also make it easier to user inner classes:

class Outer {

public class Inner {}

public function foo(Inner $bar) {}

}

The other syntax options do not allow this; at least, I don’t think so. I’m very heavily leaning towards \ as the separator.

— Rob

I’m failing to understand why you’d think this would be related at all?

If we get generics,

class Box {

public function add(T $item) {}

}

would just work, without any use or such. There will not be a symbol Box::T or Box\T, just all mentions of T within the Box class will be taken as “this is the generic placeholder” and the compiler takes care.

It’s not like that T will be directly accessible from outside of the class or actually a proper type, unlike inner classes.

A generic is not an inner class nor will it look like it. Also, there’s no accessing of a parents generic - you write class Child extends ParentClass - or something along these lines, getting the T available for your class.

Bob

Yes, that is the question right? It might not affect anything there, but there would probably be an argument to keep it consistent with inner classes. In PHP, a class is a type; thus an inner class is an inner type, and generic types are also an inner type that only exist in the scope of their enclosing class, just like private inner classes.

If my logic is incorrect, let me know.

— Rob

The difference is that inner classes are a backed type; there’s actually a class/interface/enum/… behind it.

Generic placeholders are placeholders for another type, which is strictly local to the compilation of the class.

Bob

If that were the case, then this would be an error:

function add(T $item) {

if ($item instanceof T) {}

}

because T isn’t a backed type. I don’t think they are the same thing, but during runtime, T is most definitely a backed-type; not an empty box. You still have to refer to T in some way, and I’m of the opinion that self:>T is not ideal (I think we can probably all agree on that?). Even in inner classes, it seems to make more sense to just treat it like you would any other type in the current namespace.

It’s a placeholder for a backed type.

At runtime (or during monomorphization), the T will be replaced by the actual type.

Whatever name T refers to is the actual backed type.

Bob

Right, my point is that you have to refer to T in the first place. If we use \, then T as-is is fine, because there is a clear resolution to what T you are referring to. If we use :> or ::, then when you write Box, are you referring to T, as in the generic T; or T as in the class T in the same namespace. This is clear when we use “T” literally, but quickly becomes not so clear when using something else:

class Item {}

class Box {

public function add(Item $item) {}

}

This is exactly why I said this matters! In the case above with the current :> syntax, we should probably use:

class Box {

public function add(self:>Item $item) {

}

to explicitly define that we want the inner type “Item.” To use the first example, you have to define inner types to allow that, but we are currently defining inner types in this RFC. I’d personally rather use the first example when it comes to generics, which means we need to define that now. In other words, I think you are on team "" without realizing you are on team "".

— Rob

On Thu, Mar 20, 2025, 18:58 Rob Landers rob@bottled.codes wrote:

On Thu, Mar 20, 2025, at 16:41, Bob Weinand wrote:

Am 20.03.2025 um 16:28 schrieb Rob Landers rob@bottled.codes:

On Thu, Mar 20, 2025, at 16:12, Bob Weinand wrote:

Am 20.03.2025 um 15:51 schrieb Rob Landers rob@bottled.codes:

On Wed, Mar 19, 2025, at 21:09, Bob Weinand wrote:

On 19.3.2025 16:04:06, Rob Landers wrote:

On Tue, Mar 18, 2025, at 03:37, Bob Weinand wrote:

Okay, I see the point with LSP. I’m not sure whether we need to preserve LSP for that specific scenario, but neither can I say that we should ignore it.

(Effectively implementing LSP would mean that there’s an implicit interface matching all public method signatures of the parent class, for child classes - which is doable, but possibly too much for the initial RFC.)

I would however ask, should we not implement LSP compatible inner classes, to enforce that no child class may name a class the same than any non-private inner class declared by any of its parents, until we resolve this question (in possibly a future version of PHP).

I do not think we should bar ourselves from allowing this in the future.

I’m not sure I understand what you are asking. But I think you are saying the following should be illegal?

class ParentOuter {

class ParentInner {}

}

class ChildOuter extends ParentOuter {

class ParentInner {} // not allowed

}

Precisely.

And not pretending starts with using a different symbol than a backslash.

I have been thinking about this for a couple of days now… When thinking through the ramifications of my decision to use :> over ::, this will also affect generics, most likely – whenever that happens. This is because if this RFC passes, generics will want to be consistent with whatever exists currently.

If we were to use :>, then generics would probably look something like this to be consistent:

class Box {

public function add(self:>T $item) {}

}

The same thing would also probably apply to ::

class Box {

public function add(self::T $item) {}

}

With \, it nearly follows exactly what you would expect-ish:

use \Box\T as T;

class Box {

public function add(T $item) {}

// or without use

public function add(Box\T $item) {}

}

With \, we can also just automatically check the current class as part of namespace resolution when compiling:

class Box {

public function add(T $item) {}

}

This would also make it easier to user inner classes:

class Outer {

public class Inner {}

public function foo(Inner $bar) {}

}

The other syntax options do not allow this; at least, I don’t think so. I’m very heavily leaning towards \ as the separator.

— Rob

I’m failing to understand why you’d think this would be related at all?

If we get generics,

class Box {

public function add(T $item) {}

}

would just work, without any use or such. There will not be a symbol Box::T or Box\T, just all mentions of T within the Box class will be taken as “this is the generic placeholder” and the compiler takes care.

It’s not like that T will be directly accessible from outside of the class or actually a proper type, unlike inner classes.

A generic is not an inner class nor will it look like it. Also, there’s no accessing of a parents generic - you write class Child extends ParentClass - or something along these lines, getting the T available for your class.

Bob

Yes, that is the question right? It might not affect anything there, but there would probably be an argument to keep it consistent with inner classes. In PHP, a class is a type; thus an inner class is an inner type, and generic types are also an inner type that only exist in the scope of their enclosing class, just like private inner classes.

If my logic is incorrect, let me know.

— Rob

The difference is that inner classes are a backed type; there’s actually a class/interface/enum/… behind it.

Generic placeholders are placeholders for another type, which is strictly local to the compilation of the class.

Bob

If that were the case, then this would be an error:

function add(T $item) {

if ($item instanceof T) {}

}

because T isn’t a backed type. I don’t think they are the same thing, but during runtime, T is most definitely a backed-type; not an empty box. You still have to refer to T in some way, and I’m of the opinion that self:>T is not ideal (I think we can probably all agree on that?). Even in inner classes, it seems to make more sense to just treat it like you would any other type in the current namespace.

It’s a placeholder for a backed type.

At runtime (or during monomorphization), the T will be replaced by the actual type.

Whatever name T refers to is the actual backed type.

Bob

Right, my point is that you have to refer to T in the first place. If we use \, then T as-is is fine, because there is a clear resolution to what T you are referring to. If we use :> or ::, then when you write Box, are you referring to T, as in the generic T; or T as in the class T in the same namespace. This is clear when we use “T” literally, but quickly becomes not so clear when using something else:

class Item {}

class Box {

public function add(Item $item) {}

}

This is exactly why I said this matters! In the case above with the current :> syntax, we should probably use:

class Box {

public function add(self:>Item $item) {

}

to explicitly define that we want the inner type “Item.” To use the first example, you have to define inner types to allow that, but we are currently defining inner types in this RFC. I’d personally rather use the first example when it comes to generics, which means we need to define that now. In other words, I think you are on team "" without realizing you are on team "".

— Rob

Hello all, amazing effort as always.

My 2¢:

RL usecase I immediately would adopt this for - is building IDE-supported complex DTOs. This means autocompletion, refactor rename, inspections for undefined keys, etc for the price of using no language ”hacks”.

A real life example - json response from UPS shipping API - it has 200++ keys in up to 10+ levels! The structure keeps changing too, albeit in glacial speeds, but having it in a structured definition allows one to not loose ones head when providing continuous support.

However: there’s a bunch of multilevel ”leaf nodes” which are reused dozends of times, for example MonetaryAmount - it has the numeric value, but also a Currency as a property.

If extending a class does not inherit the public/protected nested classes, we are forced to define the structure of MonetaryAmount in the ”regular” fashion as separate classes.

Not to mention this non-inheritance is counter-inuitive IMHO.

Also, IMHO ”nested classes” is a better name.

Good luck!

On Wed, Mar 12, 2025, at 5:10 AM, Rob Landers wrote:

Hello internals,

I've made some major updates to the text of the RFC to clarify
behaviors and revisited the implementation (which is still under
development, though I hope to have a draft by the end of this weekend).
Here's a broad overview of what has changed in inner classes:

- Accessing inner classes is done via a new token: ":>" instead of "::".
- Inner classes may now be infinitely nested.
- Inner classes may be declared `abstract`.
- Documented changes to ReflectionClass.
- Usage of `static` to refer to inner classes is restricted to prevent
accidental violations of LSP.

Otherwise, there are not any big changes, but a lot of time was spent
clarifying behavior and expanding on the reasoning for those decisions
in the RFC itself.

— Rob

I've been following this thread with interest, and at the moment I'm honestly undecided. I certainly see the use cases for this functionality (whatever it gets named), but as a practical matter it sounds like it introduces a lot of extra clunk and complexity. And it seems like the use cases could be addressed as well with either fileprivate or module-private. (The former being considerably less work.)

So, how would nested classes compare to fileprivate, in terms of ability to solve the problem space? As I understand it, the goal is:

1. Classes that can be instantiated only by the class that uses them.
2. But can be returned from that class to a caller and reused as appropriate.

The autoloading question (loading a whole file for just an implementation detail value object) is not one that carries much weight for me, as that's a user-space question, not an engine question. (Nothing in PHP itself says you cannot put 20 3 line classes or enums together in one file. It's just PSR-4 that says not go. Even composer would allow it if configured properly) So how would the less-complicated alternative compare?

--Larry Garfield

Hello,

I’m now deep into the rabbit hole of trying out "" as the separator. It does give some nice ergonomics when using the nested/inner classes, and I really like it much better than “:>”, but the amount of technical debt I have run into is enormous. For now, my implementation will probably be ‘hackish’ just to get it working (even though I still have to address the technical debt). However, it looks like I may have to make namespaces first-class logical spaces in the engine (instead of string prefixes) to do it properly. I will probably just share my “hackish” solution for now. It is simple enough to follow, but I promise you will cringe at the number of string operations required to make it work.

That being said, the below responses use “:>” as the separator.

On Fri, Mar 21, 2025, at 12:15, Rokas Šleinius wrote:

Hello all, amazing effort as always.

My 2¢:

RL usecase I immediately would adopt this for - is building IDE-supported complex DTOs. This means autocompletion, refactor rename, inspections for undefined keys, etc for the price of using no language ”hacks”.

A real life example - json response from UPS shipping API - it has 200++ keys in up to 10+ levels! The structure keeps changing too, albeit in glacial speeds, but having it in a structured definition allows one to not loose ones head when providing continuous support.

However: there’s a bunch of multilevel ”leaf nodes” which are reused dozends of times, for example MonetaryAmount - it has the numeric value, but also a Currency as a property.

If extending a class does not inherit the public/protected nested classes, we are forced to define the structure of MonetaryAmount in the ”regular” fashion as separate classes.

Not to mention this non-inheritance is counter-inuitive IMHO.

Also, IMHO ”nested classes” is a better name.

Good luck!

Hey Rokus,

Generally, nested classes do not get inherited in languages that support them for several reasons: they are mostly scoped types, and they help organize the code and reduce namespace pollution but aren’t considered part of the class hierarchy.

If we were to be an outlier and support inheritance, the options for inheritance basically boil down to either enforce inheritance in child classes and end up in some really weird situations; or deal with “LSP violations.”

Let’s take a look at an example where we want to add a nested child class that illustrates “weird” if we turn on forced inheritance:

abstract class Shipment {

protected final class Items {}

}

Then when we want to add our own Items in our subclass:

class InternationalShipment extends Shipment {

protected class Items extends Shipment\items {} // ummmm

}

So, now we have an abstract class that cannot be implemented with any nested class called “Items” even if they are unrelated, they are forcibly related.

If we don’t enforce inheritance but simply allow inherited classes to “just work” you can end up with this code:

abstract class Shipment {

protected class Items {}

}

class LocalShipment extends Shipment {

protected function deliver(self:>Items $items);

}

and then you later come along and add it as a class:

class LocalShipment extends Shipment {

protected class Items {}

protected function deliver(self:>Items $items) {} // now it refers to a totally different type!

}

This can make refactoring a total nightmare, whereas the current solution would have to look like this:

class LocalShipment extends Shipment {

protected function deliver(Shipment:>Items $items) {}

}

You can add a class with the name Items if you want, and there is no problem at all; everyone reading it knows you mean “this particular class’s Items” and they don’t have to search the class to see if it is inherited or not.

On Sun, Mar 23, 2025, at 16:17, Larry Garfield wrote:

On Wed, Mar 12, 2025, at 5:10 AM, Rob Landers wrote:

Hello internals,

I’ve made some major updates to the text of the RFC to clarify

behaviors and revisited the implementation (which is still under

development, though I hope to have a draft by the end of this weekend).

Here’s a broad overview of what has changed in inner classes:

  • Accessing inner classes is done via a new token: “:>” instead of “::”.
  • Inner classes may now be infinitely nested.
  • Inner classes may be declared abstract.
  • Documented changes to ReflectionClass.
  • Usage of static to refer to inner classes is restricted to prevent

accidental violations of LSP.

Otherwise, there are not any big changes, but a lot of time was spent

clarifying behavior and expanding on the reasoning for those decisions

in the RFC itself.

— Rob

I’ve been following this thread with interest, and at the moment I’m honestly undecided. I certainly see the use cases for this functionality (whatever it gets named), but as a practical matter it sounds like it introduces a lot of extra clunk and complexity. And it seems like the use cases could be addressed as well with either fileprivate or module-private. (The former being considerably less work.)

So, how would nested classes compare to fileprivate, in terms of ability to solve the problem space? As I understand it, the goal is:

  1. Classes that can be instantiated only by the class that uses them.

  2. But can be returned from that class to a caller and reused as appropriate.

The autoloading question (loading a whole file for just an implementation detail value object) is not one that carries much weight for me, as that’s a user-space question, not an engine question. (Nothing in PHP itself says you cannot put 20 3 line classes or enums together in one file. It’s just PSR-4 that says not go. Even composer would allow it if configured properly) So how would the less-complicated alternative compare?

–Larry Garfield

Hey Larry,

I think file-private would/could be useful, but that only limits you to a “private” scope, which severely hampers what you can do with it. If we went with “module-private” (rhetorical question: what is a module?), but then you wouldn’t be able to have “private” scope.

With nested/inner classes, for example, you can put a protected class on a class or interface and access it only from those that use it, regardless of what file or “module” (namespace?) they are in; the logic can be encapsulated to where it is used, not where it is defined.

interface Shipment {

protected class Items {}

public readonly class Destination {}

function deliver(self:>Destination $destination);

}

class InternationalShipment implements Shipment {

private function handle(Shipment:>Items $items) {}

public function deliver(Shipment:>Destination $destination) {}

}

However, you can use private nested/inner classes to encapsulate the logic to where it is defined instead, like file-private would give you.

The goal here isn’t to only to reduce “namespace pollution” but also to encapsulate related logic integrally connected to the outer class’s behavior. Since you’ve referenced Kotlin a number of times, I assume you are familiar with the concept of nested classes there? This is extremely similar.

It’s also worth pointing out that the nested/inner classes are more like “friend classes”, just like in other languages, which is something you don’t get from file-private or module-private. Oh, and I don’t think file-private or module-private is less complicated :slight_smile: just a different kind of complicated… For the latter, I’m guessing we’d bikeshed for months just deciding what a “module” even is. In any case, I’m going to be tackling that (module-private) next-ish; regardless of whether this RFC passes. The plumbing and implementation details can be modified for that purpose, and the changes are fairly trivial. Both of these, together, would be immensely powerful tools. Though, even just one of them would still be awesome.

— Rob

On Sun, Mar 23, 2025 at 5:20 PM Larry Garfield <larry@garfieldtech.com> wrote:

So, how would nested classes compare to fileprivate, in terms of ability to solve the problem space? As I understand it, the goal is:

  1. Classes that can be instantiated only by the class that uses them.
  2. But can be returned from that class to a caller and reused as appropriate.

I think the one other difference is that nested classes can access private variables or their outer classes.
Example:

class Polygon {
private function __construct(private array $points) {}
public function getPoints(): array {
return $this->points;
}
public class Builder {
private array $points = [];
public function addPoint(Points $point): self {
$this->points[] = $point;
return $this;
}
public function build(): Polygon {
if (count($this->points) < 3) {
throw new InvalidArgumentException('A polygon must have at least 3 points');
}
return new Polygon($this->points);
}
}
}

And it would be used like this:

$polygon = new Polygon::Builder()
->addPoint($point1)
->addPoint($point1)
->addPoint($point1)
->build();

With no way to create a Polygon otherwise, due to the private constructor.

– Alex

On Mon, Mar 24, 2025, at 3:47 AM, Rob Landers wrote:

On Sun, Mar 23, 2025, at 16:17, Larry Garfield wrote:

I've been following this thread with interest, and at the moment I'm honestly undecided. I certainly see the use cases for this functionality (whatever it gets named), but as a practical matter it sounds like it introduces a lot of extra clunk and complexity. And it seems like the use cases could be addressed as well with either fileprivate or module-private. (The former being considerably less work.)

So, how would nested classes compare to fileprivate, in terms of ability to solve the problem space? As I understand it, the goal is:

1. Classes that can be instantiated only by the class that uses them.
2. But can be returned from that class to a caller and reused as appropriate.

The autoloading question (loading a whole file for just an implementation detail value object) is not one that carries much weight for me, as that's a user-space question, not an engine question. (Nothing in PHP itself says you cannot put 20 3 line classes or enums together in one file. It's just PSR-4 that says not go. Even composer would allow it if configured properly) So how would the less-complicated alternative compare?

--Larry Garfield

Hey Larry,

I think file-private would/could be useful, but that only limits you to
a "private" scope, which severely hampers what you can do with it. If
we went with "module-private" (rhetorical question: what is a module?),
but then you wouldn't be able to have "private" scope.

When I say module scope, I'm referring to something along the lines that Arnaud and I were exploring a while back. tldr, "cluster of files with a common namespace root, which can get loaded together." It was mostly about performance, but did offer module-private as well, with some nice potential. At the moment it's stalled out on "there's nasty hard edge cases and we're not sure if it's worth it" concerns.

Concept brain dump here: php-rfcs/modules/spec-brainstorm.md at master · Crell/php-rfcs · GitHub
Code exploration from Arnaud here: PoC modules by arnaud-lb · Pull Request #10 · arnaud-lb/php-src · GitHub

Still well short of RFC state, of course, but provided for context.

With nested/inner classes, for example, you can put a protected class
on a class or interface and access it only from those that use it,
regardless of what file or "module" (namespace?) they are in; the logic
can be encapsulated to where it is used, not where it is defined.

interface Shipment {
  protected class Items {}
  public readonly class Destination {}
  function deliver(self:>Destination $destination);
}

class InternationalShipment implements Shipment {
  private function handle(Shipment:>Items $items) {}

  public function deliver(Shipment:>Destination $destination) {}
}

In this case, I am not seeing what the nesting gets you. Making Destination a normal class doesn't hurt anything here, does it?

However, you can use private nested/inner classes to encapsulate the
logic to where it is defined instead, like file-private would give you.

The goal here isn't to only to reduce "namespace pollution" but also to
encapsulate related logic integrally connected to the outer class's
behavior. Since you've referenced Kotlin a number of times, I assume
you are familiar with the concept of nested classes there? This is
extremely similar.

My short foray into Kotlin did not include nested classes, so I cannot speak to them other than knowing they exist.

--Larry Garfield

On 24 March 2025 09:20:03 GMT, "Alexandru Pătrănescu" <drealecs@gmail.com> wrote:

On Sun, Mar 23, 2025 at 5:20 PM Larry Garfield <larry@garfieldtech.com>
wrote:

So, how would nested classes compare to fileprivate, in terms of ability
to solve the problem space? As I understand it, the goal is:

1. Classes that can be instantiated only by the class that uses them.
2. But can be returned from that class to a caller and reused as
appropriate.

I think the one other difference is that nested classes can access private
variables or their outer classes.

You can achieve the same with "file private" access: in your example mark the constructor as "file private", and put the Builder in the same file

fileprivate function __construct(private array $points) {}

If you'll excuse a brief philosophical detour, I noticed something interesting in the Swift documentation: the description of nested classes doesn't describe any special scope access. Instead, the description of "access levels" defines "private" in a way that naturally includes them:

Private access restricts the use of an entity to the enclosing declaration, and to extensions of that declaration that are in the same file.

It's a subtly different framing - nested types aren't breaking into the private entity, the "private" keyword is explicitly letting nested types in.

A language could, if it chose, have different keywords for "private to exactly this type" and "private to this type and any nested types":

strict_private function __construct(private array $points) {}
private_or_nested function __construct(private array $points) {}

Just as there are many combinations like "this class, subclasses, or other classes in this module".

The point being that there's nothing fundamental about nested types that gives them access to private properties. What they do is give us a new dimension to define access levels of our choice - { contains, contained by, neither }.

File scope gives us instead the dimension { same file, different file }; and module scope gives us { same module, different module, no module }, and maybe some additional relationships between modules.

Rowan Tommins
[IMSoP]

On Mon, Mar 24, 2025 at 5:12 PM Larry Garfield <larry@garfieldtech.com> wrote:

On Mon, Mar 24, 2025, at 3:47 AM, Rob Landers wrote:

On Sun, Mar 23, 2025, at 16:17, Larry Garfield wrote:

I’ve been following this thread with interest, and at the moment I’m honestly undecided. I certainly see the use cases for this functionality (whatever it gets named), but as a practical matter it sounds like it introduces a lot of extra clunk and complexity. And it seems like the use cases could be addressed as well with either fileprivate or module-private. (The former being considerably less work.)

So, how would nested classes compare to fileprivate, in terms of ability to solve the problem space? As I understand it, the goal is:

  1. Classes that can be instantiated only by the class that uses them.
  1. But can be returned from that class to a caller and reused as appropriate.

The autoloading question (loading a whole file for just an implementation detail value object) is not one that carries much weight for me, as that’s a user-space question, not an engine question. (Nothing in PHP itself says you cannot put 20 3 line classes or enums together in one file. It’s just PSR-4 that says not go. Even composer would allow it if configured properly) So how would the less-complicated alternative compare?

–Larry Garfield

Hey Larry,

I think file-private would/could be useful, but that only limits you to

a “private” scope, which severely hampers what you can do with it. If

we went with “module-private” (rhetorical question: what is a module?),

but then you wouldn’t be able to have “private” scope.

When I say module scope, I’m referring to something along the lines that Arnaud and I were exploring a while back. tldr, “cluster of files with a common namespace root, which can get loaded together.” It was mostly about performance, but did offer module-private as well, with some nice potential. At the moment it’s stalled out on “there’s nasty hard edge cases and we’re not sure if it’s worth it” concerns.

Concept brain dump here: https://github.com/Crell/php-rfcs/blob/master/modules/spec-brainstorm.md

Code exploration from Arnaud here: https://github.com/arnaud-lb/php-src/pull/10

Still well short of RFC state, of course, but provided for context.

My email has been broken for a few days, so sorry for the late response…

I suspect I know exactly what edges you were running into now… I’m now nearing completion of using \ and I did end up just making namespaces into logical structures (this is mostly used for bookkeeping, but with ~3 lines of code – minus grammar changes – you can support module privacy). The loading aspect (loading together) would probably run into the same issue I ran into: class binding order. This is illustrated here: https://3v4l.org/g9vC4

With PSR-4 autoloading, we basically never run into this problem because as soon as we compile C, we’ll late-bind it to B, which triggers the autoloader, which eventually binds A, then B, then C, in the correct order. With nested classes (or a module), order matters. I know how to fix it, at least for nested/inner classes so they can be linked at compile time. I suspect the solution would also apply to modules, but I don’t think it could apply generally across the language.

With nested/inner classes, for example, you can put a protected class

on a class or interface and access it only from those that use it,

regardless of what file or “module” (namespace?) they are in; the logic

can be encapsulated to where it is used, not where it is defined.

interface Shipment {

protected class Items {}

public readonly class Destination {}

function deliver(self:>Destination $destination);

}

class InternationalShipment implements Shipment {

private function handle(Shipment:>Items $items) {}

public function deliver(Shipment:>Destination $destination) {}

}

In this case, I am not seeing what the nesting gets you. Making Destination a normal class doesn’t hurt anything here, does it?

I wasn’t intending to write a definitive example, just to illustrate it. A better example might be a lazy cache, using hooks to notify the outer class it has possibly been updated:

(note this is using the new syntax, but this syntax is NOT currently reflected in the RFC text, yet)

namespace Caching;

class Cache {

private array $items = ;

private array $dirty = ;

public function getItem(string $key): CacheItem {

return $this->items[$key] ?? ($this->items[$key] = new CacheItem($this, $key, null));

}

public function saveChanges(): void {

foreach ($this->dirty as $key => $value) {

echo “Saving $key to persistent storage\n”;

}

$this->dirty = ;

}

private function markDirty(string $key): void {

$this->dirty[$key] = $this->items[$key];

}

public class CacheItem {

public string|null $value {

get {

return $this->_value;

}

set {

$this->_value = $value;

$this->cache->markDirty($this->key);

}

}

public function __construct(

private readonly Cache $cache,

public private(set) string $key,

private string|null $_value,

) {}

}

}

$cache = new Cache();

$item1 = $cache->getItem(‘foo’);

$item1->value = ‘bar’;

$cache->saveChanges();

This outputs:

Saving foo to persistent storage

This provides for a simple API for outside the Cache class: getItem() and saveChanges(); that’s it. For CacheItem’s, you only have the props for the key or value, and updating the value marks the item as “dirty”.

Currently, if you want this same API, the only way to do this is by:

  1. using reflection,

  2. using a closure and binding it to the other class’s scope,

  3. providing a listener + observer (more formal version of [2]), or

  4. iterating over items while saving to find dirty items.

Those choices are not ideal (IMHO).

Reflection feels “hacky” as does using a bound closure. The listener/observer pattern is probably overkill here, unless you already have one lying around (i.e., using symfony/laravel or a related framework), but still might require making some of the “internal” api public. Finally, iterating over all the items to find dirty ones is naive and inefficient.

Hopefully this is a better illustration of what nesting provides?

– Rob

On Tue, Mar 25, 2025, at 6:59 AM, Rob Landers wrote:

When I say module scope, I'm referring to something along the lines that Arnaud and I were exploring a while back. tldr, "cluster of files with a common namespace root, which can get loaded together." It was mostly about performance, but did offer module-private as well, with some nice potential. At the moment it's stalled out on "there's nasty hard edge cases and we're not sure if it's worth it" concerns.

Concept brain dump here: php-rfcs/modules/spec-brainstorm.md at master · Crell/php-rfcs · GitHub
Code exploration from Arnaud here: PoC modules by arnaud-lb · Pull Request #10 · arnaud-lb/php-src · GitHub

Still well short of RFC state, of course, but provided for context.

My email has been broken for a few days, so sorry for the late response...

No worries, I'm about to leave town myself. :slight_smile:

In this case, I am not seeing what the nesting gets you. Making Destination a normal class doesn't hurt anything here, does it?

I wasn't intending to write a definitive example, just to illustrate
it. A better example might be a lazy cache, using hooks to notify the
outer class it has possibly been updated:

(note this is using the new syntax, but this syntax is NOT currently
reflected in the RFC text, yet)

namespace Caching;

class Cache {
    private array $items = ;
    private array $dirty = ;

    public function getItem(string $key): CacheItem {
        return $this->items[$key] ?? ($this->items[$key] = new
CacheItem($this, $key, null));
    }

    public function saveChanges(): void {
        foreach ($this->dirty as $key => $value) {
            echo "Saving $key to persistent storage\n";
        }
        $this->dirty = ;
    }

    private function markDirty(string $key): void {
        $this->dirty[$key] = $this->items[$key];
    }

    public class CacheItem {
        public string|null $value {
            get {
                return $this->_value;
            }
            set {
               $this->_value = $value;
               $this->cache->markDirty($this->key);
            }
        }

        public function __construct(
            private readonly Cache $cache,
            public private(set) string $key,
            private string|null $_value,
        ) {}
    }
}

$cache = new Cache();
$item1 = $cache->getItem('foo');
$item1->value = 'bar';

$cache->saveChanges();

This outputs:

Saving foo to persistent storage

This provides for a simple API for outside the Cache class: getItem()
and saveChanges(); that's it. For CacheItem's, you only have the props
for the key or value, and updating the value marks the item as "dirty".

Currently, if you want this same API, the only way to do this is by:

1. using reflection,
2. using a closure and binding it to the other class's scope,
3. providing a listener + observer (more formal version of [2]), or
3. iterating over items while saving to find dirty items.

Those choices are not ideal (IMHO).

Reflection feels "hacky" as does using a bound closure. The
listener/observer pattern is probably overkill here, unless you already
have one lying around (i.e., using symfony/laravel or a related
framework), but still might require making some of the "internal" api
public. Finally, iterating over all the items to find dirty ones is
naive and inefficient.

Hopefully this is a better illustration of what nesting provides?

-- Rob

I have a similar if less involved use case in my ordering library, though it doesn't need the look-back functionality.

So the use case that nested classes would enable that isn't currently covered by "just use separate files and @internal, deal" is around the lesser class having private access to the greater class. Which... I believe fileprivate and modules would also address, albeit in a different way, yes? (Presuming fileprivate is available on properties, not just the class itself.)

--Larry Garfield

On Tue, Mar 25, 2025, at 19:51, Larry Garfield wrote:

On Tue, Mar 25, 2025, at 6:59 AM, Rob Landers wrote:

When I say module scope, I’m referring to something along the lines that Arnaud and I were exploring a while back. tldr, “cluster of files with a common namespace root, which can get loaded together.” It was mostly about performance, but did offer module-private as well, with some nice potential. At the moment it’s stalled out on “there’s nasty hard edge cases and we’re not sure if it’s worth it” concerns.

Concept brain dump here: https://github.com/Crell/php-rfcs/blob/master/modules/spec-brainstorm.md

Code exploration from Arnaud here: https://github.com/arnaud-lb/php-src/pull/10

Still well short of RFC state, of course, but provided for context.

My email has been broken for a few days, so sorry for the late response…

No worries, I’m about to leave town myself. :slight_smile:

In this case, I am not seeing what the nesting gets you. Making Destination a normal class doesn’t hurt anything here, does it?

I wasn’t intending to write a definitive example, just to illustrate

it. A better example might be a lazy cache, using hooks to notify the

outer class it has possibly been updated:

(note this is using the new syntax, but this syntax is NOT currently

reflected in the RFC text, yet)

namespace Caching;

class Cache {

private array $items = ;

private array $dirty = ;

public function getItem(string $key): CacheItem {

return $this->items[$key] ?? ($this->items[$key] = new

CacheItem($this, $key, null));

}

public function saveChanges(): void {

foreach ($this->dirty as $key => $value) {

echo “Saving $key to persistent storage\n”;

}

$this->dirty = ;

}

private function markDirty(string $key): void {

$this->dirty[$key] = $this->items[$key];

}

public class CacheItem {

public string|null $value {

get {

return $this->_value;

}

set {

$this->_value = $value;

$this->cache->markDirty($this->key);

}

}

public function __construct(

private readonly Cache $cache,

public private(set) string $key,

private string|null $_value,

) {}

}

}

$cache = new Cache();

$item1 = $cache->getItem(‘foo’);

$item1->value = ‘bar’;

$cache->saveChanges();

This outputs:

Saving foo to persistent storage

This provides for a simple API for outside the Cache class: getItem()

and saveChanges(); that’s it. For CacheItem’s, you only have the props

for the key or value, and updating the value marks the item as “dirty”.

Currently, if you want this same API, the only way to do this is by:

  1. using reflection,
  1. using a closure and binding it to the other class’s scope,
  1. providing a listener + observer (more formal version of [2]), or
  1. iterating over items while saving to find dirty items.

Those choices are not ideal (IMHO).

Reflection feels “hacky” as does using a bound closure. The

listener/observer pattern is probably overkill here, unless you already

have one lying around (i.e., using symfony/laravel or a related

framework), but still might require making some of the “internal” api

public. Finally, iterating over all the items to find dirty ones is

naive and inefficient.

Hopefully this is a better illustration of what nesting provides?

– Rob

I have a similar if less involved use case in my ordering library, though it doesn’t need the look-back functionality.

So the use case that nested classes would enable that isn’t currently covered by “just use separate files and @internal, deal” is around the lesser class having private access to the greater class. Which… I believe fileprivate and modules would also address, albeit in a different way, yes? (Presuming fileprivate is available on properties, not just the class itself.)

–Larry Garfield

Well … to use the best engineering response: “it depends…”

File-private, in my mind, is different; such as when the classes are distinctly unrelated with respect to each other, but nobody else should be able to instantiate or use the private class. It is like it doesn’t exist outside that file. For example, something like a log formatter or a default strategy implementation. Personally, I’d feel that file-private should be kept as simple as possible and limit it to “top-level” things, but that doesn’t necessarily have to be the case. If we did allow it on methods/properties, when mixing it with regular visibility, what happens? fileprivate public private(set) … means what exactly? I assume we probably wouldn’t allow that particular scenario, and maybe fileprivate on a property means public in the file, but private outside the file. But then how would that intersect with inheritance? My point is that I don’t think there is an intuitive answer to these behaviors, but at least nested/inner classes, while not 100% intuitive either, at least are available in other languages, so its behavior will be familiar.

If we were to target php 9, then we could simply redefine what private even means, similar to Rowan’s findings up thread (emphasis mine), and not even have a special keyword:

On Mon, Mar 24, 2025, at 20:22, Rowan Tommins [IMSoP] wrote:

If you’ll excuse a brief philosophical detour, I noticed something interesting in the Swift documentation: the description of nested classes doesn’t describe any special scope access. Instead, the description of “access levels” defines “private” in a way that naturally includes them:

Private access restricts the use of an entity to the enclosing declaration, and to extensions of that declaration that are in the same file.

It’s a subtly different framing - nested types aren’t breaking into the private entity, the “private” keyword is explicitly letting nested types in.

Module-private, on the other hand, would be a huge boon to php; for a number of reasons. After reading your brain-dump, it would be really interesting if we could package a binary representation of the module alongside the module itself – maybe not in an initial version, but later – so that it could basically be “pre-loaded” right into OPcache… But I digress. Your module idea looks far simpler to implement than this one :smiley:

I’d be happy to give it a go, if you’re up for it and no one else is working on it. It may be a few months, though; if this one passes, I want to go for short-classes next, plus I assume there is an expectation that I would fix bugs.

— Rob

On 25 March 2025 20:29:16 GMT, Rob Landers <rob@bottled.codes> wrote:

Personally, I'd feel that file-private should be kept as simple as possible and limit it to "top-level" things, but that doesn't necessarily have to be the case. If we did allow it on methods/properties, when mixing it with regular visibility, what happens? `fileprivate public private(set)` ... means what exactly?

If we didn't have "protected", would you ask the same about "protected private"? "fileprivate" would be just another access level, not something you'd combine with existing ones.

maybe `fileprivate` on a property means `public` in the file, but `private` outside the file. But then how would that intersect with inheritance?

That was the point of my philosophical rambling about Swift: you don't have to define new access levels in relation to old ones, or new features as exceptions to old definitions.

You can just define the keywords you allow, and the access they provide. That's true whether you're defining "private", "fileprivate", or "access_level_42".

For instance, "fileprivate" could simply mean "accessible from any code defined in this file". Since classes are fully declared in one file, that makes it a strict superset of the access currently meant by "private", and applies equally well to a whole type, a method, or a property.

I see no reason for inheritance to be involved at all. If we want an access level that means "accessible from any code in this file, or any subclass of the current type", we can make up a keyword for that as well - "fileprotected", or "fileprivate_or_protected", or whatever.

Rowan Tommins
[IMSoP]

On Tue, Mar 25, 2025, at 22:05, Rowan Tommins [IMSoP] wrote:

On 25 March 2025 20:29:16 GMT, Rob Landers <rob@bottled.codes> wrote:

Personally, I’d feel that file-private should be kept as simple as possible and limit it to “top-level” things, but that doesn’t necessarily have to be the case. If we did allow it on methods/properties, when mixing it with regular visibility, what happens? fileprivate public private(set) … means what exactly?

If we didn’t have “protected”, would you ask the same about “protected private”? “fileprivate” would be just another access level, not something you’d combine with existing ones.

Actually, probably yes :slight_smile: Mostly just to ask for clarification. In this case though, we have private(set) and protected(set); would we also want fileprivate(set)? That’s what I was getting at. How do we mix/match up all these things?

maybe fileprivate on a property means public in the file, but private outside the file. But then how would that intersect with inheritance?

That was the point of my philosophical rambling about Swift: you don’t have to define new access levels in relation to old ones, or new features as exceptions to old definitions.

You can just define the keywords you allow, and the access they provide. That’s true whether you’re defining “private”, “fileprivate”, or “access_level_42”.

For instance, “fileprivate” could simply mean “accessible from any code defined in this file”. Since classes are fully declared in one file, that makes it a strict superset of the access currently meant by “private”, and applies equally well to a whole type, a method, or a property.

I see no reason for inheritance to be involved at all. If we want an access level that means “accessible from any code in this file, or any subclass of the current type”, we can make up a keyword for that as well - “fileprotected”, or “fileprivate_or_protected”, or whatever.

Rowan Tommins

[IMSoP]

Inheritance gets involved in traits. Traits do “inherit” private access properties (currently): https://3v4l.org/89I7A

Would file-private properties/methods also be available outside the file, or would those allow access from inside the new file? Or both?

— Rob

On 25 March 2025 21:23:48 GMT, Rob Landers <rob@bottled.codes> wrote:

If we didn't have "protected", would you ask the same about "protected private"? "fileprivate" would be just another access level, not something you'd combine with existing ones.

Actually, probably yes :slight_smile: Mostly just to ask for clarification. In this case though, we have private(set) and protected(set); would we also want fileprivate(set)? That's what I was getting at. How do we mix/match up all these things?

I don't see what needs deciding - private(set) isn't a new access level, it's basically sugar for a setter that's marked private.

Try replacing "private" with "level1", and "protected" with "level2":

level1(set) level2(get) int $foo;

And then add "level1a"; can you use it in the same places? Of course:

level1a(set) level2(get) int $foo;

So why would there be any ambiguity about writing this?

fileprivate(set) protected(get) int $foo;

> maybe `fileprivate` on a property means `public` in the file, but `private` outside the file. But then how would that intersect with inheritance?

Just to call back to this: it's like saying "protected means public inside the class and its descendants, but private everywhere else". It's an unnecessarily confusing way of describing it, because you then have to define "public" and "private" without the definitions being recursive.

A more straightforward description is "protected means accessible inside the class and its descendants, and nowhere else".

Maybe having "private" in the name is putting you off, and this is clearer:

samefile(set) samemodule(get) int $foo;

No "private" or "public" involved anywhere, just descriptions of where the property can be accessed.

I see no reason for inheritance to be involved at all. If we want an access level that means "accessible from any code in this file, or any subclass of the current type", we can make up a keyword for that as well - "fileprotected", or "fileprivate_or_protected", or whatever.

Inheritance gets involved in traits. Traits do "inherit" private access properties (currently): Online PHP editor | output for 89I7A

Traits don't inherit anything, and they don't restrict anything either. They paste code in, and once pasted it acts like it was written in the new location. You can even change access levels while pasting, with the syntax "use Foo { bar as public }".

The "private" keyword in your example is pasted into class Foo, and means "accessible within class Foo". It never applies any restriction relative to trait Bar, because running code never belongs to the trait.

A "fileprivate"/"samefile" keyword would be pasted into the file it was used in, and mean accessible within that file; it wouldn't matter what file the trait was defined in. It would probably be useless, but lots of useless code is possible in any language.

Besides, all these questions have to be answered for nested classes as well. Just because you've reused the keyword "private" rather than adding "private_or_nested", you still have to define exactly what it does and doesn't allow access from in these new scopes.

Rowan Tommins
[IMSoP]

On Tue, Mar 25, 2025, at 23:20, Rowan Tommins [IMSoP] wrote:

On 25 March 2025 21:23:48 GMT, Rob Landers <rob@bottled.codes> wrote:

If we didn’t have “protected”, would you ask the same about “protected private”? “fileprivate” would be just another access level, not something you’d combine with existing ones.

Actually, probably yes :slight_smile: Mostly just to ask for clarification. In this case though, we have private(set) and protected(set); would we also want fileprivate(set)? That’s what I was getting at. How do we mix/match up all these things?

I don’t see what needs deciding - private(set) isn’t a new access level, it’s basically sugar for a setter that’s marked private.

Try replacing “private” with “level1”, and “protected” with “level2”:

level1(set) level2(get) int $foo;

And then add “level1a”; can you use it in the same places? Of course:

level1a(set) level2(get) int $foo;

So why would there be any ambiguity about writing this?

fileprivate(set) protected(get) int $foo;

maybe fileprivate on a property means public in the file, but private outside the file. But then how would that intersect with inheritance?

Just to call back to this: it’s like saying “protected means public inside the class and its descendants, but private everywhere else”. It’s an unnecessarily confusing way of describing it, because you then have to define “public” and “private” without the definitions being recursive.

A more straightforward description is “protected means accessible inside the class and its descendants, and nowhere else”.

Maybe having “private” in the name is putting you off, and this is clearer:

samefile(set) samemodule(get) int $foo;

No “private” or “public” involved anywhere, just descriptions of where the property can be accessed.

To be clear, I’m not trying to be difficult. As you mentioned, these were all things I had to think about for nested classes too. I know how nested classes work and why. How file-private would work, on the other hand, I have put little thought into. You say it is obvious (to paraphrase), but the devil is in the details, and there isn’t much prior-art to draw from here either.

The only prior art I can think of is Swift’s fileprivate and C’s static. Beyond that, I am not aware of any other language to offer this feature. That being said, we can certainly define it any way we want to, but asking ‘dumb questions’ and challenging assumptions will help us find the rough edges and things we didn’t consider before.

I see no reason for inheritance to be involved at all. If we want an access level that means “accessible from any code in this file, or any subclass of the current type”, we can make up a keyword for that as well - “fileprotected”, or “fileprivate_or_protected”, or whatever.

Inheritance gets involved in traits. Traits do “inherit” private access properties (currently): https://3v4l.org/89I7A

Traits don’t inherit anything, and they don’t restrict anything either. They paste code in, and once pasted it acts like it was written in the new location. You can even change access levels while pasting, with the syntax “use Foo { bar as public }”.

The “private” keyword in your example is pasted into class Foo, and means “accessible within class Foo”. It never applies any restriction relative to trait Bar, because running code never belongs to the trait.

A “fileprivate”/“samefile” keyword would be pasted into the file it was used in, and mean accessible within that file; it wouldn’t matter what file the trait was defined in. It would probably be useless, but lots of useless code is possible in any language.

I agree, but these are all things we’d have to consider. I, personally, would consider it working the other way around. A trait declaring fileprivate would only be accessible in the trait; otherwise you would have to explain how fileprivate works without saying “the file it is written in” and in a way that is easy to understand – for the RFC + docs. I don’t know if it would be useless or useful though. Personally, I’d probably implement both ways and see how useful each one is when implementing a toy project and then weigh the pros/cons. It might even be a “why not both?” type of situation. It’s easier to explain if it is both (your description above works perfectly for that), but then would allow for some odd behaviors.

— Rob

On Tue, Mar 25, 2025, at 3:29 PM, Rob Landers wrote:

File-private, in my mind, is different; such as when the classes are
distinctly unrelated with respect to each other, but nobody else should
be able to instantiate or use the private class. It is like it doesn't
exist outside that file. For example, something like a log formatter or
a default strategy implementation. Personally, I'd feel that
file-private should be kept as simple as possible and limit it to
"top-level" things, but that doesn't necessarily have to be the case.
If we did allow it on methods/properties, when mixing it with regular
visibility, what happens? `fileprivate public private(set)` ... means
what exactly? I assume we probably wouldn't allow that particular
scenario, and maybe `fileprivate` on a property means `public` in the
file, but `private` outside the file. But then how would that intersect
with inheritance? My point is that I don't think there is an intuitive
answer to these behaviors, but at least nested/inner classes, while not
100% intuitive either, at least are available in other languages, so
its behavior will be familiar.

The aviz RFC was designed explicitly to support this sort of case. :slight_smile: (Module or file visibility.) The syntax was borrowed from Swift, which has 5 levels: public, package, internal (within the same module, which is apparently not the same as a package), file-private, and private.

The syntax for a visibility modifier is

$accessLevel($operation)

If the $operation is omitted, it means "any operation not otherwise specified." Right now the only explicit operation is `set`, though there's no reason `get` couldn't be made available on its own, too. We didn't, because right now there's no reason *to* do it, either. :slight_smile: But the logical structure is there.

If we add more access levels, we just have more options for that part of the modifier. If we add more operations, we just have more options for that modifier.

So if, hypothetically, we added a `module` visibility between public and protected, and added support for controlling the ability to unset() a property separately from setting it (for whatever reason, bear with me), the syntax extends to that quite naturally:

public fileprivate(set) private(unset) string $foo;

(With the standalone `public` still being optional.)

Methods only have one action, call, so there's no aviz to consider. Just `fileprivate function foo(int $a) { ... }`.

There may be other edge cases and gotchas to consider (eg, traits, as you note), but the syntax to use is already defined.

--Larry Garfield

Why not just add structs at this point? It’s almost like we don’t want to acknowledge that structs are a thing.

On 26 March 2025 00:22:47 GMT, Rob Landers <rob@bottled.codes> wrote:

To be clear, I'm not trying to be difficult. As you mentioned, these were all things I had to think about for nested classes too. I know how nested classes work and why. How file-private would work, on the other hand, I have put little thought into. You say it is obvious (to paraphrase), but the devil is in the details, and there isn't much prior-art to draw from here either.

I think in some ways this is similar to the discussion I had with Edmond on the async thread - my starting assumption is that new features should be consistent with existing ones, unless there's a good reason to make an exception.

For instance, the language has various built-in types, so if we add a new type, I would assume it will work in all the places that other types work. It might be useful to ask questions like "does it need any special handling as a return type?" but the answer will nearly always be "no". Asking "what behaviour should it have as a return type?" is only necessary if you've answered "yes" to the simpler question.

Similarly, the language has three built-in access levels, so if we add a new one, I would assume it will work in all the same places, unless there's some specific reason to add a special case.

I agree, but these are all things we'd have to consider. I, personally, would consider it working the other way around. A trait declaring fileprivate would only be accessible in the trait; otherwise you would have to explain how fileprivate works without saying "the file it is written in" and in a way that is easy to understand -- for the RFC + docs. I don't know if it would be useless or useful though.

Just to be clear, my description is just how traits work today. The phrase "compiler-assisted copy-and-paste" is widely used; I thought it was in the manual, but can't see it at a glance. As soon as you talk about "inheriting from traits", you're going to be on the wrong track.

Following the reasoning above, my thought process goes like this:

1) What does "private" mean in a trait?

It means "copy this property / method with a private modifier by default". Access is granted to all code in the target class, not just other code in the trait.

2) Can that apply to the proposed description of "fileprivate"/"samefile"?

Yes.

3) Is there a good reason to change that meaning and make the new feature inconsistent?

I don't think so.

Any change would mean changing the description of traits (they would no longer just be copied and pasted code), and would lead to additional questions (e.g. what happens when the target class changes the visibility with an "as" clause?).

Any inconsistent behaviour should have to clear a high bar.

Rowan Tommins
[IMSoP]