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.
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,
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
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
- 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
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.
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.
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
Hi
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
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-classesThis 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 viainstanceof
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/
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
Hi
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
Hi
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
Hi
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 toP
, not toC
, so you can donew C::Inner()
and it will resolve toP::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
Hi
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 toP
, not toC
, so you can donew C::Inner()
and it will resolve toP::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
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
- Even shorter way to define promoted properties for the common case.
- 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
Hi Rob
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
Hi Rob
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
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
Hey Ilija,
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
Hi Bob
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 Bob
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\Barpublic 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
withoutself:>
, 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
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]
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
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]
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
Hey Ilija,
Hi Bob
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\Barpublic 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
withoutself:>
, 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
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 viainstanceof
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
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
Hi Rob
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
Hi Rob
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 themI 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
Hi Rob
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 simpleAnd 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
Hi Rob
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 simpleAnd 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
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/namespaceseparatorWould
\\
(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
Hey Rob,
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/namespaceseparatorWould
\\
(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üsterhusI 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
Hey Rob,
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/namespaceseparatorWould
\\
(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üsterhusI 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
Hey Rob,
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 constantI 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
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
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
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 viainstanceof
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
Hey Rob,
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
Hey Rob,
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.
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
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]
Le 15 mars 2025 à 12:53, Rowan Tommins [IMSoP] imsop.php@rwec.co.uk a écrit :
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
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 :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
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
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]
On Mon, Ma0r 17, 2025 at 9:56 AM Rowan Tommins [IMSoP] imsop.php@rwec.co.uk
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
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]
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
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]
I'd like to introduce my RFC for discussion:
https://wiki.php.net/rfc/short-and-inner-classesAs 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-typesI 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
I'd like to introduce my RFC for discussion:
https://wiki.php.net/rfc/short-and-inner-classesAs 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
I'd like to introduce my RFC for discussion:
https://wiki.php.net/rfc/short-and-inner-classesAs 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
Hello PHP Internals,
I'd like to introduce my RFC for discussion:
https://wiki.php.net/rfc/short-and-inner-classesThis 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 viainstanceof
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
- What problem does this solve that anonymous classes
https://www.php.net/manual/en/language.oop5.anonymous.php do not? - 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? - 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.
__
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 viainstanceof
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
- 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.
- 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.
- 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
Hey Rob,
- 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:
- 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. - Autoloading will have to become aware of inner classes.
- 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. - Humans, navigating to a path, will not be able to navigate to a file
directly but have to search for it. - 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. - 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. - 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;".
- 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 whystatic::
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.
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 addgetOuterClass(): 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).
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]
Hey Rob,
- 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:
- 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.
- Autoloading will have to become aware of inner classes.
- 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.
- Humans, navigating to a path, will not be able to navigate to a file directly but have to search for it.
- 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.
- 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.
- 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;".
- 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.
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
Hey,
. 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 whystatic::
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
Hey,
. 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
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