RFC: short and inner classes

php.internals

Rob Landers

1 year ago
Hello PHP Internals, I'd like to introduce my RFC for discussion: https://wiki.php.net/rfc/short-and-inner-classes This RFC defines a short class syntax as well as the ability to nest classes inside another class. This introduces an unprecedented amount of control, flexibility, and expressiveness over how objects are used and instantiated in PHP. There is a PR (https://github.com/php/php-src/pull/17895) that implements this functionality -- all test failures are related to different/new/incorrect error messages being generated. However, the core functionality exists to take for a test ride. So, what do I mean by "unprecedented amount of control"? With this change, you can declare an inner class as private or protected, preventing its usage outside of the outer class: class User { private class Id {} public function __construct(public self::Id $id) {} } In the above example, the class `User` is impossible to construct even though it has a public constructor (except through reflection) because User::Id is private; User::Id cannot be instantiated, used as a type hint, or even via `instanceof` outside of the User class itself. This example isn't practical but demonstrates something that is nearly impossible in previous versions of PHP, where all classes are essentially publicly accessible from anywhere within the codebase. As a number of inner classes will probably be used as DTOs, the RFC introduces a "short syntax" for declaring classes, which enhances expressiveness, even allowing the usage of traits, all in a single line: // declare a readonly Point, that implements Vector2 and uses the Evolvable trait readonly class Point(public int $x, public int $y) implements Vector2 use Evolvable; When combined with inner classes, it looks something like this: class Pixel { public readonly class Point(public int $x, public int $y) implements Vector2 use Evolvable; } // Create a new pixel point with property $x and $y set to 0 $p = new Pixel::Point(0, 0); There are far more details in the RFC itself, so please check it out. I'm quite excited to hear your thoughts! — Rob PS. I know I tend to rush into things, but I want to make it clear that I'm not rushing this -- I've learned from my mistakes (thank you to those who have given me advice). I'm going to do this right.

Juris Evertovskis

1 year ago
Hi, Maybe I didn’t read the RFC carefully enough, but… Do any of these features require the other one? I’m asking because I wouldn’t want to see them both denied just because the voters disagree with one of them. Good luck with this proposal! Juris

Rob Landers

1 year ago
On Thu, Mar 6, 2025, at 02:08, Juris Evertovskis wrote:
> Hi, > > Maybe I didn’t read the RFC carefully enough, but… Do any of these features require the other one? > > I’m asking because I wouldn’t want to see them both denied just because the voters disagree with one of them. > > Good luck with this proposal! > > Juris
Hi Juris, Yes and no. Without the short syntax, classes explode in size if you just want to have simple inner classes for DTO/organization. For example, I used this for quite a bit over the last couple of weeks. One big usage, from my experiments, was to replace array returns: class StringList(public array $strings); And it makes it “easy on the eyes” to just have a list of classes at the top of the class. By itself, a short syntax makes some sense, but isn’t compelling (IMHO), and inner classes cause an explosion in LoC without it. So, technically, they aren’t required to be in the same RFC; but also, they complement each other very well. If the RFC fails due to them being together, I’ll take the feedback and go back to the drawing board. — Rob

Tim Düsterhus

1 year ago
Hi Am 2025-03-06 07:23, schrieb Rob Landers:
> So, technically, they aren’t required to be in the same RFC; but also, > they complement each other very well.
They really should be separate RFCs then. Your RFC text acknowledges that in the very first sentence: “two significant enhancements to the language”. Each individual proposal likely has sufficient bike-shedding potential on its own and discussion will likely get messy, because one needs to closely follow which of the two proposals an argument relates to. --------- I've also given your RFC a first pass, without yet trying to understand all the implications. Here's some comments. As for the “Short classes” proposal: - I don't understand the use of `private` properties. Given that the classes cannot have methods, they would be inaccessible, no? As for the “Inner classes” proposal: - “abstract is not allowed as an inner class cannot be parent classes.” - Why? - “type hint” - PHP does not have type hints, types are enforced. You mean “Type declaration”. - “this allows you to redefine an inner class in a subclass, allowing rich hierarchies” - The RFC does not specify if and how this interacts with the LSP checks. Best regards Tim Düsterhus

Juris Evertovskis

1 year ago
On 2025-03-06 10:04, Tim Düsterhus wrote:
> - I don't understand the use of `private` properties. Given that the > classes cannot have methods, they would be inaccessible, no?
I think the RFC was a bit unclear on this. Short classes can have methods. The short syntax just doesn't provide the ability to define them whilst defining the class. But otherwise they are indistinguishable from normal classes and they can have methods by inheriting them or by using traits. BR, Juris

Rob Landers

1 year ago
On Thu, Mar 6, 2025, at 09:04, Tim Düsterhus wrote:
> Hi > > Am 2025-03-06 07:23, schrieb Rob Landers: > > So, technically, they aren’t required to be in the same RFC; but also, > > they complement each other very well. > > They really should be separate RFCs then. Your RFC text acknowledges > that in the very first sentence: “two significant enhancements to the > language”. Each individual proposal likely has sufficient bike-shedding > potential on its own and discussion will likely get messy, because one > needs to closely follow which of the two proposals an argument relates > to.
I put a lot of thought into this issue off and on, all day. I've decided to remove short syntax from the RFC and focus on inner classes. If this passes, then I will propose it as a separate RFC. Introducing them concurrently makes little sense in light of the feedback I have gotten so far, and it is turning out that there is much more to discuss than I initially expected. Thus, I will skip replying about short classes.
> > As for the “Inner classes” proposal: > > - “abstract is not allowed as an inner class cannot be parent classes.” > - Why?
This is mostly a technical reason, as I was unable to determine a grammar rule that didn't result in ambiguity. Another reason is to ensure encapsulation and prevent usages outside their intended scope. We can always add it later.
> - “type hint” - PHP does not have type hints, types are enforced. You > mean “Type declaration”.
Thank you for pointing this out! I learned something new today! I've updated the RFC.
> - “this allows you to redefine an inner class in a subclass, allowing > rich hierarchies” - The RFC does not specify if and how this interacts > with the LSP checks.
It doesn't affect LSP. I've updated the RFC accordingly. On Thu, Mar 6, 2025, at 20:08, Niels Dossche wrote:
> Hi Rob > > Without looking too deep (yet) into the details, I'm generally in favor of the idea. > What I'm less in favor of is the implementation choice to expose the inner class as a property/const and using a fetch mode to grab it. > That feels quite weird to me honestly. How did you arrive at this choice? > > Kind regards > Niels
It's a slightly interesting story about how I arrived at this particular implementation. If you noticed the branch name, this is the second implementation. The first implementation used a dedicated list on the class-entry for inner classes. Since I wanted to prevent static property/consts from being declared with the same name, I had just set it to a string of the full class name as a placeholder. That implementation also required some pretty dramatic OPcache changes, which I didn't like. At one point, I went to add the first test that did `new Outer::Inner()` and the test passed... You can imagine my surprise to see a test pass that I had expected to fail, and it was then that I went into the details of what was going on. Any `new ClassName` essentially results in the following AST: ZEND_AST_NEW -- ZEND_AST_ZVAL -- "ClassName" -- (... args) The original grammar, at the time, was to reuse the existing static property access AST until I could properly understand OPcache/JIT. My change had resulted in (approximately) this AST: ZEND_AST_NEW -- ZEND_AST_ZVAL -- ZEND_AST_STATIC_PROP -- "Outer::Inner" -- (... args) Which, effectively resulted in emitting opcodes that found the prop + string value I happened to put there as a placeholder until I figured out a better solution, handling autoloading properly and everything. This pretty much negated all efforts up to that point, and I was stunned. So, I branched off from an earlier point and eventually wrote the version you see today. It's 1000x simpler and faster than the original implementation (literally), since it uses all pre-existing (optimized)) infrastructure instead of creating entirely new infrastructure. It doesn't have to check another hashmap (which is slow) for static props vs. constants vs. inner classes. In essence, while the diff can be improved further, it is quite simple; the core of it is less than 500 lines of code. I'd recommend leaving any comments about the PR on the PR itself (or via private email if you'd prefer that). I'm by no means an expert on this code base, and if it is not what you'd expect, being an expert yourself, I'd love to hear any suggestions for improvements or other approaches. On Thu, Mar 6, 2025, at 20:33, Larry Garfield wrote:
> My biggest concern with this is that it makes methods and short-classes mutually incompatible. So if you have a class that uses short-syntax, and as it evolves you realize it needs one method, sucks to be you, now you have to rewrite basically the whole class to a long-form constructor. That sucks even more than rewriting a short-lambda arrow function to a long-form closure, except without the justification of capture semantics.
I literally fell out of my chair laughing. Thanks for that, and it is true. I look forward to discussing this further!
> ## Inner classes > > I'm on board with the use case. What I'm not sure on is inner classes vs file-private visibility, something that Ilija was working on at one point and Michał Brzuchalski suggested in his post. Both solve largely the same problem with different spelling. > > Arguably, inner classes have fewer issues with current autoload conventions. I must ponder this further.
Indeed! See: https://externals.io/message/126331#126337
> > > However, no classes may not inherit from inner classes, but inner classes may inherit from other classes, including the outer class. > > I think you have one too many negatives in that sentence.
Thank you, this is fixed! — Rob

Tim Düsterhus

1 year ago
Hi On 3/6/25 22:38, Rob Landers wrote:
> I put a lot of thought into this issue off and on, all day. I've decided to remove short syntax from the RFC and focus on inner classes.
Good choice. Don't forget to update the title of the RFC in the Overview page: https://wiki.php.net/rfc Best regards Tim Düsterhus

Michał Brzuchalski

1 year ago
Hi Rob, czw., 6 mar 2025 o 00:16 Rob Landers <rob@bottled.codes> napisał(a):
> Hello PHP Internals, > > I'd like to introduce my RFC for discussion: > https://wiki.php.net/rfc/short-and-inner-classes > > This RFC defines a short class syntax as well as the ability to nest > classes inside another class. This introduces an unprecedented amount of > control, flexibility, and expressiveness over how objects are used and > instantiated in PHP. There is a PR ( > https://github.com/php/php-src/pull/17895) that implements this > functionality -- all test failures are related to different/new/incorrect > error messages being generated. However, the core functionality exists to > take for a test ride. > > So, what do I mean by "unprecedented amount of control"? With this change, > you can declare an inner class as private or protected, preventing its > usage outside of the outer class: > > class User { > private class Id {} > > public function __construct(public self::Id $id) {} > } > > In the above example, the class `User` is impossible to construct even > though it has a public constructor (except through reflection) because > User::Id is private; User::Id cannot be instantiated, used as a type hint, > or even via `instanceof` outside of the User class itself. This example > isn't practical but demonstrates something that is nearly impossible in > previous versions of PHP, where all classes are essentially publicly > accessible from anywhere within the codebase. > > As a number of inner classes will probably be used as DTOs, the RFC > introduces a "short syntax" for declaring classes, which enhances > expressiveness, even allowing the usage of traits, all in a single line: > > // declare a readonly Point, that implements Vector2 and uses the > Evolvable trait > readonly class Point(public int $x, public int $y) implements Vector2 use > Evolvable; > > When combined with inner classes, it looks something like this: > > class Pixel { > public readonly class Point(public int $x, public int $y) implements > Vector2 use Evolvable; > } > > // Create a new pixel point with property $x and $y set to 0 > $p = new Pixel::Point(0, 0); > > There are far more details in the RFC itself, so please check it out. I'm > quite excited to hear your thoughts! > > — Rob > > PS. I know I tend to rush into things, but I want to make it clear that > I'm not rushing this -- I've learned from my mistakes (thank you to those > who have given me advice). I'm going to do this right. >
Inner classes - YES, Short classes - YES, Short empty classes - YES - very nice solution for extending Exception class, maybe there is no need for parentheses??, Traits in single line - dunno, personally don't see the need so no much preference here, IMO would be debatable. Especially both together. Inner classes are something I was thinking about many years ago [1]. And the short classes are something that is also in my field of interest. I remember this was proposed some time ago on ML. Currently, all of the classes replace array-shapes I mark with additional docblock including `@internal` tag. Many DTO's have no logic or not that much, meaning they don't need methods. An example which you may think could be expressed differently but since I'm used to expressing my opinion as fast as possible this is what came to my mind now. Possibly not all of the inner classes should be inner, but there is one that definitely should be, see: final class TZParser { /** * @param TZTypeInfo[] $types * @param TTInfo[] $transitions * @param string[] $abbreviations * @param array<int,array{timestamp:int,corr:int}> $leapSecondData */ public class TZInfo( string $version, bool $isV2Plus, array $types, array $transitions, array $abbreviations, array $leapSecondData, string|null $posixString, ); protected class TZTypeInfo(public int $gmtOffset, bool $isDst, int $abbreviationIndex, bool $isStd = false, bool $isUt = false) protected class TTInfo(int $timestamp, int $typeIndex); private class TZInfoSection(array $types, array $transitions, array $abbreviations, array $leaps, int $newOffset); public static function parse(string $filename): TZInfo { /** impl */ } } While it looks different with short class and doc block for TZInfo, for the others that don't require docblock this is pretty nice, even if the fields would be declared each in a separate line - it's a personal preference. Additionally, I could easily remove the some prefixes I see this way of declaring DTO's more convenient than having 3 separate files for just a small DTO class where I need to put `/** @internal */` on each of them. Inner classes won't be possible to instantiate outside of the TZParser class which is intended here. All instances should be available outside of the parser to read the information with one exception for TZInfoSection. In this example the TZInfoSection class is used internally (which is why marked as private) by the parser itself, it's not exposed outside because there is no real use case for that which makes this feature very interesting to me. If the RFC would be splitted into smaller or not I say YES for inner+short(and empty). I hope others will see proposed features as useful as I. Good luck. [1] https://brzuchal.com/posts/inner-classes-in-php-concept/

Niels Dossche

1 year ago
Hi Rob Without looking too deep (yet) into the details, I'm generally in favor of the idea. What I'm less in favor of is the implementation choice to expose the inner class as a property/const and using a fetch mode to grab it. That feels quite weird to me honestly. How did you arrive at this choice? Kind regards Niels

Tim Düsterhus

1 year ago
Hi On 3/6/25 20:08, Niels Dossche wrote:
> What I'm less in favor of is the implementation choice to expose the inner class as a property/const and using a fetch mode to grab it. > That feels quite weird to me honestly. How did you arrive at this choice?
Somewhat relatedly, the RFC does not mention how the choice of `::` as the separator interacts with the following features (i.e. what will the result of each of the statements be): Closure::fromCallable('Outer::Inner::method'); new ReflectionMethod('Outer::Inner::method'); defined('Outer::Inner'); constant('Outer::Inner'); $inner = 'Inner'; Outer::{$inner}; … and any other meta-programming functionality working on class constants or static methods. Also, what will happen for: class P { class Inner { } } class C extends P { const Inner = 'x'; } (and vice versa) Best regards Tim Düsterhus

Rob Landers

1 year ago
On Thu, Mar 6, 2025, at 22:00, Tim Düsterhus wrote:
> Hi > > On 3/6/25 20:08, Niels Dossche wrote: > > What I'm less in favor of is the implementation choice to expose the inner class as a property/const and using a fetch mode to grab it. > > That feels quite weird to me honestly. How did you arrive at this choice? > > Somewhat relatedly, the RFC does not mention how the choice of `::` as > the separator interacts with the following features (i.e. what will the > result of each of the statements be):
Sorry to double post, but before updating the RFC, I figure I'll go ahead and answer here and see if it is what you expect.
> > Closure::fromCallable('Outer::Inner::method');
You end up with: object(Closure)#1 (1) { ["function"]=> string(20) "Outer::Inner::method" }
> new ReflectionMethod('Outer::Inner::method');
The current implementation returns an error here, but it should give you an actual ReflectionMethod. I'll have to take a look and see what is going on.
> defined('Outer::Inner');
This returns: `true`.
> constant('Outer::Inner');
This returns: string(12) "Outer::Inner"
> $inner = 'Inner'; > Outer::{$inner};
This does nothing (but resolves to "Outer::Inner")
> … and any other meta-programming functionality working on class > constants or static methods. > > Also, what will happen for: > > class P { > class Inner { } > } > > class C extends P { > const Inner = 'x'; > } > > (and vice versa)
This is a really good one. If for no other reason than I did a really poor job of explaining resolution(?) in the RFC. `P::Inner` belongs to `P`, not to `C`, so you can do `new C::Inner()` and it will resolve to `P::Inner()`: object(P::Inner)#2 (0) { } As with other static things in PHP, you can do some really strange things like this. This is similar to how you can redefine static constants in subclasses. My main goal is to prevent exactly this type of confusion: new C::Inner() vs. echo C::Inner.
> Somewhat relatedly, the RFC does not mention how the choice of `::` as > the separator
This felt the most natural to me, but as with all syntax on this list, I'd be interested in other colors to paint the bikeshed! Especially ones that I may not have previously considered! — Rob

Tim Düsterhus

1 year ago
Hi On 3/6/25 23:05, Rob Landers wrote:
>> >> Closure::fromCallable('Outer::Inner::method'); > > You end up with: > > object(Closure)#1 (1) { > ["function"]=> > string(20) "Outer::Inner::method" > }
Okay, does calling the closure work and correctly call the `method` on the inner class? The question was intended to make sure that the implementation for callables uses the correct `::` to split. Here's another one that might be interesting: Closure::fromCallable(["Outer::Inner", "method"]); Closure::fromCallable(["Outer", "Inner::method"]);
>> constant('Outer::Inner'); > > This returns: > > string(12) "Outer::Inner"
Okay, so this behaves as a constant containing the class name. I assume it's with the full namespace if the outer class is namespaced? I'm not sure if I want this to work like this (i.e. whether this should be an error).
>> $inner = 'Inner'; >> Outer::{$inner}; > > This does nothing (but resolves to "Outer::Inner")
It's consistent with `constant()` and that's good.
>> … and any other meta-programming functionality working on class >> constants or static methods. >> >> Also, what will happen for: >> >> class P { >> class Inner { } >> } >> >> class C extends P { >> const Inner = 'x'; >> } >> >> (and vice versa) > > This is a really good one. If for no other reason than I did a really poor job of explaining resolution(?) in the RFC. `P::Inner` belongs to `P`, not to `C`, so you can do `new C::Inner()` and it will resolve to `P::Inner()`:
I don't think the RFC explains “resolution” at all. That's why I'm asking with those specific “edge-casey” examples, so that the RFC explicitly spells out the behavior. This is not something that should be “implementation defined”, but something where an explicit design decision has been made. I also don't understand why `new C::Inner()` (w|sh)ould resolve to `P::Inner()`. I think my expectation of the code snippet above would be that it is an error. Likewise, LSP being ignored for inner classes raises an interesting question about the behavior of: class P { class Inner { public function __construct(public string $foo) { } } public static function create() { return new static::Inner('x'); } } class C extends P { class Inner { public function __construct(public int $bar) { } } } What happens if I call `C::create()`? This should also be specified in the RFC (and tested with a .phpt test).
> As with other static things in PHP, you can do some really strange things like this. This is similar to how you can redefine static constants in subclasses.
We should remove the number of strange things, not add to them. Best regards Tim Düsterhus

Rob Landers

1 year ago
On Thu, Mar 6, 2025, at 23:31, Tim Düsterhus wrote:
> Hi > > On 3/6/25 23:05, Rob Landers wrote: > >> > >> Closure::fromCallable('Outer::Inner::method'); > > > > You end up with: > > > > object(Closure)#1 (1) { > > ["function"]=> > > string(20) "Outer::Inner::method" > > } > > Okay, does calling the closure work and correctly call the `method` on > the inner class?
Yep, it works!
> The question was intended to make sure that the > implementation for callables uses the correct `::` to split.
This is largely why nesting can only be one level deep. Multiple levels end up with far more intrusive changes to the engine and IMHO, severely impact readability. I'll update the RFC on this.
> Here's > another one that might be interesting: > > Closure::fromCallable(["Outer::Inner", "method"]); > Closure::fromCallable(["Outer", "Inner::method"]);
object(Closure)#1 (1) { ["function"]=> string(20) "Outer::Inner::method" } and Uncaught TypeError: Failed to create closure from callable: class "Inner" not found I believe this is correct? I haven't seen this form since my 5.3 days, so I am not sure if it is correct or a bug.
> > >> constant('Outer::Inner'); > > > > This returns: > > > > string(12) "Outer::Inner" > > Okay, so this behaves as a constant containing the class name. I assume > it's with the full namespace if the outer class is namespaced? I'm not > sure if I want this to work like this (i.e. whether this should be an > error).
That's correct, it contains the fully qualified name. I may have to think on this, but I concur that this might be better as an error.
> > >> $inner = 'Inner'; > >> Outer::{$inner}; > > > > This does nothing (but resolves to "Outer::Inner") > > It's consistent with `constant()` and that's good. > > >> … and any other meta-programming functionality working on class > >> constants or static methods. > >> > >> Also, what will happen for: > >> > >> class P { > >> class Inner { } > >> } > >> > >> class C extends P { > >> const Inner = 'x'; > >> } > >> > >> (and vice versa) > > > > This is a really good one. If for no other reason than I did a really poor job of explaining resolution(?) in the RFC. `P::Inner` belongs to `P`, not to `C`, so you can do `new C::Inner()` and it will resolve to `P::Inner()`: > > I don't think the RFC explains “resolution” at all. That's why I'm > asking with those specific “edge-casey” examples, so that the RFC > explicitly spells out the behavior. This is not something that should be > “implementation defined”, but something where an explicit design > decision has been made.
100% agree. I'll update the RFC after thinking through some points in your email.
> > I also don't understand why `new C::Inner()` (w|sh)ould resolve to > `P::Inner()`. I think my expectation of the code snippet above would be > that it is an error.
100% agree. It actually should be an error, according to the RFC. To be honest, until I wrote my response to you, I forgot you could redefine constants. So, I'll have to take a look at this.
> > Likewise, LSP being ignored for inner classes raises an interesting > question about the behavior of: > > class P { > class Inner { > public function __construct(public string $foo) { } > } > > public static function create() { > return new static::Inner('x'); > } > } > > class C extends P { > class Inner { > public function __construct(public int $bar) { } > } > } > > What happens if I call `C::create()`? This should also be specified in > the RFC (and tested with a .phpt test).
I'll add a test for it (and detail in the RFC), but it will instantiate a C::Inner. This doesn't violate LSP, though, because C::Inner is distinct from P::Inner -- they are separate classes and completely unrelated. This is just like using static properties or constants. To explain further, static::Inner is about the same as doing `new $class::Inner;`-ish. You are not referencing the same class depending on where you are calling the function from. The fact that `static` is a shorthand for this is what makes it weird. I wouldn't be opposed to making `new static::Inner` (or even `new self::Inner`) an error, since it is confusing; it would force people to spell out the class name. However, I think it is useful when the different inner classes implement the same interfaces (explicitly or implicitly). I'd be interested to hear thoughts on this, but I'm now wondering if static:: allows casual violations of LSP, in general. 🤔
> > > As with other static things in PHP, you can do some really strange things like this. This is similar to how you can redefine static constants in subclasses. > > We should remove the number of strange things, not add to them.
Truth. Thank you for these questions Tim! — Rob

Larry Garfield

1 year ago
On Wed, Mar 5, 2025, at 5:11 PM, Rob Landers wrote:
> Hello PHP Internals, > > I'd like to introduce my RFC for discussion: > https://wiki.php.net/rfc/short-and-inner-classes
I agree with others who have said this should be two RFCs. They stand alone, but can complement each other well. That's fine, talk about how that works, but they're separate RFCs. (Like we split hooks and aviz.) I'm broadly in favor of short classes and warm on inner classes, with assorted caveats as below. ## Short classes The no-method-definitions restriction seems odd to me. I don't really see what value that provides. The main advantages are 1. Even shorter way to define promoted properties for the common case. 2. No need to define a body if you don't need one. Neither of those conflicts with defining methods. If the concern is the verbosity of method definitions, well, I tried: https://wiki.php.net/rfc/short-functions I still think that would be beneficial, but am frying other fish at the moment. So really, all it need to do is allow inline definition of the properties by the class name as an alternative to a constructor. class Point(int $x, int $y) {} class Rect(Point $tl, Point $br) { public method area() { /* ... */ } } (Also allowing to skip the empty {} is fine with me, but that the last of my concerns.) The other question is trait usage. The RFC proposes just shifting that up to the declaration line. That also seems fine to do either way, regardless of whether there are methods. My biggest concern with this is that it makes methods and short-classes mutually incompatible. So if you have a class that uses short-syntax, and as it evolves you realize it needs one method, sucks to be you, now you have to rewrite basically the whole class to a long-form constructor. That sucks even more than rewriting a short-lambda arrow function to a long-form closure, except without the justification of capture semantics. Additionally: The RFC doesn't specify if or how properties with hooks are supported. That should be defined so we can argue about it. :-) Additionally: What happens here: class Point(int $x, int $y); class 3DPoint(int $z) extends Point; I have to redeclare all of the parameters from the parent, just to add one? That's ugly. ## Inner classes I'm on board with the use case. What I'm not sure on is inner classes vs file-private visibility, something that Ilija was working on at one point and Michał Brzuchalski suggested in his post. Both solve largely the same problem with different spelling. Arguably, inner classes have fewer issues with current autoload conventions. I must ponder this further.
> However, no classes may not inherit from inner classes, but inner classes may inherit from other classes, including the outer class.
I think you have one too many negatives in that sentence. --Larry Garfield

Ilija Tovilo

1 year ago
Hi Rob On Thu, Mar 6, 2025 at 12:14 AM Rob Landers <rob@bottled.codes> wrote:
> > I'd like to introduce my RFC for discussion: https://wiki.php.net/rfc/short-and-inner-classes
Thank you for your proposal. I'm very much against the idea of introducing yet another slightly shorter form to declare a class. In your examples (they have been removed in the meantime), it's unclear how the syntax interacts with inherited constructors, trait-used constructors, whether repetition of readonly parent properties leads to a "Cannot modify readonly property" error, etc. The concept of visibility for classes does seem useful to me. Some questions that crossed my mind when reading the proposal:
> Inner classes may only be nested one level deep, may not be a parent class, and may not be declared abstract
These restrictions seem somewhat arbitrary. For example, you may want a private class to extend another private class, creating some local class hierarchy. I think there's value in relaxing this restriction, if technically possible.
> PHP Fatal error: Private inner class Box::Point cannot be used in the global scope
How is this implemented? I presume using a public nested class as type hints should be allowed, but these classes may not be loaded when the function is declared. We implement delayed variance checks for methods, which do trigger the autoloader, but functions do not (since they cannot violate variance rules).
> Visibility Rules: Private and protected inner classes are only instantiable within their outer class (or subclasses for protected) and cannot be used as type declarations outside their outer class. This encapsulation ensures that the inner class’s implementation details remain within their intended scope.
This introduces a weird case where methods with parameter or return types referring to private classes may not be redeclared in their subclasses, given that the type cannot be specified, even if the methods themselves are not private or final. You do mention something very similar in your e-mail with the __constructor case, but I really fail to see how this provides any benefit. I would also like to echo what has been said about the :: operator, which feels out of place. I understand that \ comes with additional autoloading challenges, namely requiring a fallback autoloading strategy that currently does not conform to PSR-4. Disclaimer: I have not looked at the implementation at all yet. Ilija

Rob Landers

1 year ago
On Thu, Mar 6, 2025, at 23:20, Ilija Tovilo wrote:
> Hi Rob > > On Thu, Mar 6, 2025 at 12:14 AM Rob Landers <rob@bottled.codes> wrote: > > > > I'd like to introduce my RFC for discussion: https://wiki.php.net/rfc/short-and-inner-classes > > Thank you for your proposal. > > I'm very much against the idea of introducing yet another slightly > shorter form to declare a class. In your examples (they have been > removed in the meantime), it's unclear how the syntax interacts with > inherited constructors, trait-used constructors, whether repetition of > readonly parent properties leads to a "Cannot modify readonly > property" error, etc. > > The concept of visibility for classes does seem useful to me. Some > questions that crossed my mind when reading the proposal: > > > Inner classes may only be nested one level deep, may not be a parent class, and may not be declared abstract > > These restrictions seem somewhat arbitrary. For example, you may want > a private class to extend another private class, creating some local > class hierarchy. I think there's value in relaxing this restriction, > if technically possible.
They're not 100% arbitrary, but mostly due to technical limitations. - One level deep: Nesting multiple levels results in ambiguous grammar. - As a parent class: This also results in ambiguity. - Abstract: If it cannot be a parent class, it doesn't make sense for it to be abstract.
> > > PHP Fatal error: Private inner class Box::Point cannot be used in the global scope > > How is this implemented? I presume using a public nested class as type > hints should be allowed, but these classes may not be loaded when the > function is declared. We implement delayed variance checks for > methods, which do trigger the autoloader, but functions do not (since > they cannot violate variance rules).
This happens at run time, when the function is called. I believe we can guarantee that it will pass/fail with one of the following cases: - The type check passes and everything is fine - The type check fails (due to not being the correct type); autoloading never happens - The type check succeeds, but it is private/protected; to have an object of that type, you'd have to have loaded the outer class. This is no different than having: function foo(MadeUpClass $c) {} and then never calling foo.
> > > Visibility Rules: Private and protected inner classes are only instantiable within their outer class (or subclasses for protected) and cannot be used as type declarations outside their outer class. This encapsulation ensures that the inner class’s implementation details remain within their intended scope. > > This introduces a weird case where methods with parameter or return > types referring to private classes may not be redeclared in their > subclasses, given that the type cannot be specified, even if the > methods themselves are not private or final. You do mention something > very similar in your e-mail with the __constructor case, but I really > fail to see how this provides any benefit.
Yes. This is correct and inline with other languages with inner types. The idea behind it is to encapsulate behavior and hide information. The idea is that external code should not know anything about the internal structure. This grants control over inheritance, which you pointed out, though it is better to use final to do that. However, it could be useful if you wanted to create a class meant to be inherited (such as a url parser) but prevent modification to important methods via inheritance.
> > I would also like to echo what has been said about the :: operator, > which feels out of place. I understand that \ comes with additional > autoloading challenges, namely requiring a fallback autoloading > strategy that currently does not conform to PSR-4.
It felt natural to me at the time, but I suspect there may be a better nomenclature; we just need to discover it.
> > Disclaimer: I have not looked at the implementation at all yet. > > Ilija >
— Rob

Juris Evertovskis

1 year ago
On 2025-03-07 01:48, Rob Landers wrote:
> They're not 100% arbitrary, but mostly due to technical limitations. > > - One level deep: Nesting multiple levels results in ambiguous grammar. > - As a parent class: This also results in ambiguity. > - Abstract: If it cannot be a parent class, it doesn't make sense for > it to be abstract.
Hey, I thought that an abstract inner class would be one that a subclass (of the outer class) must implement, not one that must be extended. Similar to abstract methods. But have you considered enums, traits and interfaces? I assume inner classes would work as expected if defined on traits or enums, but what is going to happen if I try to define one on an interface? BR, Juris

Bob Weinand

1 year ago
Hey Ilija, On 6.3.2025 23:20:37, Ilija Tovilo wrote:
> I would also like to echo what has been said about the :: operator, > which feels out of place. I understand that \ comes with additional > autoloading challenges, namely requiring a fallback autoloading > strategy that currently does not conform to PSR-4.
Could you please elaborate on why the :: operator feels out of place? \ is a namespace separator. :: is a class scoping separator. You, yourself did decide to use nested :: for property hook scoping, like parent::$x::set() - a property scoped within a class, having methods. The same applies here - it's a class scoped within a class, having methods. Breaking from these patterns seems very surprising to me. Bob

Ilija Tovilo

1 year ago
Hi Bob On Thu, Mar 13, 2025 at 11:36 PM Bob Weinand <bobwei9@hotmail.com> wrote:
> > On 6.3.2025 23:20:37, Ilija Tovilo wrote: > > > I would also like to echo what has been said about the :: operator, > > which feels out of place. I understand that \ comes with additional > > autoloading challenges, namely requiring a fallback autoloading > > strategy that currently does not conform to PSR-4. > > Could you please elaborate on why the :: operator feels out of place? > > \ is a namespace separator. > > :: is a class scoping separator. > > > You, yourself did decide to use nested :: for property hook scoping, like parent::$x::set() - a property scoped within a class, having methods. > The same applies here - it's a class scoped within a class, having methods. > > Breaking from these patterns seems very surprising to me.
:: is an operation performed on the class. E.g. fetch a static property, fetch a constant, call a static method, while \ is part of the class name. The way I see it, the outer class can simply add an additional namespace component to the inner class. class Foo { class Bar {} // Called Foo\Bar public Bar $bar; } This is dead simple, it doesn't change any assumptions about class names by embedding new symbols, it doesn't need a new operator, you can refer to the class with its short name from inside the outer class without `self:>`, which is shorter and more straight forward, `use Foo\Bar;` will just work in other classes, etc. One thing this approach breaks is that it doesn't allow for polymorphic inner class resolution, i.e. static:>Bar. But given this was removed anyway, that point no longer applies. This can also easily be replicated by a simple method: class Foo { class Bar {} public function createBar(): Bar { return new Bar(); } } Except this is actually type-safe, because the return value of createBar() must stay compatible with Foo\Bar. Ilija

Rob Landers

1 year ago
On Fri, Mar 14, 2025, at 17:09, Ilija Tovilo wrote:
> Hi Bob > > On Thu, Mar 13, 2025 at 11:36 PM Bob Weinand <bobwei9@hotmail.com> wrote: > > > > On 6.3.2025 23:20:37, Ilija Tovilo wrote: > > > > > I would also like to echo what has been said about the :: operator, > > > which feels out of place. I understand that \ comes with additional > > > autoloading challenges, namely requiring a fallback autoloading > > > strategy that currently does not conform to PSR-4. > > > > Could you please elaborate on why the :: operator feels out of place? > > > > \ is a namespace separator. > > > > :: is a class scoping separator. > > > > > > You, yourself did decide to use nested :: for property hook scoping, like parent::$x::set() - a property scoped within a class, having methods. > > The same applies here - it's a class scoped within a class, having methods. > > > > Breaking from these patterns seems very surprising to me. > > :: is an operation performed on the class. E.g. fetch a static > property, fetch a constant, call a static method, while \ is part of > the class name. The way I see it, the outer class can simply add an > additional namespace component to the inner class. > > class Foo { > class Bar {} // Called Foo\Bar > > public Bar $bar; > } > > This is dead simple, it doesn't change any assumptions about class > names by embedding new symbols, it doesn't need a new operator, you > can refer to the class with its short name from inside the outer class > without `self:>`, which is shorter and more straight forward, `use > Foo\Bar;` will just work in other classes, etc. > > One thing this approach breaks is that it doesn't allow for > polymorphic inner class resolution, i.e. static:>Bar. But given this > was removed anyway, that point no longer applies. This can also easily > be replicated by a simple method: > > class Foo { > class Bar {} > > public function createBar(): Bar { > return new Bar(); > } > } > > Except this is actually type-safe, because the return value of > createBar() must stay compatible with Foo\Bar. > > Ilija >
Hi Ilija, What about a hybrid approach? Maybe something like `\\` that Tim suggested? But hear me out. Instead of it being between all inner parts, it is only between the outermost and inner parts of the class, otherwise just use `\`. This also solves a problem where: - we don't need to change anything with autoloading - we can differentiate between different types with the same name So namespace Foo; class Outer { class Middle { class Inner {} } } namespace Foo\Outer; class Middle { } can be differentiated from each other (Foo\Outer\Middle vs. Foo\Outer\\Middle\Inner). I also like the idea of just using the name instead of having Foo:>Bar... I think that is possible now that I have all the machinery in place for visibility. I may have the implementation ready today/tomorrow (as per the currently written RFC). 🤞 — Rob

Rowan Tommins [IMSoP]

1 year ago
On 14/03/2025 17:37, Rob Landers wrote:
> What about a hybrid approach? Maybe something like `\\` that Tim > suggested?
I'm surprised nobody has pointed out yet that \ as namespace separator is already controversial because of how commonly it is used as an escape prefix, leading to a lot of situations where it has to be doubled. A double backslash would be, literally, twice as bad, needing `\\\\` (four backslashes) to reference the name in a string, a JSON or YAML config file, a Markdown tutorial, etc As a perfect example of this, check out how the second paragraph is rendered wrong here: https://externals.io/message/126589#126741 (compare here: https://news-web.php.net/php.internals/126741) The other thing I wonder is whether the original reason why `::` wasn't used as the namespace separator still applies, and needs to be accounted for here?
-- Rowan Tommins [IMSoP]

Bilge

1 year ago
On 14/03/2025 21:08, Rowan Tommins [IMSoP] wrote:
> On 14/03/2025 17:37, Rob Landers wrote: >> What about a hybrid approach? Maybe something like `\\` that Tim >> suggested? > > As a perfect example of this, check out how the second paragraph is > rendered wrong here: https://externals.io/message/126589#126741 > (compare here: https://news-web.php.net/php.internals/126741)
Just because some (incompetent) developers cannot grasp proper escaping should have no bearing whatsoever on language design. Cheers, Bilge

Rowan Tommins [IMSoP]

1 year ago
On 14/03/2025 21:18, Bilge wrote:
> Just because some (incompetent) developers cannot grasp proper > escaping should have no bearing whatsoever on language design.
The main problem is not "grasping" it, it's the inconvenience of having to do it at all, and the "ugliness" (subjective, obviously) of the resulting code. Backslash is used as an escape in so many different syntaxes that it's not uncommon to be nesting two of them inside each other, e.g. Markdown inside JSON: { "markdown": "The inner class is called MyNamespace\\\\MyClass\\\\\\\\InnerClass" } The quadrupled namespace separator is still just about readable, but could you tell me at a glance if I have the right number of backslashes for the proposed inner class separator? If we can't use "::", I'm confident we can find one that's more convenient to use than double-backslash.
-- Rowan Tommins [IMSoP]

Derick Rethans

1 year ago
On Fri, 14 Mar 2025, Bilge wrote:
> On 14/03/2025 21:08, Rowan Tommins [IMSoP] wrote: > > On 14/03/2025 17:37, Rob Landers wrote: > > > What about a hybrid approach? Maybe something like `\\` that Tim > > > suggested? > > > > As a perfect example of this, check out how the second paragraph is > > rendered wrong here: https://externals.io/message/126589#126741 > > (compare here: https://news-web.php.net/php.internals/126741) > > Just because some (incompetent) developers cannot grasp proper > escaping should have no bearing whatsoever on language design.
I'm just catching up with this thread, but ad-hominems are not wanted on this list. Nor is it acceptable to indicate that because one developer might have less time, to call "RIP PHP". Please remember that we're trying to improve the language here. I would urge you to familiarise yourself with the Mailinglist Rules again: https://github.com/php/php-src/blob/master/docs/mailinglist-rules.md cheers, Derick

Bob Weinand

1 year ago
Hey Ilija, On 14.3.2025 17:09:40, Ilija Tovilo wrote:
> Hi Bob > > On Thu, Mar 13, 2025 at 11:36 PM Bob Weinand<bobwei9@hotmail.com> wrote: >> On 6.3.2025 23:20:37, Ilija Tovilo wrote: >> >>> I would also like to echo what has been said about the :: operator, >>> which feels out of place. I understand that \ comes with additional >>> autoloading challenges, namely requiring a fallback autoloading >>> strategy that currently does not conform to PSR-4. >> Could you please elaborate on why the :: operator feels out of place? >> >> \ is a namespace separator. >> >> :: is a class scoping separator. >> >> >> You, yourself did decide to use nested :: for property hook scoping, like parent::$x::set() - a property scoped within a class, having methods. >> The same applies here - it's a class scoped within a class, having methods. >> >> Breaking from these patterns seems very surprising to me. > :: is an operation performed on the class. E.g. fetch a static > property, fetch a constant, call a static method, while \ is part of > the class name. The way I see it, the outer class can simply add an > additional namespace component to the inner class.
I'd consider this a very internals perspective. Yes, internally it will include its separator and the reported name includes the separator. From a user perspective, the class is fetched (an operation!) from its defining class. Similarly, static::Foo (if it is going to be re-introduced) and parent::Foo, are definitely *fetches* of the class. And as such, it should also have the :: operator.
> class Foo { > class Bar {} // Called Foo\Bar > > public Bar $bar; > } > > This is dead simple, it doesn't change any assumptions about class > names by embedding new symbols, it doesn't need a new operator, you > can refer to the class with its short name from inside the outer class > without `self:>`, which is shorter and more straight forward, `use > Foo\Bar;` will just work in other classes, etc.
use Foo\Bar; to reference to an inner-class sounds very much like a bad idea to me. Inner classes are supposed to be intrinsically tied to their containing class, and making it work like a namespace reduces the association a lot. I desire explicitness, which a different symbol can give you. Using namespace-syntax makes the autoloading and human resolution more complex for no gain. Removing the self:: seems enticing, but it breaks at the moment you add inheritance. Then you'll have to either make it a binding-time decision whether it's a namespace access or a parent inner class access. class Foo { class Bar {} } class Baz extends Foo { public Bar $bar; // ??? parent::Bar would be obvious. } Certainly you could opt for only removing self:: in classes they are declared in, but what's then the syntax when referring to an inner class up the inheritance chain? parent\Foo? That's plain weird. Or explicitly requiring the concrete class name the inner class is implemented on? Then this becomes the only weird case which cannot be accessed through the scope resolution. Why would one want that?
> One thing this approach breaks is that it doesn't allow for > polymorphic inner class resolution, i.e. static:>Bar. But given this > was removed anyway, that point no longer applies. This can also easily > be replicated by a simple method: > > class Foo { > class Bar {} > > public function createBar(): Bar { > return new Bar(); > } > } > > Except this is actually type-safe, because the return value of > createBar() must stay compatible with Foo\Bar.
I don't care much about static resolution for this, but self:: and parent:: are relevant. Bob

Rob Landers

1 year ago
On Thu, Mar 6, 2025, at 00:11, Rob Landers wrote:
> Hello PHP Internals, > > I'd like to introduce my RFC for discussion: https://wiki.php.net/rfc/short-and-inner-classes > > This RFC defines a short class syntax as well as the ability to nest classes inside another class. This introduces an unprecedented amount of control, flexibility, and expressiveness over how objects are used and instantiated in PHP. There is a PR (https://github.com/php/php-src/pull/17895) that implements this functionality -- all test failures are related to different/new/incorrect error messages being generated. However, the core functionality exists to take for a test ride. > > So, what do I mean by "unprecedented amount of control"? With this change, you can declare an inner class as private or protected, preventing its usage outside of the outer class: > > class User { > private class Id {} > > public function __construct(public self::Id $id) {} > } > > In the above example, the class `User` is impossible to construct even though it has a public constructor (except through reflection) because User::Id is private; User::Id cannot be instantiated, used as a type hint, or even via `instanceof` outside of the User class itself. This example isn't practical but demonstrates something that is nearly impossible in previous versions of PHP, where all classes are essentially publicly accessible from anywhere within the codebase. > > As a number of inner classes will probably be used as DTOs, the RFC introduces a "short syntax" for declaring classes, which enhances expressiveness, even allowing the usage of traits, all in a single line: > > // declare a readonly Point, that implements Vector2 and uses the Evolvable trait > readonly class Point(public int $x, public int $y) implements Vector2 use Evolvable; > > When combined with inner classes, it looks something like this: > > class Pixel { > public readonly class Point(public int $x, public int $y) implements Vector2 use Evolvable; > } > > // Create a new pixel point with property $x and $y set to 0 > $p = new Pixel::Point(0, 0); > > There are far more details in the RFC itself, so please check it out. I'm quite excited to hear your thoughts! > > — Rob > > PS. I know I tend to rush into things, but I want to make it clear that I'm not rushing this -- I've learned from my mistakes (thank you to those who have given me advice). I'm going to do this right. > >
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

Tim Düsterhus

1 year ago
Hi Am 2025-03-12 11:10, schrieb Rob Landers:
> - Accessing inner classes is done via a new token: ":>" instead of > "::".
I don't particularly like that. It is “invented syntax” and I don't think that inner classes are sufficiently valuable to dedicate an entire operator to them that could serve a more useful purpose in the future. It also got 4 negative points in the rating back when the namespace separator was decided: https://wiki.php.net/rfc/namespaceseparator Would `\\` (i.e. two backslashes) work? The outer class for inner classes effectively act as a namespace, so it makes sense to me to use syntax that is similar to namespaces. I'll look into the rest when there is a new implementation, since I assume some details will still be clarified and fixed as part of implementing the proposal. Best regards Tim Düsterhus

Ilija Tovilo

1 year ago
Hi Rob On Thu, Mar 13, 2025 at 12:01 PM Tim Düsterhus <tim@bastelstu.be> wrote:
> > Am 2025-03-12 11:10, schrieb Rob Landers: > > - Accessing inner classes is done via a new token: ":>" instead of > > "::". > > I don't particularly like that. It is “invented syntax” and I don't > think that inner classes are sufficiently valuable to dedicate an entire > operator to them
I would like to echo these thoughts. I also think the RFC is kind of missing the mark. Arguably the most common use-case for `@internal` is to restrict a class to a specific package/module or namespace, regardless of the user's class hierarchy. I believe you specifically call this a non-goal:
> Currently, many libraries implement “internal” classes by using a naming convention or an @internal annotation in the docblock. Inner classes enable libraries to define an internal class that cannot be used outside the class it is defined inside. This feature is not meant to be used as a “module” system, but rather as a way to encapsulate logic internal to a class, such as DTOs or helper classes.
At least if I understand this statement correctly. In that case, I see much less value in nested classes. They might still be ok if they are extremely simple, but the proposal is currently quite complex. It has a custom operator, it deals with shadowing, LSP, runtime resolution and more, in addition to visibility which is the actual goal. Maybe more use-cases are enabled, but I don't think they are currently well described. Assuming visibility can partially be implemented in a simpler way (e.g. top-level, file-private classes), the costs seem to outweigh the benefits in my opinion. Hence I would not be in favor of the RFC in this or a similar form. Ilija

Rob Landers

1 year ago
On Thu, Mar 13, 2025, at 12:41, Ilija Tovilo wrote:
> Hi Rob > > On Thu, Mar 13, 2025 at 12:01 PM Tim Düsterhus <tim@bastelstu.be> wrote: > > > > Am 2025-03-12 11:10, schrieb Rob Landers: > > > - Accessing inner classes is done via a new token: ":>" instead of > > > "::". > > > > I don't particularly like that. It is “invented syntax” and I don't > > think that inner classes are sufficiently valuable to dedicate an entire > > operator to them > > I would like to echo these thoughts. I also think the RFC is kind of > missing the mark. Arguably the most common use-case for `@internal` is > to restrict a class to a specific package/module or namespace, > regardless of the user's class hierarchy. I believe you specifically > call this a non-goal: > > > Currently, many libraries implement “internal” classes by using a naming convention or an @internal annotation in the docblock. Inner classes enable libraries to define an internal class that cannot be used outside the class it is defined inside. This feature is not meant to be used as a “module” system, but rather as a way to encapsulate logic internal to a class, such as DTOs or helper classes. > > At least if I understand this statement correctly. > > In that case, I see much less value in nested classes. They might > still be ok if they are extremely simple, but the proposal is > currently quite complex. It has a custom operator, it deals with > shadowing, LSP, runtime resolution and more, in addition to visibility > which is the actual goal. Maybe more use-cases are enabled, but I > don't think they are currently well described. Assuming visibility can > partially be implemented in a simpler way (e.g. top-level, > file-private classes), the costs seem to outweigh the benefits in my > opinion. Hence I would not be in favor of the RFC in this or a similar > form. > > Ilija >
Hey Ilija,
> the proposal is > currently quite complex.
Most of this is just describing how classes work already and going in-depth on where there may be confusion -- there are no significant changes to how classes actually work. The actual changes to the engine are basically just visibility rules, some syntax changes (to allow nesting `class` inside another class), and handling the new operator. The hard part is explaining how classes work, because they don't really have a defined behavior. In other words, I cannot just say "the way this works doesn't change anything."
> They might > still be ok if they are extremely simple
And now you can understand why they WERE just simple classes (short classes). So, you can see why I originally bundled them together because of this EXACT argument. :sigh:
> to restrict a class to a specific package/module or namespace, > regardless of the user's class hierarchy. I believe you specifically > call this a non-goal:
I was specifically echoing this callout from Michał earlier in this thread and other emails/comments I have gotten off-list; including my own libraries. I usually have DTOs that I want to use specifically for certain things, but not expose them in the user application -- or even the rest of the library. This isn't a replacement for "modules" or "namespace privacy" but rather, complements it, if we ever implement it. — Rob

Ilija Tovilo

1 year ago
Hi Rob On Thu, Mar 13, 2025 at 1:57 PM Rob Landers <rob@bottled.codes> wrote:
> > > the proposal is > > currently quite complex. > > Most of this is just describing how classes work already and going in-depth on where there may be confusion -- there are no significant changes to how classes actually work. The actual changes to the engine are basically just visibility rules, some syntax changes (to allow nesting `class` inside another class), and handling the new operator. The hard part is explaining how classes work, because they don't really have a defined behavior. In other words, I cannot just say "the way this works doesn't change anything."
Well, you cut out the examples I gave:
> It has a custom operator, it deals with shadowing, LSP, runtime resolution and more, in addition to visibility which is the actual goal. Those are unrelated to existing behavior, they are introduced in the proposal.
* The custom operator and runtime resolution is not something that technically needs to be there. The name of class Foo { class Bar {} } could simply be Foo\Bar. I've mentioned before that this does not work with PSR-4, but there's no reason it can't work with autoloading at all. An extended autoloading spec could recursively search Foo/Bar/Baz.php, then Foo/Bar.php, and so forth. Given this happens only when loading the class, and nesting would usually be limited in quantity and amount, that seems like a reasonable solution, and Composers optimized autoloader could avoid it entirely. This would also solve the same issue for sealed classes, assuming they're named in a similar fashion. * By shadowing I referred to static:>MyClass. This makes it more modular, but it's also another layer of complexity and indirection. You haven't really provided a reasoning and examples of how this could be used. It's also not clear if this can be prevented. The inner class can be marked as final, but that wouldn't stop you from shadowing the inner class without extending it. Similarly, static:>MyClass would have no type guarantees, given that it can be shadowed by any classes, not just classes that are compatible. class Outer { protected class Inner { public static function innerTest() {} } public static function outerTest() { static:>Inner::innerTest(); } } class OuterV2 extends Outer { protected class Inner {} // This breaks outerTest() } * LSP issues have been mentioned before. You can use self:>Inner in method signatures even if the inner classes are private. The method could be called from sub-classes, but they couldn't ever override it, since there's no way to satisfy the parent signature. This makes it implicitly final. Not technically a problem, just odd. As mentioned, maybe there are additional use-cases this complexity can cover, but the RFC doesn't give many examples. If the primary reason for this complexity is just visibility, then I don't think this is the simplest and best way in which that goal could be achieved.
> > They might > > still be ok if they are extremely simple > > And now you can understand why they WERE just simple classes (short classes). So, you can see why I originally bundled them together because of this EXACT argument. :sigh:
The arguments above are not limited to complex classes. Simple classes would apply to. * The :> operator is still something new that I don't believe needs to be there. * Shadowing simple classes can still cause incompatibilities between constructors. * The LSP issue applies. * As soon as we try to add support for complex classes, we'll run into the same questions again. Thinking ahead is always worth it, to prevent us from running into issues later. Doesn't mean everything needs to land at the same time ofc. Ilija

Rob Landers

1 year ago
On Thu, Mar 13, 2025, at 21:41, Ilija Tovilo wrote:
> Hi Rob > > On Thu, Mar 13, 2025 at 1:57 PM Rob Landers <rob@bottled.codes> wrote: > > > > > the proposal is > > > currently quite complex. > > > > Most of this is just describing how classes work already and going in-depth on where there may be confusion -- there are no significant changes to how classes actually work. The actual changes to the engine are basically just visibility rules, some syntax changes (to allow nesting `class` inside another class), and handling the new operator. The hard part is explaining how classes work, because they don't really have a defined behavior. In other words, I cannot just say "the way this works doesn't change anything." > > Well, you cut out the examples I gave:
Ahh, ok. FWIW, I only cut out your examples to shorten the email. But I see what you mean here. I think I can word the RFC in such a way as to make it so I don't have to specify how classes work along with how inner classes work... I'll have to think about it. Maybe I can just explain more about how it actually works? I'm not sure. I'll probably look at the enums RFC to get a better idea of how to do this. For example, I can explain that an inner class is basically a class with a special name and special scopes attached. Then I can simply explain how these special scopes interact with the rest of the language. Then I can get around defining class-like behaviors, and it may even be easier to reason about.
> > It has a custom operator, it deals with shadowing, LSP, runtime resolution and more, in addition to visibility which is the actual goal. Those are unrelated to existing behavior, they are introduced in the proposal. > > * The custom operator and runtime resolution is not something that > technically needs to be there. The name of class Foo { class Bar {} } > could simply be Foo\Bar. I've mentioned before that this does not work > with PSR-4, but there's no reason it can't work with autoloading at > all. An extended autoloading spec could recursively search > Foo/Bar/Baz.php, then Foo/Bar.php, and so forth. Given this happens > only when loading the class, and nesting would usually be limited in > quantity and amount, that seems like a reasonable solution, and > Composers optimized autoloader could avoid it entirely. This would > also solve the same issue for sealed classes, assuming they're named > in a similar fashion.
I disagree completely. If you recall, last year, I tried to pass function autoloading (which would have helped with records) and people really, really, really didn't want to have to change how autoloading worked (and I'm not talking about the list -- I mean projects). So, suggesting a change to autoloading is probably a non-starter. That being said, I don't hate it either.
> > * By shadowing I referred to static:>MyClass. This makes it more > modular, but it's also another layer of complexity and indirection. > You haven't really provided a reasoning and examples of how this could > be used. It's also not clear if this can be prevented. The inner class > can be marked as final, but that wouldn't stop you from shadowing the > inner class without extending it. Similarly, static:>MyClass would > have no type guarantees, given that it can be shadowed by any classes, > not just classes that are compatible. > > class Outer { > protected class Inner { > public static function innerTest() {} > } > > public static function outerTest() { > static:>Inner::innerTest(); > } > } > > class OuterV2 extends Outer { > protected class Inner {} // This breaks outerTest() > }
`static` is expressly forbidden with inner classes after Tim pointed out that it basically breaks LSP. It doesn't, technically, but it makes it hard to reason about for the exact example you gave. One thing worth pointing out here, though, is that in your example you have Outer:>Inner and OuterV2:>Inner. These are two completely distinct classes. They are not related to each other at all.
> > * LSP issues have been mentioned before. You can use self:>Inner in > method signatures even if the inner classes are private. The method > could be called from sub-classes, but they couldn't ever override it, > since there's no way to satisfy the parent signature. This makes it > implicitly final. Not technically a problem, just odd.
This is not the case any more. If you have a private inner class, you cannot use it as a type declaration on a protected/public method, property, or static member. A private class can still be returned and passed around once instantiated, and it can implement an interface/base class (or not). This is very similar to C# or Java. For example, this is ok: interface Shape {} class ShapeFactory { private class Rect implements Shape {} public function makeRect()): Shape { return new ShapeFactory:>Rect(); } } But this is not: class ShapeFactory { private class Rect {} // Private inner class ShapeFactory::Rect cannot be used as a return type for public methods public function makeRect()): ShapeFactory:>Rect { return new ShapeFactory:>Rect(); } }
> As mentioned, maybe there are additional use-cases this complexity can > cover, but the RFC doesn't give many examples. If the primary reason > for this complexity is just visibility, then I don't think this is the > simplest and best way in which that goal could be achieved.
I'll add some more realistic examples to the RFC.
> > > > They might > > > still be ok if they are extremely simple > > > > And now you can understand why they WERE just simple classes (short classes). So, you can see why I originally bundled them together because of this EXACT argument. :sigh: > > The arguments above are not limited to complex classes. Simple classes > would apply to.
> > * The :> operator is still something new that I don't believe needs to be there. > * Shadowing simple classes can still cause incompatibilities between > constructors. > * The LSP issue applies. > * As soon as we try to add support for complex classes, we'll run into > the same questions again. Thinking ahead is always worth it, to > prevent us from running into issues later. Doesn't mean everything > needs to land at the same time ofc. > > Ilija >
— Rob

Rob Landers

1 year ago
On Thu, Mar 13, 2025, at 12:01, Tim Düsterhus wrote:
> Hi > > Am 2025-03-12 11:10, schrieb Rob Landers: > > - Accessing inner classes is done via a new token: ":>" instead of > > "::". > > I don't particularly like that. It is “invented syntax” and I don't > think that inner classes are sufficiently valuable to dedicate an entire > operator to them that could serve a more useful purpose in the future. > It also got 4 negative points in the rating back when the namespace > separator was decided: https://wiki.php.net/rfc/namespaceseparator > > Would `\\` (i.e. two backslashes) work? The outer class for inner > classes effectively act as a namespace, so it makes sense to me to use > syntax that is similar to namespaces. > > I'll look into the rest when there is a new implementation, since I > assume some details will still be clarified and fixed as part of > implementing the proposal. > > Best regards > Tim Düsterhus >
I am not particularly attached to the separator. I specifically chose it due to being a mixture of :: and -> and -: seemed like a bad idea. In other words, an inner class felt natural to use :> -- however, I have some issues with it myself. Particularly, it is too much like |> and as shown in the namespace RFC, way too easy to typo. Personally, after using it for a few days, I'd almost rather go back to :: ... I will give \\ a try, but it has to be typed quite a bit when referencing inner classes, so keeping it easy to type is a must. I feel like \\ requires a large movement to type, at least on a qwerty non-english keyboard. Maybe people using other keyboards can chime in.
> I don't think that inner classes are sufficiently valuable
I'm curious why some people feel this way and why some other people are saying the opposite (emphatically). I'll nudge the private emails I've received to speak up publicly on the list as well. But, why do you feel this way? — Rob

Bob Weinand

1 year ago
Hey Rob, On 13.3.2025 21:46:49, Rob Landers wrote:
> On Thu, Mar 13, 2025, at 12:01, Tim Düsterhus wrote: >> Hi >> >> Am 2025-03-12 11:10, schrieb Rob Landers: >> > - Accessing inner classes is done via a new token: ":>" instead of >> > "::". >> >> I don't particularly like that. It is “invented syntax” and I don't >> think that inner classes are sufficiently valuable to dedicate an entire >> operator to them that could serve a more useful purpose in the future. >> It also got 4 negative points in the rating back when the namespace >> separator was decided: https://wiki.php.net/rfc/namespaceseparator >> >> Would `\\` (i.e. two backslashes) work? The outer class for inner >> classes effectively act as a namespace, so it makes sense to me to use >> syntax that is similar to namespaces. >> >> I'll look into the rest when there is a new implementation, since I >> assume some details will still be clarified and fixed as part of >> implementing the proposal. >> >> Best regards >> Tim Düsterhus >> > > I am not particularly attached to the separator. I specifically chose > it due to being a mixture of :: and -> and -: seemed like a bad idea. > In other words, an inner class felt natural to use :> -- however, I > have some issues with it myself. Particularly, it is too much like |> > and as shown in the namespace RFC, way too easy to typo. Personally, > after using it for a few days, I'd almost rather go back to :: ... > > I will give \\ a try, but it has to be typed quite a bit when > referencing inner classes, so keeping it easy to type is a must. I > feel like \\ requires a large movement to type, at least on a qwerty > non-english keyboard. Maybe people using other keyboards can chime in.
Please go back to ::. The double colon was perfectly fine, the only thing which was weird was the implicit constant. It's fine for constants and inner classes to share the same name scoping (i.e. a constant must not share a name with an inner class). But an inner class should not be an actual constant. But otherwise, this was perfectly fine.
>> I don't think that inner classes are sufficiently valuable > > I'm curious why some people feel this way and why some other people > are saying the opposite (emphatically). I'll nudge the private emails > I've received to speak up publicly on the list as well. But, why do > you feel this way?
I don't know why some people feel this way, but at least with the autoloading mechanisms we have in PHP there are definite limitations to multiple classes in one file: - If you deserialize your data structure and it contains another class, whose name does not match the file name, you better hope to god that it has been autoloaded already. Surprising failures in production follow. (e.g.: the amphp\parallel process runner will try to serialize your exception. That's fine. But as soon as it's accidentally bubbling up and as it's not autoloadable, the hell breaks loose.) - Enums or Data Transfer objects specific to one or multiple functions of only this specific class would naturally fit into the same file. But you can't do it ... because the autoloading might try to load the enum first, before the class whose constructor or function you want to call is actually loaded. - Now, if you actually stuff multiple classes / enums into a single file, it's non-trivial to figure out (as the human reader, but obviously for the autoloader too) which file to access to read up the definition of a specific enum (short of using dedicated tooling for it). Certainly, one may say - yeah, just religiously use different files for ... every ... single ... class. But what's the point of that dogma? It definitely doesn't help the organization of dedicated helper structures tied to a single class. It's a tool for organization. It's not complex to understand. Outside of very puristic arguments I don't see any reason why one would not want inner classes. Further, with short classes, should they hopefully be introduced as well, it will become trivial to write shapes for any internal datastructures - simply using a "class Point(int $x, int $y); private Point $pos;" rather than "/** @var list{int, int} */ private array $pos;". This will be a very ergonomic way to reduce ad-hoc arrays which are only typehinted by phpdoc, giving proper typing and also safe access to structures via named accessors rather than [0] and [1] etc. Further Questions on the (original) RFC: - Why is there a conflict in static properties and the inner class name? The former always has a leading $ sigil. - Any particular reason to disallow abstract inner classes? Feels arbitrary. And a note: I consider the inheritance example misguided as "static constructors" (static methods which invoke new()) would be a better pattern here. Could you maybe come up with another example here? Bob

Rob Landers

1 year ago
On Thu, Mar 13, 2025, at 23:26, Bob Weinand wrote:
> Hey Rob, > > On 13.3.2025 21:46:49, Rob Landers wrote: >> On Thu, Mar 13, 2025, at 12:01, Tim Düsterhus wrote: >>> Hi >>> >>> Am 2025-03-12 11:10, schrieb Rob Landers: >>> > - Accessing inner classes is done via a new token: ":>" instead of >>> > "::". >>> >>> I don't particularly like that. It is “invented syntax” and I don't >>> think that inner classes are sufficiently valuable to dedicate an entire >>> operator to them that could serve a more useful purpose in the future. >>> It also got 4 negative points in the rating back when the namespace >>> separator was decided: https://wiki.php.net/rfc/namespaceseparator >>> >>> Would `\\` (i.e. two backslashes) work? The outer class for inner >>> classes effectively act as a namespace, so it makes sense to me to use >>> syntax that is similar to namespaces. >>> >>> I'll look into the rest when there is a new implementation, since I >>> assume some details will still be clarified and fixed as part of >>> implementing the proposal. >>> >>> Best regards >>> Tim Düsterhus >>> >> >> I am not particularly attached to the separator. I specifically chose it due to being a mixture of :: and -> and -: seemed like a bad idea. In other words, an inner class felt natural to use :> -- however, I have some issues with it myself. Particularly, it is too much like |> and as shown in the namespace RFC, way too easy to typo. Personally, after using it for a few days, I'd almost rather go back to :: ... >> >> I will give \\ a try, but it has to be typed quite a bit when referencing inner classes, so keeping it easy to type is a must. I feel like \\ requires a large movement to type, at least on a qwerty non-english keyboard. Maybe people using other keyboards can chime in. > Please go back to ::. The double colon was perfectly fine, the only thing which was weird was the implicit constant. It's fine for constants and inner classes to share the same name scoping (i.e. a constant must not share a name with an inner class). But an inner class should not be an actual constant. > > But otherwise, this was perfectly fine. >
My biggest issue with `::` is that it gets weird: class Foo { public class Bar {} public const Bar = ""; public static function Bar() {} } echo Foo::Bar; // this is the constant new Foo::Bar(); // this is the class Foo::Bar(); // this is the method new (Foo::Bar()); // this is the method new (Foo::Bar); // this is constant I can now differentiate between these all in the AST, but it seems weird to me. If we go this route, I'd personally have the preference to allow them all and let people's code-style dictate what is acceptable or not -- assuming I can ensure there is no ambiguity in the grammar. At least with `:>` (or something else) we don't have to even have that discussion. :)
>>> I don't think that inner classes are sufficiently valuable >> >> I'm curious why some people feel this way and why some other people are saying the opposite (emphatically). I'll nudge the private emails I've received to speak up publicly on the list as well. But, why do you feel this way? > > I don't know why some people feel this way, but at least with the autoloading mechanisms we have in PHP there are definite limitations to multiple classes in one file: > - If you deserialize your data structure and it contains another class, whose name does not match the file name, you better hope to god that it has been autoloaded already. Surprising failures in production follow. (e.g.: the amphp\parallel process runner will try to serialize your exception. That's fine. But as soon as it's accidentally bubbling up and as it's not autoloadable, the hell breaks loose.) > - Enums or Data Transfer objects specific to one or multiple functions of only this specific class would naturally fit into the same file. But you can't do it ... because the autoloading might try to load the enum first, before the class whose constructor or function you want to call is actually loaded. > - Now, if you actually stuff multiple classes / enums into a single file, it's non-trivial to figure out (as the human reader, but obviously for the autoloader too) which file to access to read up the definition of a specific enum (short of using dedicated tooling for it). > > > Certainly, one may say - yeah, just religiously use different files for ... every ... single ... class. > But what's the point of that dogma? > It definitely doesn't help the organization of dedicated helper structures tied to a single class. > It's a tool for organization. It's not complex to understand. > > Outside of very puristic arguments I don't see any reason why one would not want inner classes. > > > Further, with short classes, should they hopefully be introduced as well, it will become trivial to write shapes for any internal datastructures - simply using a "class Point(int $x, int $y); private Point $pos;" rather than "/** @var list{int, int} */ private array $pos;". > This will be a very ergonomic way to reduce ad-hoc arrays which are only typehinted by phpdoc, giving proper typing and also safe access to structures via named accessors rather than [0] and [1] etc. > > Further Questions on the (original) RFC: > > - Why is there a conflict in static properties and the inner class name? The former always has a leading $ sigil. >
Funny enough, this was due to me messing up the AST such that it was trying to access a property instead of a constant. Once I fixed that, the conflict went away.
> - Any particular reason to disallow abstract inner classes? Feels arbitrary. >
This went away as well, once I started cleaning up the grammar. Basically, I couldn't work out how to allow `::` in `extends`. I eventually figured it out once I implemented `:>`. Replacing `:>` with virtually anything else is quite possible -- including going back to `::` (I think).
> And a note: I consider the inheritance example misguided as "static constructors" (static methods which invoke new()) would be a better pattern here. Could you maybe come up with another example here? > > > > Bob >
That was a pretty bad example! It illustrated the point but would probably be a bad practice. — Rob

Bob Weinand

1 year ago
Hey Rob, On 14.3.2025 00:26:03, Rob Landers wrote:
> My biggest issue with `::` is that it gets weird: > > class Foo { >   public class Bar {} >   public const Bar = ""; >   public static function Bar() {} > } > > echo Foo::Bar; // this is the constant > new Foo::Bar(); // this is the class > Foo::Bar(); // this is the method > new (Foo::Bar()); // this is the method > new (Foo::Bar); // this is constant > > I can now differentiate between these all in the AST, but it seems > weird to me. If we go this route, I'd personally have the preference > to allow them all and let people's code-style dictate what is > acceptable or not -- assuming I can ensure there is no ambiguity in > the grammar. At least with `:>` (or something else) we don't have to > even have that discussion. :)
Why would that be weird? In 99% of the cases new followed by something followed by double colons followed by something else is just the inner class. Writing new (Foo::Bar) or new (Foo::Bar()) already today looks suspicious and will remain looking suspicious. Nothing will change about that. And "Foo::Bar" (or "Foo::Bar()") without being preceded by "new" or followed by "::" is just the normal class constant. On top of that, there are naming conventions in PHP which will make it even more obvious: class constants in uppercase and methods in camelCase and class names in PascalCase. So, just looking at Foo::Bar (without considering any surrounding tokens), you expect a class name. Looking at Foo::BAR, you expect a constant. Looking at Foo::bar you expect a method. echo Foo::BAR; // this is the constant new Foo::Bar(); // this is the class Foo::bar(); // this is the method new (Foo::bar()); // this is the method new (Foo::BAR); // this is constant // or alternatively: $myFantasticInterface = Foo::bar(); new $myFantasticInterface; $myAwesomeInterface = Foo::BAR; new $myAwesomeInterface; There's no real surprises at a glance as long as you don't intentionally make your code obscure. Bob

Tim Düsterhus

1 year ago
Hi Am 2025-03-14 01:22, schrieb Bob Weinand:
> […] class constants in uppercase […]
enum cases are a notable Exception. They also use PascalCase (both internal enums and the PER-CS coding style as published by PHP-FIG). But that's also a good question for the RFC author: Is defining inner classes within an enum legal? The RFC says that inner enums are future scope, but what about “outer” enums? The specification in the “Usage” section is not entirely clear to me. Best regards Tim Düsterhus

Tim Düsterhus

1 year ago
Hi Am 2025-03-13 21:46, schrieb Rob Landers:
> I will give \\ a try, but it has to be typed quite a bit when > referencing inner classes, so keeping it easy to type is a must. I feel > like \\ requires a large movement to type, at least on a qwerty > non-english keyboard. Maybe people using other keyboards can chime in.
I'm using a German keyboard where the backslash is indeed not particularly convenient to type, but that already applies to regular classes / namespaces. Once I reach out to type the backslash, the number of backslashes I type doesn't particularly matter, so I don't follow how typing would be any more complicated than regular class names. The double backslash has one notable drawback in strings, where it would blow up to four backslashes in a row, but with the `::class` syntax and first class callables, putting class-names in strings should hopefully be rare going forward.
>> I don't think that inner classes are sufficiently valuable > > I'm curious why some people feel this way and why some other people are > saying the opposite (emphatically). I'll nudge the private emails I've > received to speak up publicly on the list as well. But, why do you feel > this way?
You cut the context from the part you quoted, so I'm not sure how to answer to that question, since it seems to be based on a false premise. Best regards Tim Düsterhus

Rob Landers

1 year ago
On Wed, Mar 12, 2025, at 11:10, Rob Landers wrote:
> On Thu, Mar 6, 2025, at 00:11, Rob Landers wrote: >> Hello PHP Internals, >> >> I'd like to introduce my RFC for discussion: https://wiki.php.net/rfc/short-and-inner-classes >> >> This RFC defines a short class syntax as well as the ability to nest classes inside another class. This introduces an unprecedented amount of control, flexibility, and expressiveness over how objects are used and instantiated in PHP. There is a PR (https://github.com/php/php-src/pull/17895) that implements this functionality -- all test failures are related to different/new/incorrect error messages being generated. However, the core functionality exists to take for a test ride. >> >> So, what do I mean by "unprecedented amount of control"? With this change, you can declare an inner class as private or protected, preventing its usage outside of the outer class: >> >> class User { >> private class Id {} >> >> public function __construct(public self::Id $id) {} >> } >> >> In the above example, the class `User` is impossible to construct even though it has a public constructor (except through reflection) because User::Id is private; User::Id cannot be instantiated, used as a type hint, or even via `instanceof` outside of the User class itself. This example isn't practical but demonstrates something that is nearly impossible in previous versions of PHP, where all classes are essentially publicly accessible from anywhere within the codebase. >> >> As a number of inner classes will probably be used as DTOs, the RFC introduces a "short syntax" for declaring classes, which enhances expressiveness, even allowing the usage of traits, all in a single line: >> >> // declare a readonly Point, that implements Vector2 and uses the Evolvable trait >> readonly class Point(public int $x, public int $y) implements Vector2 use Evolvable; >> >> When combined with inner classes, it looks something like this: >> >> class Pixel { >> public readonly class Point(public int $x, public int $y) implements Vector2 use Evolvable; >> } >> >> // Create a new pixel point with property $x and $y set to 0 >> $p = new Pixel::Point(0, 0); >> >> There are far more details in the RFC itself, so please check it out. I'm quite excited to hear your thoughts! >> >> — Rob >> >> PS. I know I tend to rush into things, but I want to make it clear that I'm not rushing this -- I've learned from my mistakes (thank you to those who have given me advice). I'm going to do this right. >> >> > > 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
For those who are interested, I've opened the PR that enables this feature: https://github.com/php/php-src/pull/18069 — Rob

Larry Garfield

1 year ago
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

Alexandru Pătrănescu

1 year ago
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

Rowan Tommins [IMSoP]

1 year ago
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]

Bob Weinand

1 year ago
Hey Rob, On 6.3.2025 00:11:22, Rob Landers wrote:
> Hello PHP Internals, > > I'd like to introduce my RFC for discussion: > https://wiki.php.net/rfc/short-and-inner-classes
A small note on the Reflection section: it should include a method giving you an array of all contained ReflectionClasses: $reflection->getInnerClasses() And likely also a method to get an inner class by name $reflection->getInnerClass("Foo"). Bob

Rob Landers

1 year ago
On Fri, Mar 14, 2025, at 19:16, Bob Weinand wrote:
> Hey Rob, > > On 6.3.2025 00:11:22, Rob Landers wrote: >> Hello PHP Internals, >> >> I'd like to introduce my RFC for discussion: https://wiki.php.net/rfc/short-and-inner-classes > A small note on the Reflection section: it should include a method giving you an array of all contained ReflectionClasses: $reflection->getInnerClasses() > > And likely also a method to get an inner class by name $reflection->getInnerClass("Foo"). > > > > Bob >
Classes don't actually know their inner classes -- they aren't like properties. In essence, an inner class is just a regular class with a funny name and access to scopes it wouldn't normally have access to. We could probably add `getOuterClass(): string` if that is useful. It is possible to keep track of a class's inner classes, but then that introduces a paradox chicken/egg type problem during construction, which may or may not be a problem. On Fri, Mar 14, 2025, at 22:08, Rowan Tommins [IMSoP] wrote:
> The other thing I wonder is whether the original reason why `::` wasn't > used as the namespace separator still applies, and needs to be accounted > for here? > > -- > Rowan Tommins > [IMSoP] >
I'm replying to you Rowan, but also to the whole conversation on the topic of separating inner classes. I was going to add this example to the RFC: https://gist.github.com/withinboredom/c007e35d3b042b87b02ac550db589a0c, but I decided to get food poisoning for the last few hours instead. :joy: Where using it looks like this: $user = new User:>Builder("Rob")->withEmail("rob@bottled.codes")->build(); The user builder is intrinsically tied to the User class itself, it isn't just a namespace. The user builder shares scope with the user class and is able to be the only way to construct a user (barring reflection). So, I'm inclined to agree with Bob here:
> Inner classes are supposed to be intrinsically tied to their containing class, and making it work like a namespace reduces the association a lot.
Furthermore, I'm relatively certain this approach can be slightly modified to support "namespace private/protected" classes, in general. So, that will also possibly be a follow-up RFC and having them mixed up will complicate things. In any case, I am not a fan of using the namespace separator here. I could get behind `::`, but I feel that it introduces human ambiguity. I don't believe it would introduce compiler ambiguity, but as a human, I have to hope the programmers are using a style that makes it obvious what are inner classes and what are constants/methods. I don't know, I keep coming back to `:>` ... it isn't perfect, and it is somewhat annoying to type, but I don't have anything better. — Rob

Rowan Tommins [IMSoP]

1 year ago
On 14 March 2025 23:37:08 GMT, Rob Landers <rob@bottled.codes> wrote:
>I could get behind `::`, but I feel that it introduces human ambiguity. I don't believe it would introduce compiler ambiguity, but as a human, I have to hope the programmers are using a style that makes it obvious what are inner classes and what are constants/methods.
As far as I can see, all four languages I looked up last night (Java, C#, Swift, Kotlin) use the same syntax for accessing a nested type as for accessing a property or method, so we'd be following the crowd to use "::" That said, I think they all also use that same syntax for namespace (or equivalent) lookups, so the same argument can be made for "\". (Why PHP separates those isn't entirely clear to me.) Having some new syntax makes them feel more "exotic" to me. The C# and Swift docs give the impression that nesting types is "just something that can happen", rather than a whole new language concept to learn. Java's "inner classes" are something different, and I think we should avoid using that term.
> Furthermore, I'm relatively certain this approach can be slightly modified to support "namespace private/protected" classes, in general. So, that will also possibly be a follow-up RFC and having them mixed up will complicate things. In any case, I am not a fan of using the namespace separator here.
To me namespace, module, or file visibility seem like much more natural additions to the language, and to solve most of the same problems as nesting types. I guess a public nested class is a bit like a "friend class"? In that it has special access to its surrounding class, but otherwise might as well just be sharing a namespace with it. But it's also necessarily in the same file, so marking those members "file private" rather than "private" would also allow that. A private nested class can only be explicitly mentioned within that narrow context, which again feels very much equivalent to "file private".
> $user = new User:>Builder("Rob")->withEmail("rob@bottled.codes")->build(); > > The user builder is intrinsically tied to the User class itself, it isn't just a namespace. The user builder shares scope with the user class and is able to be the only way to construct a user (barring reflection).
If we had either file or namespace visibility, exactly the same thing could be achieved with those, and would look like this: $user = new User\Builder("Rob")->withEmail("rob@bottled.codes")->build(); The "User" class would have a "file private" or "namespace private" constructor, callable from the "User\Builder" class but not elsewhere; the build() method would return the "User" instance. I think I'm coming to the conclusion that we should use backslash: nested types can be viewed as a shorthand way of having a class and namespace with the same name, plus applying some visibility rules to that namespace. Rowan Tommins [IMSoP]

Claude Pache

1 year ago
> Le 15 mars 2025 à 12:53, Rowan Tommins [IMSoP] <imsop.php@rwec.co.uk> a écrit : > > > > On 14 March 2025 23:37:08 GMT, Rob Landers <rob@bottled.codes> wrote: >> I could get behind `::`, but I feel that it introduces human ambiguity. I don't believe it would introduce compiler ambiguity, but as a human, I have to hope the programmers are using a style that makes it obvious what are inner classes and what are constants/methods. > > As far as I can see, all four languages I looked up last night (Java, C#, Swift, Kotlin) use the same syntax for accessing a nested type as for accessing a property or method, so we'd be following the crowd to use "::" > > That said, I think they all also use that same syntax for namespace (or equivalent) lookups, so the same argument can be made for "\". (Why PHP separates those isn't entirely clear to me.) >
According to my archeological research, it was originally designed to reuse `::` as namespace separator, but it was finally changed to something else due to ambiguity between static class elements and namespaced functions/constants. See https://wiki.php.net/rfc/namespaceissues and https://wiki.php.net/rfc/backslashnamespaces (where `::` is assumed to be the namespace separator). —Claude

Iliya Miroslavov Iliev

1 year ago
Claude, exactly! By using '::' you cannot distinguish between a class and a function. So this is not an option because it leads to a headache. Just my 2 cents On Sun, Mar 16, 2025 at 11:08 PM Claude Pache <claude.pache@gmail.com> wrote:
> > > Le 15 mars 2025 à 12:53, Rowan Tommins [IMSoP] <imsop.php@rwec.co.uk> a > écrit : > > > > On 14 March 2025 23:37:08 GMT, Rob Landers <rob@bottled.codes> wrote: > > I could get behind `::`, but I feel that it introduces human ambiguity. I > don't believe it would introduce compiler ambiguity, but as a human, I have > to hope the programmers are using a style that makes it obvious what are > inner classes and what are constants/methods. > > > As far as I can see, all four languages I looked up last night (Java, C#, > Swift, Kotlin) use the same syntax for accessing a nested type as for > accessing a property or method, so we'd be following the crowd to use "::" > > That said, I think they all also use that same syntax for namespace (or > equivalent) lookups, so the same argument can be made for "\". (Why PHP > separates those isn't entirely clear to me.) > > > According to my archeological research, it was originally designed to > reuse `::` as namespace separator, but it was finally changed to something > else due to ambiguity between static class elements and namespaced > functions/constants. See https://wiki.php.net/rfc/namespaceissues and > https://wiki.php.net/rfc/backslashnamespaces (where `::` is assumed to be > the namespace separator). > > —Claude >
-- Iliya Miroslavov Iliev i.miroslavov@gmail.com

Rob Landers

1 year ago
On Sat, Mar 15, 2025, at 12:53, Rowan Tommins [IMSoP] wrote:
> > > On 14 March 2025 23:37:08 GMT, Rob Landers <rob@bottled.codes> wrote: > >I could get behind `::`, but I feel that it introduces human ambiguity. I don't believe it would introduce compiler ambiguity, but as a human, I have to hope the programmers are using a style that makes it obvious what are inner classes and what are constants/methods. > > As far as I can see, all four languages I looked up last night (Java, C#, Swift, Kotlin) use the same syntax for accessing a nested type as for accessing a property or method, so we'd be following the crowd to use "::" > > That said, I think they all also use that same syntax for namespace (or equivalent) lookups, so the same argument can be made for "\". (Why PHP separates those isn't entirely clear to me.) > > Having some new syntax makes them feel more "exotic" to me. The C# and Swift docs give the impression that nesting types is "just something that can happen", rather than a whole new language concept to learn. > > Java's "inner classes" are something different, and I think we should avoid using that term. > > > > > Furthermore, I'm relatively certain this approach can be slightly modified to support "namespace private/protected" classes, in general. So, that will also possibly be a follow-up RFC and having them mixed up will complicate things. In any case, I am not a fan of using the namespace separator here. > > To me namespace, module, or file visibility seem like much more natural additions to the language, and to solve most of the same problems as nesting types. > > I guess a public nested class is a bit like a "friend class"? In that it has special access to its surrounding class, but otherwise might as well just be sharing a namespace with it. But it's also necessarily in the same file, so marking those members "file private" rather than "private" would also allow that. > > A private nested class can only be explicitly mentioned within that narrow context, which again feels very much equivalent to "file private". > > > > $user = new User:>Builder("Rob")->withEmail("rob@bottled.codes")->build(); > > > > The user builder is intrinsically tied to the User class itself, it isn't just a namespace. The user builder shares scope with the user class and is able to be the only way to construct a user (barring reflection). > > If we had either file or namespace visibility, exactly the same thing could be achieved with those, and would look like this: > > $user = new User\Builder("Rob")->withEmail("rob@bottled.codes")->build(); > > The "User" class would have a "file private" or "namespace private" constructor, callable from the "User\Builder" class but not elsewhere; the build() method would return the "User" instance. > > > > I think I'm coming to the conclusion that we should use backslash: nested types can be viewed as a shorthand way of having a class and namespace with the same name, plus applying some visibility rules to that namespace. > > Rowan Tommins > [IMSoP] >
I don’t think `\` is viable. If we used slash it would complicate autoloading and be impossible to differentiate between namespace Outer; class Inner {} And class Outer { class Inner {} } These would both resolve to the same class name for Outer\Inner. Which one it resolves to would depend on how you implement autoloading (which may be desirable for BC reasons). Then there becomes the question of either letting user-land implement the autoloading changes, or have php walk “up” the namespace chain in the hopes it implements an inner class. So, maybe, it could be useful to use `\` but in the long run, I’m not sure it makes sense. — Rob

Rowan Tommins [IMSoP]

1 year ago
On 17 March 2025 07:11:23 GMT, Rob Landers <rob@bottled.codes> wrote:
> >namespace Outer; > >class Inner {} > >And > >class Outer { > class Inner {} >} > >These would both resolve to the same class name for Outer\Inner.
That's the frame challenge: is that actually a good thing, because it makes the description of nested types a lot simpler?
> Which one it resolves to would depend on how you implement autoloading
That's just the same as having the same class defined in two files on disk - PHP doesn't know which will be used until an autoloader runs. From what I understand of the proposal, the calling code won't know anything different based on it being "nested" or "namespaced", it will just see a class with a long name with some punctuation in.
> Then there becomes the question of either letting user-land implement the autoloading changes, or have php walk “up” the namespace chain in the hopes it implements an inner class.
I think it should be left to the implementation - PHP makes no assumption about one class per file anyway, so am implementation could already do this with a namespace{} block. It is a reasonable point that the convention for when an autoloader should walk up, and how far, would need to be defined. Then again, I'm increasingly convinced autoloading is a dead-end for the language, and hope to see more enhancements to preloading and a module system instead.
> So, maybe, it could be useful to use \ but in the long run, I’m not sure it makes sense.
I rather think the other way round: in the short term, a new separator would save users a bit of pain with autoloading, but in the long run it will look like a weird anomaly that no other language needs. Rowan Tommins [IMSoP]

Alexandru Pătrănescu

1 year ago
On Mon, Ma0r 17, 2025 at 9:56 AM Rowan Tommins [IMSoP] <imsop.php@rwec.co.uk> wrote:
> > > On 17 March 2025 07:11:23 GMT, Rob Landers <rob@bottled.codes> wrote: > > > > Which one it resolves to would depend on how you implement autoloading > > > That's just the same as having the same class defined in two files on disk > - PHP doesn't know which will be used until an autoloader runs. > > From what I understand of the proposal, the calling code won't know > anything different based on it being "nested" or "namespaced", it will just > see a class with a long name with some punctuation in. > >
The problem for me is not autoloading, but that you can't have the two classes defined at the same time, while using some other punctuation it would allow it. I believe that there are other operators that we can use, to allow this.
> I rather think the other way round: in the short term, a new separator > would save users a bit of pain with autoloading, but in the long run it > will look like a weird anomaly that no other language needs. > >
There was much discussion about various ways to solve the problem stated in the RFC, but I think we need to look at it as a broader picture again. I'll share what I've been thinking about in the past week and didn't yet had time to reply here. Right now, PHP only has public classes. For better encapsulation and cohesion, we want to define some new ways to define classes with some limited restrictions or improved access. Taking examples from other languages, we can have two ways to do this: - namespace private classes, or file-private classes - this controls the visibility of the class at the namespace or file level. They’re essentially about managing the scope of classes without changing their fundamental behavior. - nested (inner) classes - this allows a class defined inside another to access private members of its enclosing class, increasing cohesion. And we can have further here just public visibility, or protected/private visibility, for better encapsulation. Given this, I think we could even implement all of these in separate RFCs: - public nested classes - file-private classes - namespace private classes - protected/private nested classes - other-grouping-level-that-will-exist-in-the-future private classes Of all of this, the first, public nested classes are the most simple and would drive cohesion. And maybe we can have only that for now. The other features would be mostly important for encapsulation, with limiting the class visibility. We just need to agree on a separator/operator, and IMHO, it should be clear and distinct from namespace separator, as nested classes are a separate concept. Maybe having a vote on the operator would be enough. (As a note, and this might have been discussed already, but I would prefer to use the term nested class instead of inner class, as in java the inner classes means non-static classes, and I don't think we should go that way.)
-- Alex

Rowan Tommins [IMSoP]

1 year ago
On 17/03/2025 10:37, Alexandru Pătrănescu wrote:
> > From what I understand of the proposal, the calling code won't > know anything different based on it being "nested" or > "namespaced", it will just see a class with a long name with some > punctuation in. > > > The problem for me is not autoloading, but that you can't have the two > classes defined at the same time, while using some other punctuation > it would allow it. > I believe that there are other operators that we can use, to allow this.
Again, I challenge the premise: why would you want to have both defined at the same time? Do developers in Java, C#, Kotlin, or Swift complain about this limitation? Is there any other language which makes a syntax distinction between "class Foo in namespace Bar" and "class Foo nested in class Bar"?
> (As a note, and this might have been discussed already, but I would > prefer to use the term nested class instead of inner class, as in java > the inner classes means non-static classes, and I don't think we > should go that way.)
100% agree. If we did implement something more like "inner classes", a special syntax might make more sense - there would be a very specific relationship between the inner and outer classes. I don't think "has special visibility of members, like a friend-class or file-private feature" needs to be highlighted in the syntax that way.
-- Rowan Tommins [IMSoP]

Rob Landers

1 year ago
On Mon, Mar 17, 2025, at 12:29, Rowan Tommins [IMSoP] wrote:
> On 17/03/2025 10:37, Alexandru Pătrănescu wrote: >>> From what I understand of the proposal, the calling code won't know anything different based on it being "nested" or "namespaced", it will just see a class with a long name with some punctuation in. >> >> The problem for me is not autoloading, but that you can't have the two classes defined at the same time, while using some other punctuation it would allow it. >> I believe that there are other operators that we can use, to allow this. > > > Again, I challenge the premise: why would you want to have both defined at the same time? > > Do developers in Java, C#, Kotlin, or Swift complain about this limitation? > > Is there any other language which makes a syntax distinction between "class Foo in namespace Bar" and "class Foo nested in class Bar"? >
No, almost no other language I know of seems to make a distinction like PHP does. It appears that PHP is unique in this. Does that mean we can learn more from these other languages, or less? I’d be more than happy to put forward a short RFC or poll on the syntax choices, as Alexander suggested. Even if this RFC fails, it would be good to get a consensus for the future.
>> (As a note, and this might have been discussed already, but I would prefer to use the term nested class instead of inner class, as in java the inner classes means non-static classes, and I don't think we should go that way.) > 100% agree. > > If we did implement something more like "inner classes", a special syntax might make more sense - there would be a very specific relationship between the inner and outer classes. I don't think "has special visibility of members, like a friend-class or file-private feature" needs to be highlighted in that way. >
I am inclined to agree with you both on this. Originally I had planned for an “outer” and “$outer” that made it more like Java inner classes, but decided to leave it to a future RFC if someone ever wanted to implement it. When I first started thinking about this RFC, static classes had fairly recently failed, and I didn’t want to define what a “static inner class” was. This is part of the reason why “static” isn’t allowed as an inner class modifier. This may be a potential future RFC if people have need of it. — Rob

Rowan Tommins [IMSoP]

1 year ago
On 05/03/2025 23:11, Rob Landers wrote:
> > I'd like to introduce my RFC for discussion: > https://wiki.php.net/rfc/short-and-inner-classes
As a user, I find the concept of inner classes quite confusing. However, I was looking at some code earlier and thought an "inner enum" would be useful, to replace various "MODE_FOO" type class constants - but I see those are left to future scope. :( I'm aware they exist in a lot of other languages, though, so I thought I'd look around at how the proposed version compares. It seems there's quite a zoo out there... One common theme I do note is that all four of the pages I found are titled "nested classes" or "nested types"; in some cases, "inner class"/"inner type" has a specific meaning, which I don't think matches the RFC's proposal. C# - https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/nested-types I think this is most similar to what you're proposing. Interestingly, the nested class defaults to being private, i.e. only usable within the parent class. Classes, structs and interfaces can all be nested inside each other. Java - https://docs.oracle.com/javase/tutorial/java/javaOO/nested.html There are two types of nested class (plus some more related concepts which seem less relevant) - "Static nested classes" - static in the sense that they act like a static member of the surrounding class; appear to be *only* about namespacing and visibility of the class, with no special access to the surrounding scope - "Inner classes" - associated not just with a parent class, but a parent *instance*, so have to be created with the syntax "ParentClassName.new InnerClassName" or "this.new InnerClassName"; are not allowed to have static members Kotlin - https://kotlinlang.org/docs/nested-classes.html Just nesting a class gives it no special access, but the keyword "inner" causes it to carry an *instance* of the outer class, which can be explicitly referenced with the syntax "this@OuterClassName". Classes and interfaces can be nested in any combination. Swift - https://docs.swift.org/swift-book/documentation/the-swift-programming-language/nestedtypes Type declarations of various sorts can be nested, and have all the visibility modifiers available to other declarations. "Private access restricts the use of an entity to the enclosing declaration, and to extensions of that declaration that are in the same file" - https://docs.swift.org/swift-book/documentation/the-swift-programming-language/accesscontrol/ The example of a nested enum also demonstrates a nice shorthand syntax, where the ".ace" in "BlackjackCard(rank: .ace, suit: .spades)" is short for BlackjackCard.Rank.ace, inferred from the parameter type. I don't have any specific conclusions, but I think with features like this it's always worth examining other people's ideas, to see if we want to include (or avoid) any of them.
-- Rowan Tommins [IMSoP]

Bob Weinand

1 year ago
On 15.3.2025 00:21:32, Rowan Tommins [IMSoP] wrote:
> On 05/03/2025 23:11, Rob Landers wrote: >> >> I'd like to introduce my RFC for discussion: >> https://wiki.php.net/rfc/short-and-inner-classes > > > As a user, I find the concept of inner classes quite confusing. > > However, I was looking at some code earlier and thought an "inner > enum" would be useful, to replace various "MODE_FOO" type class > constants - but I see those are left to future scope. :(
Yeah, that's quite disappointing. I expected this to simply apply to all types of "class" until I saw the explicit exclusion.
> > I'm aware they exist in a lot of other languages, though, so I thought > I'd look around at how the proposed version compares. It seems there's > quite a zoo out there... > > One common theme I do note is that all four of the pages I found are > titled "nested classes" or "nested types"; in some cases, "inner > class"/"inner type" has a specific meaning, which I don't think > matches the RFC's proposal. > > > C# - > https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/nested-types > > I think this is most similar to what you're proposing. Interestingly, > the nested class defaults to being private, i.e. only usable within > the parent class.
The default visibility in c# is private, for everything. The RFC is consistent with the default visibility in PHP: public. c# is consistent with the default visibility for c#.
> Classes, structs and interfaces can all be nested inside each other. > > > Java - https://docs.oracle.com/javase/tutorial/java/javaOO/nested.html > > There are two types of nested class (plus some more related concepts > which seem less relevant) > > - "Static nested classes" - static in the sense that they act like a > static member of the surrounding class; appear to be *only* about > namespacing and visibility of the class, with no special access to the > surrounding scope > - "Inner classes" - associated not just with a parent class, but a > parent *instance*, so have to be created with the syntax > "ParentClassName.new InnerClassName" or "this.new InnerClassName"; are > not allowed to have static members
inner (instance) classes in Java are quite a weird beast. I don't think it fits PHP paradigm nicely. Bob

Rob Landers

1 year ago
On Sat, Mar 15, 2025, at 00:21, Rowan Tommins [IMSoP] wrote:
> On 05/03/2025 23:11, Rob Landers wrote: > > > > I'd like to introduce my RFC for discussion: > > https://wiki.php.net/rfc/short-and-inner-classes > > > As a user, I find the concept of inner classes quite confusing. > > However, I was looking at some code earlier and thought an "inner enum" > would be useful, to replace various "MODE_FOO" type class constants - > but I see those are left to future scope. :(
Actually, it appears I did not limit it in the implementation... So, maybe we could play with it and see what breaks. I simply left traits, enums, and interfaces to future scope because 1) I didn't have a working implementation yet, 2) had no idea what would break and 3) every time someone suggests a change to enums, the discussion explodes. Having inner enums, traits, and interfaces is actually quite simple. Might as well see how simple. :) But fwiw, I do plan on a near immediate RFC(s) for these if this thing passes, as well as short classes -- I also have a semi-finished draft with short enums as well, but that one is actually physically impossible without making 'enum' a true reserved word. The "hack" (if you will) to bypass that requirement is still in place and makes the grammar (likely) impossible. I will have to fix this tomorrow, because I am not a fan of having inner classes on interfaces, at least. I will play with it on enums and traits and see what breaks. I suspect inner classes on traits will cause utter chaos.
> I'm aware they exist in a lot of other languages, though, so I thought > I'd look around at how the proposed version compares. It seems there's > quite a zoo out there... > > One common theme I do note is that all four of the pages I found are > titled "nested classes" or "nested types"; in some cases, "inner > class"/"inner type" has a specific meaning, which I don't think matches > the RFC's proposal.
I actually borrowed heavily from C#. I'm familiar with its usage and rules, and it fits nicely with PHP paradigms. — Rob

Rob Landers

1 year ago
On Sat, Mar 15, 2025, at 01:01, Rob Landers wrote:
> > > On Sat, Mar 15, 2025, at 00:21, Rowan Tommins [IMSoP] wrote: >> On 05/03/2025 23:11, Rob Landers wrote: >> > >> > I'd like to introduce my RFC for discussion: >> > https://wiki.php.net/rfc/short-and-inner-classes >> >> >> As a user, I find the concept of inner classes quite confusing. >> >> However, I was looking at some code earlier and thought an "inner enum" >> would be useful, to replace various "MODE_FOO" type class constants - >> but I see those are left to future scope. :( > > Actually, it appears I did not limit it in the implementation... So, maybe we could play with it and see what breaks. I simply left traits, enums, and interfaces to future scope because 1) I didn't have a working implementation yet, 2) had no idea what would break and 3) every time someone suggests a change to enums, the discussion explodes. > > Having inner enums, traits, and interfaces is actually quite simple. Might as well see how simple. :) But fwiw, I do plan on a near immediate RFC(s) for these if this thing passes, as well as short classes -- I also have a semi-finished draft with short enums as well, but that one is actually physically impossible without making 'enum' a true reserved word. The "hack" (if you will) to bypass that requirement is still in place and makes the grammar (likely) impossible. > > I will have to fix this tomorrow, because I am not a fan of having inner classes on interfaces, at least. I will play with it on enums and traits and see what breaks. I suspect inner classes on traits will cause utter chaos. > >> I'm aware they exist in a lot of other languages, though, so I thought >> I'd look around at how the proposed version compares. It seems there's >> quite a zoo out there... >> >> One common theme I do note is that all four of the pages I found are >> titled "nested classes" or "nested types"; in some cases, "inner >> class"/"inner type" has a specific meaning, which I don't think matches >> the RFC's proposal. > > I actually borrowed heavily from C#. I'm familiar with its usage and rules, and it fits nicely with PHP paradigms. > > — Rob
After playing around with it in enums, traits, and even interfaces, I've decided to allow it in them all. I've updated the RFC, fixed an issue in the implementation, and clarified some poorly worded sections. I've also added more realistic examples. — Rob

tag Knife

1 year ago
On Wed, 5 Mar 2025 at 23:14, Rob Landers <rob@bottled.codes> wrote:
> Hello PHP Internals, > > I'd like to introduce my RFC for discussion: > https://wiki.php.net/rfc/short-and-inner-classes > > This RFC defines a short class syntax as well as the ability to nest > classes inside another class. This introduces an unprecedented amount of > control, flexibility, and expressiveness over how objects are used and > instantiated in PHP. There is a PR ( > https://github.com/php/php-src/pull/17895) that implements this > functionality -- all test failures are related to different/new/incorrect > error messages being generated. However, the core functionality exists to > take for a test ride. > > So, what do I mean by "unprecedented amount of control"? With this change, > you can declare an inner class as private or protected, preventing its > usage outside of the outer class: > > class User { > private class Id {} > > public function __construct(public self::Id $id) {} > } > > In the above example, the class `User` is impossible to construct even > though it has a public constructor (except through reflection) because > User::Id is private; User::Id cannot be instantiated, used as a type hint, > or even via `instanceof` outside of the User class itself. This example > isn't practical but demonstrates something that is nearly impossible in > previous versions of PHP, where all classes are essentially publicly > accessible from anywhere within the codebase. > > As a number of inner classes will probably be used as DTOs, the RFC > introduces a "short syntax" for declaring classes, which enhances > expressiveness, even allowing the usage of traits, all in a single line: > > // declare a readonly Point, that implements Vector2 and uses the > Evolvable trait > readonly class Point(public int $x, public int $y) implements Vector2 use > Evolvable; > > When combined with inner classes, it looks something like this: > > class Pixel { > public readonly class Point(public int $x, public int $y) implements > Vector2 use Evolvable; > } > > // Create a new pixel point with property $x and $y set to 0 > $p = new Pixel::Point(0, 0); > > There are far more details in the RFC itself, so please check it out. I'm > quite excited to hear your thoughts! > > — Rob > > PS. I know I tend to rush into things, but I want to make it clear that > I'm not rushing this -- I've learned from my mistakes (thank you to those > who have given me advice). I'm going to do this right. >
I have read and reread this RFC, and I am struggling to see 1. What problem does this solve that anonymous classes <https://www.php.net/manual/en/language.oop5.anonymous.php> do not? 2. As with any syntax change and new operator there needs to be very careful consideration, do we need a new operation, or could `::` if the parent is static or `->` if the class is initialized? 3. The idea that extending the parent class doesnt no inherit the child classes doesnt make sense to me. As then if you extend a parent class and call a function of that class which could rely on the existence of an inner class, I can see a lot of headaches caused by this exact scenario. As a developer, if I extend a class, I expect the entire dependance of that class to be inherited, otherwise the extending class won't work.

Rob Landers

1 year ago
On Mon, Mar 17, 2025, at 16:30, fennic log wrote:
> > On Wed, 5 Mar 2025 at 23:14, Rob Landers <rob@bottled.codes> wrote: >> __ >> Hello PHP Internals, >> >> I'd like to introduce my RFC for discussion: https://wiki.php.net/rfc/short-and-inner-classes >> >> This RFC defines a short class syntax as well as the ability to nest classes inside another class. This introduces an unprecedented amount of control, flexibility, and expressiveness over how objects are used and instantiated in PHP. There is a PR (https://github.com/php/php-src/pull/17895) that implements this functionality -- all test failures are related to different/new/incorrect error messages being generated. However, the core functionality exists to take for a test ride. >> >> So, what do I mean by "unprecedented amount of control"? With this change, you can declare an inner class as private or protected, preventing its usage outside of the outer class: >> >> class User { >> private class Id {} >> >> public function __construct(public self::Id $id) {} >> } >> >> In the above example, the class `User` is impossible to construct even though it has a public constructor (except through reflection) because User::Id is private; User::Id cannot be instantiated, used as a type hint, or even via `instanceof` outside of the User class itself. This example isn't practical but demonstrates something that is nearly impossible in previous versions of PHP, where all classes are essentially publicly accessible from anywhere within the codebase. >> >> As a number of inner classes will probably be used as DTOs, the RFC introduces a "short syntax" for declaring classes, which enhances expressiveness, even allowing the usage of traits, all in a single line: >> >> // declare a readonly Point, that implements Vector2 and uses the Evolvable trait >> readonly class Point(public int $x, public int $y) implements Vector2 use Evolvable; >> >> When combined with inner classes, it looks something like this: >> >> class Pixel { >> public readonly class Point(public int $x, public int $y) implements Vector2 use Evolvable; >> } >> >> // Create a new pixel point with property $x and $y set to 0 >> $p = new Pixel::Point(0, 0); >> >> There are far more details in the RFC itself, so please check it out. I'm quite excited to hear your thoughts! >> >> — Rob >> >> PS. I know I tend to rush into things, but I want to make it clear that I'm not rushing this -- I've learned from my mistakes (thank you to those who have given me advice). I'm going to do this right. > > I have read and reread this RFC, and I am struggling to see > > 1. What problem does this solve that anonymous classes <https://www.php.net/manual/en/language.oop5.anonymous.php> do not?
Thank you for asking this question. :) Anonymous classes can be useful to "hack your way around" the problem that inner (nested?) classes solve. Anonymous classes are inline, leading to complex/long functions that would do nothing but define a class. Anonymous classes can (could?) be hacked to allow for reuse (extending), but only exist where they are defined. An inner class can be extended and reused just fine. Finally, nested classes allow for more readable code without sacrificing encapsulation.
> 2. As with any syntax change and new operator there needs to be very careful consideration, do we need a new operation, or could `::` if the parent is static or `->` if the class is initialized?
There's quite a long thread already about this very topic. That being said, the inner class has no bearing on whether the outer class is instantiated or not. Originally, I used `::` as the separator, but it seems there are some good arguments for `\`, so we will see.
> 3. The idea that extending the parent class doesnt no inherit the child classes doesnt make sense to me. > As then if you extend a parent class and call a function of that class which could rely on the existence of an inner class, I can see a lot of headaches caused by this exact scenario. > As a developer, if I extend a class, I expect the entire dependance of that class to be inherited, otherwise the extending class won't work.
I'm not sure what you mean. When you inherit a class, you do not necessarily inherit everything from its superclass. You are free to override it however you want. Since we are defining "types" in the sense of PHP, we cannot merely inherit a "type", otherwise we would cause all kinds of issues with LSP. This is specifically why inheritance works the way it does in this RFC and why `static::` is forbidden. — Rob

Bob Weinand

1 year ago
Hey Rob, On 17.3.2025 17:53:26, Rob Landers wrote:
> On Mon, Mar 17, 2025, at 16:30, fennic log wrote: >> 2. As with any syntax change and new operator there needs to be very >> careful consideration, do we need a new operation, or could `::` if >> the parent is static or `->` if the class is initialized? > > There's quite a long thread already about this very topic. That being > said, the inner class has no bearing on whether the outer class is > instantiated or not. Originally, I used `::` as the separator, but it > seems there are some good arguments for `\`, so we will see.
I have not grasped any single argument in favour of \, except "other languages are doing it too", "existing tooling splitting on backslash would continue to work" and "we could use the existing use statement as is". The problems are numerous though: 1. Unlike other languages (e.g. C# and java), namespaces and classes may share a name in PHP. E.g. class "Y" in a namespace "X" and class "X" may not exist both. In PHP that's allowed. Inner classes compete with namespaced classes for their fully qualified name. 2. Autoloading will have to become aware of inner classes. 3. Autoloading will have to do a non-cacheable check for existence on the inner class, before iteratively testing whether any parent has a respective .php file. Currently autoloading takes a class name, transforms it to a single canonical path, and includes it. Always succeeding. 4. Humans, navigating to a path, will not be able to navigate to a file directly but have to search for it. 5. Tooling splitting on backslash might assume the 1:1 mapping to the filesystem and break. It's really not a catch all. Some things may be broken and some not, but all need to be probably revisited anyway. 6. It makes accessing via parent, self or static weird. These keywords have always been followed by a double colon. (Even if this particular RFC does not end up adding them) And it will conflict with a namespace named "parent" for example. 7. At least right now, every backslash identified symbol is trivially universally public. With \ as an inner class separator, this would no longer be the case. Maybe that will eventually change, but today this can be relied upon. Also, just because other languages are doing a mistake, it does not mean we have to repeat it. They are generally doing it because their identifier separator is universal and it's consistent. It does not mean that it's without its own problems. Using the double colon is a very minor BC break (accessing a class by a class constant value?! That's also quite inconsistent that it works at all, as you can't do that with normal constants, only class constants.). Using another sigil would also be possible (like :>). But for the backslash I only see drawbacks. Also, nothing precludes us from allowing "use Foo\Bar::Inner;".
>> 3. The idea that extending the parent class doesnt no inherit the >> child classes doesnt make sense to me. >> As then if you extend a parent class and call a function of that >> class which could rely on the existence of an inner class, I can see >> a lot of headaches caused by this exact scenario. >> As a developer, if I extend a class, I expect the entire dependance >> of that class to be inherited, otherwise the extending class won't work. > > I'm not sure what you mean. When you inherit a class, you do not > necessarily inherit everything from its superclass. You are free to > override it however you want. Since we are defining "types" in the > sense of PHP, we cannot merely inherit a "type", otherwise we would > cause all kinds of issues with LSP. This is specifically why > inheritance works the way it does in this RFC and why `static::` is > forbidden.
I don't understand the problem here. for each nested class in parent class:     class_alias(substitute parent class with child class in class name, class name) Very simple. There's no magic needed, it can be simply class aliases. This will also make static, self and parent trivially work. On 15.3.2025 00:37:08, Rob Landers wrote:
> Classes don't actually know their inner classes -- they aren't like > properties. In essence, an inner class is just a regular class with a > funny name and access to scopes it wouldn't normally have access to. > We could probably add `getOuterClass(): string` if that is useful. It > is possible to keep track of a class's inner classes, but then that > introduces a paradox chicken/egg type problem during construction, > which may or may not be a problem. >
I don't see the chicken-and-egg problem here. You can simply collect all the inner class names at compilation. You don't even need to actually store the zend_class_entry - just collect the names and fetch from the class table at runtime (and ignore missing ones, e.g. if there was an error during linking).

Rowan Tommins [IMSoP]

1 year ago
On 17 March 2025 18:05:49 GMT, Bob Weinand <bobwei9@hotmail.com> wrote:
>I have not grasped any single argument in favour of \, except "other languages are doing it too", "existing tooling splitting on backslash would continue to work" and "we could use the existing use statement as is".
This wording feels a bit disingenuous - clearly, you *can* grasp some advantages. It's fine if you don't think those advantages outweigh the disadvantages, but that's different from believing they don't exist. In other words, it's like the famous Monty Python joke:
> All right, but apart from the sanitation, medicine, education, wine, public order, irrigation, roads, the fresh water system and public health, what have the Romans ever done for us?
>Also, just because other languages are doing a mistake, it does not mean we have to repeat it. They are generally doing it because their identifier separator is universal and it's consistent. It does not mean that it's without its own problems.
Absolutely, but where there's a wide adoption of a particular pattern or style, it's worth at least asking whether we're making things better or worse by doing something different. If we look at that, and decide we can do something better, great!
>Using the double colon is a very minor BC break (accessing a class by a class constant value?! That's also quite inconsistent that it works at all, as you can't do that with normal constants, only class constants.). >Using another sigil would also be possible (like :>). But for the backslash I only see drawbacks. > >Also, nothing precludes us from allowing "use Foo\Bar::Inner;".
Personally, I would be equally happy with either \ or :: and less happy with anything that required us choosing yet another set of punctuation, for what is otherwise quite a minor feature in its language impact. Rowan Tommins [IMSoP]

Rob Landers

1 year ago
On Mon, Mar 17, 2025, at 19:05, Bob Weinand wrote:
> Hey Rob, > > On 17.3.2025 17:53:26, Rob Landers wrote: >> On Mon, Mar 17, 2025, at 16:30, fennic log wrote: >>> 2. As with any syntax change and new operator there needs to be very careful consideration, do we need a new operation, or could `::` if the parent is static or `->` if the class is initialized? >> >> There's quite a long thread already about this very topic. That being said, the inner class has no bearing on whether the outer class is instantiated or not. Originally, I used `::` as the separator, but it seems there are some good arguments for `\`, so we will see. > I have not grasped any single argument in favour of \, except "other languages are doing it too", "existing tooling splitting on backslash would continue to work" and "we could use the existing use statement as is". > > > The problems are numerous though: > 1. Unlike other languages (e.g. C# and java), namespaces and classes may share a name in PHP. E.g. class "Y" in a namespace "X" and class "X" may not exist both. In PHP that's allowed. Inner classes compete with namespaced classes for their fully qualified name. > 2. Autoloading will have to become aware of inner classes. > 3. Autoloading will have to do a non-cacheable check for existence on the inner class, before iteratively testing whether any parent has a respective .php file. Currently autoloading takes a class name, transforms it to a single canonical path, and includes it. Always succeeding. > 4. Humans, navigating to a path, will not be able to navigate to a file directly but have to search for it. > 5. Tooling splitting on backslash might assume the 1:1 mapping to the filesystem and break. It's really not a catch all. Some things may be broken and some not, but all need to be probably revisited anyway. > 6. It makes accessing via parent, self or static weird. These keywords have always been followed by a double colon. (Even if this particular RFC does not end up adding them) And it will conflict with a namespace named "parent" for example. > 7. At least right now, every backslash identified symbol is trivially universally public. With \ as an inner class separator, this would no longer be the case. Maybe that will eventually change, but today this can be relied upon. > > Also, just because other languages are doing a mistake, it does not mean we have to repeat it. They are generally doing it because their identifier separator is universal and it's consistent. It does not mean that it's without its own problems. > > > Using the double colon is a very minor BC break (accessing a class by a class constant value?! That's also quite inconsistent that it works at all, as you can't do that with normal constants, only class constants.). > Using another sigil would also be possible (like :>). But for the backslash I only see drawbacks. > > Also, nothing precludes us from allowing "use Foo\Bar::Inner;". > > > >>> 3. The idea that extending the parent class doesnt no inherit the child classes doesnt make sense to me. >>> As then if you extend a parent class and call a function of that class which could rely on the existence of an inner class, I can see a lot of headaches caused by this exact scenario. >>> As a developer, if I extend a class, I expect the entire dependance of that class to be inherited, otherwise the extending class won't work. >> >> I'm not sure what you mean. When you inherit a class, you do not necessarily inherit everything from its superclass. You are free to override it however you want. Since we are defining "types" in the sense of PHP, we cannot merely inherit a "type", otherwise we would cause all kinds of issues with LSP. This is specifically why inheritance works the way it does in this RFC and why `static::` is forbidden. > I don't understand the problem here. > > > for each nested class in parent class: > class_alias(substitute parent class with child class in class name, class name) > > Very simple. There's no magic needed, it can be simply class aliases. This will also make static, self and parent trivially work. >
PHP doesn't go out of its way to prevent the developer from violating LSP -- but where it can, it does. If a type were inherited, then the subclasses wouldn't be able to guarantee that an inner/nested class implemented the parent inner/nested class. Or, if it did, it would require that all subclasses using a class of the same name MUST inherit from the parent class as well. This is non-trivial to implement as the parent class may or may not have been compiled yet when we are compiling the subclass. So, we have no idea what the parent class actually looks like until runtime. Further, it is much simpler to reason about each class as a distinct type vs. maybe inheriting a type from a supertype. Thus, if you want to inherit from the super-class's inner classes, you can, but this RFC won't force you to do so. In my mind, this is the biggest argument for a `\`, because the enclosing class acts more like a namespace than a type, from the perspective of the inner class. If we were to embrace `\`, then it could be argued that a namespace is technically a class in itself, (but a partial class, to borrow from C# terminology) and every class in a namespace is essentially just a public class in that super-class/namespace. Nobody has argued that perspective, but I believe it may be interesting (and would lay a foundation for namespace private/public class behavior/visibility). That being said, it truly does cause issues with autoloading -- at least, PSR-4 autoloading -- and I'm not sure whether we should solve that problem here; however, it is something to be cognizant of, for sure. There are other types of autoloading supported by tools, such as composer, that do not have any issues with using `\` as a separator.
> On 15.3.2025 00:37:08, Rob Landers wrote: > > > >> Classes don't actually know their inner classes -- they aren't like properties. In essence, an inner class is just a regular class with a funny name and access to scopes it wouldn't normally have access to. We could probably add `getOuterClass(): string` if that is useful. It is possible to keep track of a class's inner classes, but then that introduces a paradox chicken/egg type problem during construction, which may or may not be a problem. >> > > > I don't see the chicken-and-egg problem here. You can simply collect all the inner class names at compilation. You don't even need to actually store the zend_class_entry - just collect the names and fetch from the class table at runtime (and ignore missing ones, e.g. if there was an error during linking). >
🙃sometimes the obvious solutions are the best solutions. — Rob

Bob Weinand

1 year ago
Hey, On 17.3.2025 19:58:39, Rob Landers wrote:
> On Mon, Mar 17, 2025, at 19:05, Bob Weinand wrote: >>>> . The idea that extending the parent class doesnt no inherit the >>>> child classes doesnt make sense to me. >>>> As then if you extend a parent class and call a function of that >>>> class which could rely on the existence of an inner class, I can >>>> see a lot of headaches caused by this exact scenario. >>>> As a developer, if I extend a class, I expect the entire dependance >>>> of that class to be inherited, otherwise the extending class won't >>>> work. >>> >>> I'm not sure what you mean. When you inherit a class, you do not >>> necessarily inherit everything from its superclass. You are free to >>> override it however you want. Since we are defining "types" in the >>> sense of PHP, we cannot merely inherit a "type", otherwise we would >>> cause all kinds of issues with LSP. This is specifically why >>> inheritance works the way it does in this RFC and why `static::` is >>> forbidden. >> >> I don't understand the problem here. >> >> for each nested class in parent class: >>     class_alias(substitute parent class with child class in class >> name, class name) >> >> Very simple. There's no magic needed, it can be simply class aliases. >> This will also make static, self and parent trivially work. >> > > PHP doesn't go out of its way to prevent the developer from violating > LSP -- but where it can, it does. If a type were inherited, then the > subclasses wouldn't be able to guarantee that an inner/nested class > implemented the parent inner/nested class. Or, if it did, it would > require that all subclasses using a class of the same name MUST > inherit from the parent class as well. > > This is non-trivial to implement as the parent class may or may not > have been compiled yet when we are compiling the subclass. So, we have > no idea what the parent class actually looks like until runtime. > Further, it is much simpler to reason about each class as a distinct > type vs. maybe inheriting a type from a supertype. > > Thus, if you want to inherit from the super-class's inner classes, you > can, but this RFC won't force you to do so. In my mind, this is the > biggest argument for a `\`, because the enclosing class acts more like > a namespace than a type, from the perspective of the inner class. > > If we were to embrace `\`, then it could be argued that a namespace is > technically a class in itself, (but a partial class, to borrow from C# > terminology) and every class in a namespace is essentially just a > public class in that super-class/namespace. > > Nobody has argued that perspective, but I believe it may be > interesting (and would lay a foundation for namespace private/public > class behavior/visibility). That being said, it truly does cause > issues with autoloading -- at least, PSR-4 autoloading -- and I'm not > sure whether we should solve that problem here; however, it is > something to be cognizant of, for sure. There are other types of > autoloading supported by tools, such as composer, that do not have any > issues with using `\` as a separator.
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. However nice grand unified naming schemes may seem, I don't think it's a good idea to pursue. Clarity and explicitness shall reign supreme here. I also don't think that the proposed visibilities are applicable to namespaced classes. In particular and in practice shared internal classes are not necessarily directly organized in a way it makes sense to organize inner classes. Also visibilities like protected propagate along the inheritance chain, something which is not given with (outer) namespaced classes. The less we mix these slightly different concepts, the better. "It's similar, except in these and those cases" is the death of all consistent experiences. Thus, let's not even attempt to pretend it is. And not pretending starts with using a different symbol than a backslash. Bob

Rob Landers

1 year ago
On Tue, Mar 18, 2025, at 03:37, Bob Weinand wrote:
> Hey, > > On 17.3.2025 19:58:39, Rob Landers wrote: >> On Mon, Mar 17, 2025, at 19:05, Bob Weinand wrote: >>>>> . The idea that extending the parent class doesnt no inherit the child classes doesnt make sense to me. >>>>> As then if you extend a parent class and call a function of that class which could rely on the existence of an inner class, I can see a lot of headaches caused by this exact scenario. >>>>> As a developer, if I extend a class, I expect the entire dependance of that class to be inherited, otherwise the extending class won't work. >>>> >>>> I'm not sure what you mean. When you inherit a class, you do not necessarily inherit everything from its superclass. You are free to override it however you want. Since we are defining "types" in the sense of PHP, we cannot merely inherit a "type", otherwise we would cause all kinds of issues with LSP. This is specifically why inheritance works the way it does in this RFC and why `static::` is forbidden. >>> I don't understand the problem here. >>> >>> for each nested class in parent class: >>> class_alias(substitute parent class with child class in class name, class name) >>> Very simple. There's no magic needed, it can be simply class aliases. This will also make static, self and parent trivially work. >>> >> >> PHP doesn't go out of its way to prevent the developer from violating LSP -- but where it can, it does. If a type were inherited, then the subclasses wouldn't be able to guarantee that an inner/nested class implemented the parent inner/nested class. Or, if it did, it would require that all subclasses using a class of the same name MUST inherit from the parent class as well. >> >> This is non-trivial to implement as the parent class may or may not have been compiled yet when we are compiling the subclass. So, we have no idea what the parent class actually looks like until runtime. Further, it is much simpler to reason about each class as a distinct type vs. maybe inheriting a type from a supertype. >> >> Thus, if you want to inherit from the super-class's inner classes, you can, but this RFC won't force you to do so. In my mind, this is the biggest argument for a `\`, because the enclosing class acts more like a namespace than a type, from the perspective of the inner class. >> >> If we were to embrace `\`, then it could be argued that a namespace is technically a class in itself, (but a partial class, to borrow from C# terminology) and every class in a namespace is essentially just a public class in that super-class/namespace. >> >> Nobody has argued that perspective, but I believe it may be interesting (and would lay a foundation for namespace private/public class behavior/visibility). That being said, it truly does cause issues with autoloading -- at least, PSR-4 autoloading -- and I'm not sure whether we should solve that problem here; however, it is something to be cognizant of, for sure. There are other types of autoloading supported by tools, such as composer, that do not have any issues with using `\` as a separator. > 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 }
> > However nice grand unified naming schemes may seem, I don't think it's a good idea to pursue. Clarity and explicitness shall reign supreme here. > I also don't think that the proposed visibilities are applicable to namespaced classes. In particular and in practice shared internal classes are not necessarily directly organized in a way it makes sense to organize inner classes. Also visibilities like protected propagate along the inheritance chain, something which is not given with (outer) namespaced classes. > The less we mix these slightly different concepts, the better. > > > "It's similar, except in these and those cases" is the death of all consistent experiences. Thus, let's not even attempt to pretend it is. >
This is true.
> > 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<T> { public function add(self:>T $item) {} } The same thing would also probably apply to :: class Box<T> { public function add(self::T $item) {} } With `\`, it nearly follows exactly what you would expect-ish: use \Box\T as T; class Box<T> { 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<T> { 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

Bob Weinand

1 year ago
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<T> { >   public function add(self:>T $item) {} > } > > The same thing would also probably apply to :: > > class Box<T> { >   public function add(self::T $item) {} > } > > With `\`, it nearly follows exactly what you would expect-ish: > > use \Box\T as T; > > class Box<T> { >   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<T> { >   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<T> {   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<T> extends ParentClass<T> - or something along these lines, getting the T available for your class. Bob

Rob Landers

1 year ago
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<T> { >> public function add(self:>T $item) {} >> } >> >> The same thing would also probably apply to :: >> >> class Box<T> { >> public function add(self::T $item) {} >> } >> >> With `\`, it nearly follows exactly what you would expect-ish: >> >> use \Box\T as T; >> >> class Box<T> { >> 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<T> { >> 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<T> { > 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<T> extends ParentClass<T> - 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

Bob Weinand

1 year ago
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<T> { public function add(self:>T $item) {} } The same thing would also probably apply to :: class Box<T> { public function add(self::T $item) {} } With `\`, it nearly follows exactly what you would expect-ish: use \Box\T as T; class Box<T> { 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<T> { 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<T> { 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<T> extends ParentClass<T> - 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

Peter Stalman

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