From f3aa678b65dfb481b883521881b1761f915e84de Mon Sep 17 00:00:00 2001 From: Robert Landers Date: Mon, 17 Feb 2025 20:43:52 +0100 Subject: [PATCH 01/13] add short classes --- drafts/short-class.md | 274 ++++++++++++++++++++++++++++++++++++++++++ drafts/template.md | 9 +- 2 files changed, 279 insertions(+), 4 deletions(-) create mode 100644 drafts/short-class.md diff --git a/drafts/short-class.md b/drafts/short-class.md new file mode 100644 index 0000000..66942ba --- /dev/null +++ b/drafts/short-class.md @@ -0,0 +1,274 @@ +# PHP RFC: Short and Inner Classes + +* Version: 0.1 +* Date: 2025-02-08 +* Author: Rob Landers, rob@bottled.codes +* Status: Draft (or Under Discussion or Accepted or Declined) +* First Published at: + +## Introduction + +This RFC proposes a new short syntax for class definitions in PHP and the ability to embed these classes within other +classes. + +## Proposal + +Data transfer objects (DTOs) are a common pattern in PHP applications and are usually simple data structures that hold +data and have no behavior. +With this RFC, +we propose a simple and concise syntax for defining these classes, looking almost like named anonymous classes, as well as +the ability to embed them within other classes. + +### Short Class Syntax + +The proposed syntax for a short class definition is as follows: a keyword `class`, +followed by the class name, then a list of public properties enclosed in parentheses. +Optionally, a list of traits, interfaces, and a parent class may be defined. + +```php +class Point(int $x, int $y); +``` + +This is equivalent to the following full class definition: + +```php +class Point { + public function __construct(public int $x, public int $y) {} +} +``` + +Any properties defined within the parenthesis are defined as a public property of the class. + +#### Default Values + +Default values may be provided for properties: + +```php +class Point(int $x = 0, int $y = 0); +``` + +#### Inheritance and Behavior + +With class short syntax, no behavior may be defined, yet it can still utilize traits, interfaces, and other classes. + +```php +class Point(int $x, int $y) extends BasePoint implements JsonSerializable use PointTrait, Evolvable; +``` + +Note that the original constructor from any parent class is overridden and not called by the short syntax. + +#### Empty Classes + +Short classes may also be empty: + +```php +class Point() extends BasePoint use PointTrait; +``` + +#### Attributes + +Attributes may also be used with short classes: + +```php +class Password(#[SensitiveParameter] string $password); +``` + +### Inner Classes + +Inner classes are classes that are defined within another class. + +```php +class Foo { + class Bar(public string $message); + + private class Baz { + public function __construct(public string $message) {} + } +} + +$foo = new Foo::Bar('Hello, world!'); +echo $foo->message; +// outputs: Hello, world! +$baz = new Foo::Baz('Hello, world!'); +// Fatal error: Uncaught Error: Cannot access private class Foo::Baz +``` + +Inner classes have scope similar to properties, which applies to parameters and returns types as well. + +#### Modifiers + +Properties support modifiers such as `public`, `protected`, and `private` as well as `static`, `final` and `readonly`. +When using these as modifiers on an inner class, there are some intuitive rules: + +- `public`, `private`, and `protected` apply to the visibility of the class. +- `static`, `final`, and `readonly` apply to the class itself. + +Thus, an inner class with the modifier `private readonly` is only accessible within the class +and any instances are readonly. + +#### Visibility + +A `private` or `protected` inner class is only accessible within the class it is defined in +(or its subclasses in the case of protected classes). +This also applies to methods and return types. + +```php +class Foo { + private class Bar(public string $message); + + // Fatal error: Uncaught Error: Cannot return private class Foo::Bar + public function getMessage(): Bar { + return new Bar('Hello, world!'); + } +} +``` + +#### Accessing Inner Classes + +From outside the class, public inner classes may be accessed using the `::` operator: + +```php +new Foo::Bar('Hello, world!'); +``` + +This may also be used from inside the class or in subclasses at the developer’s discretion. Alternatively, inner classes +may be accessed using `self::` or `static::` from inside the class, or just using the name itself: + +```php + +private function getBar(): Foo::Bar { + $a = new Bar('Hello, world!'); + $b = new self::Bar('Hello, world!'); + $c = new static::Bar('Hello, world!'); + $d = new Foo::Bar('Hello, world!'); +} + +``` + +Note that inner classes effectively "shadow" outer classes of the same name: + +```php +readonly class Vect(int $x, int $y); + +class Foo { + class Vect(int $x, int $y, int $z); + + // Vect is Foo::Vect not \Vect + public function __construct(public Vect $vect) {} +} +``` + +#### Names + +Inner classes may not have any name that conflicts with a constant or static method of the same name. + +```php +class Foo { + const Bar = 'bar'; + class Bar(public string $message); + + // Fatal error: Uncaught Error: Cannot redeclare Foo::Bar +} + +class Foo { + static function Bar() {} + class Bar(public string $message); + + // Fatal error: Uncaught Error: Cannot redeclare Foo::Bar +} +``` + +These rules are to prevent developer confusion because these instantiations all look similar, +but without this rule, they would all work: + +```php +new (Foo::Bar); // create a new class from the name stored in Foo::Bar +new (Foo::Bar()); // create a new instance from the name returned by Foo::Bar() +new Foo::Bar(); // create a new instance of the class Foo::Bar +``` + +## Backward Incompatible Changes + +What breaks, and what is the justification for it? + +## Proposed PHP Version(s) + +List the proposed PHP versions that the feature will be included in. Use +relative versions such as "next PHP 8.x" or "next PHP 8.x.y". + +## RFC Impact + +### To SAPIs + +Describe the impact to CLI, Development web server, embedded PHP etc. + +### To Existing Extensions + +Will existing extensions be affected? + +### To Opcache + +It is necessary to develop RFC's with opcache in mind, since opcache is +a core extension distributed with PHP. + +Please explain how you have verified your RFC's compatibility with +opcache. + +### New Constants + +Describe any new constants so they can be accurately and comprehensively +explained in the PHP documentation. + +### php.ini Defaults + +If there are any php.ini settings then list: \* hardcoded default values +\* php.ini-development values \* php.ini-production values + +## Open Issues + +Make sure there are no open issues when the vote starts! + +## Unaffected PHP Functionality + +List existing areas/features of PHP that will not be changed by the RFC. + +This helps avoid any ambiguity, shows that you have thought deeply about +the RFC's impact, and helps reduces mail list noise. + +## Future Scope + +This section details areas where the feature might be improved in +future, but that are not currently proposed in this RFC. + +## Proposed Voting Choices + +Include these so readers know where you are heading and can discuss the +proposed voting options. + +## Patches and Tests + +Links to any external patches and tests go here. + +If there is no patch, make it clear who will create a patch, or whether +a volunteer to help with implementation is needed. + +Make it clear if the patch is intended to be the final patch, or is just +a prototype. + +For changes affecting the core language, you should also provide a patch +for the language specification. + +## Implementation + +After the project is implemented, this section should contain - the +version(s) it was merged into - a link to the git commit(s) - a link to +the PHP manual entry for the feature - a link to the language +specification section (if any) + +## References + +Links to external references, discussions or RFCs + +## Rejected Features + +Keep this updated with features that were discussed on the mail lists. diff --git a/drafts/template.md b/drafts/template.md index 6142836..eedddc1 100644 --- a/drafts/template.md +++ b/drafts/template.md @@ -1,9 +1,10 @@ # PHP RFC: Your Title Here -\* Version: 0.9 \* Date: 2013-02-24 (use today's date here) \* Author: -Your Name, your_email_address@example.com \* Status: Draft (or Under -Discussion or Accepted or Declined) \* First Published at: - +* Version: 0.9 +* Date: 2013-02-24 (use today's date here) +* Author: Your Name, your_email_address@example.com +* Status: Draft (or Under Discussion or Accepted or Declined) +* First Published at: This is a suggested template for PHP Request for Comments (RFCs). Change this template to suit your RFC. Not all RFCs need to be tightly From 6e7f995ea1b1cd7ce0ee4f769fafa72ecb26410e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 17 Feb 2025 19:44:24 +0000 Subject: [PATCH 02/13] Automated changes from GitHub Actions --- published/short-class.ptxt | 246 +++++++++++++++++++++++++++++++++++++ published/template.ptxt | 12 +- 2 files changed, 254 insertions(+), 4 deletions(-) create mode 100644 published/short-class.ptxt diff --git a/published/short-class.ptxt b/published/short-class.ptxt new file mode 100644 index 0000000..6cf5d85 --- /dev/null +++ b/published/short-class.ptxt @@ -0,0 +1,246 @@ +====== PHP RFC: Short and Inner Classes ====== + + * Version: 0.1 + * Date: 2025-02-08 + * Author: Rob Landers, + * Status: Draft (or Under Discussion or Accepted or Declined) + * First Published at: http://wiki.php.net/rfc/short-class + +===== Introduction ===== + +This RFC proposes a new short syntax for class definitions in PHP and the ability to embed these classes within other classes. + +===== Proposal ===== + +Data transfer objects (DTOs) are a common pattern in PHP applications and are usually simple data structures that hold data and have no behavior. With this RFC, we propose a simple and concise syntax for defining these classes, looking almost like named anonymous classes, as well as the ability to embed them within other classes. + +==== Short Class Syntax ==== + +The proposed syntax for a short class definition is as follows: a keyword ''%%class%%'', followed by the class name, then a list of public properties enclosed in parentheses. Optionally, a list of traits, interfaces, and a parent class may be defined. + + +class Point(int $x, int $y); + + +This is equivalent to the following full class definition: + + +class Point { + public function __construct(public int $x, public int $y) {} +} + + +Any properties defined within the parenthesis are defined as a public property of the class. + +=== Default Values === + +Default values may be provided for properties: + + +class Point(int $x = 0, int $y = 0); + + +=== Inheritance and Behavior === + +With class short syntax, no behavior may be defined, yet it can still utilize traits, interfaces, and other classes. + + +class Point(int $x, int $y) extends BasePoint implements JsonSerializable use PointTrait, Evolvable; + + +Note that the original constructor from any parent class is overridden and not called by the short syntax. + +=== Empty Classes === + +Short classes may also be empty: + + +class Point() extends BasePoint use PointTrait; + + +=== Attributes === + +Attributes may also be used with short classes: + + +class Password(#[SensitiveParameter] string $password); + + +==== Inner Classes ==== + +Inner classes are classes that are defined within another class. + + +class Foo { + class Bar(public string $message); + + private class Baz { + public function __construct(public string $message) {} + } +} + +$foo = new Foo::Bar('Hello, world!'); +echo $foo->message; +// outputs: Hello, world! +$baz = new Foo::Baz('Hello, world!'); +// Fatal error: Uncaught Error: Cannot access private class Foo::Baz + + +Inner classes have scope similar to properties, which applies to parameters and returns types as well. + +=== Modifiers === + +Properties support modifiers such as ''%%public%%'', ''%%protected%%'', and ''%%private%%'' as well as ''%%static%%'', ''%%final%%'' and ''%%readonly%%''. When using these as modifiers on an inner class, there are some intuitive rules: + + * ''%%public%%'', ''%%private%%'', and ''%%protected%%'' apply to the visibility of the class. + * ''%%static%%'', ''%%final%%'', and ''%%readonly%%'' apply to the class itself. + +Thus, an inner class with the modifier ''%%private readonly%%'' is only accessible within the class and any instances are readonly. + +=== Visibility === + +A ''%%private%%'' or ''%%protected%%'' inner class is only accessible within the class it is defined in (or its subclasses in the case of protected classes). This also applies to methods and return types. + + +class Foo { + private class Bar(public string $message); + + // Fatal error: Uncaught Error: Cannot return private class Foo::Bar + public function getMessage(): Bar { + return new Bar('Hello, world!'); + } +} + + +=== Accessing Inner Classes === + +From outside the class, public inner classes may be accessed using the ''%%::%%'' operator: + + +new Foo::Bar('Hello, world!'); + + +This may also be used from inside the class or in subclasses at the developer’s discretion. Alternatively, inner classes may be accessed using ''%%self::%%'' or ''%%static::%%'' from inside the class, or just using the name itself: + + + +private function getBar(): Foo::Bar { + $a = new Bar('Hello, world!'); + $b = new self::Bar('Hello, world!'); + $c = new static::Bar('Hello, world!'); + $d = new Foo::Bar('Hello, world!'); +} + + +Note that inner classes effectively "shadow" outer classes of the same name: + + +readonly class Vect(int $x, int $y); + +class Foo { + class Vect(int $x, int $y, int $z); + + // Vect is Foo::Vect not \Vect + public function __construct(public Vect $vect) {} +} + + +=== Names === + +Inner classes may not have any name that conflicts with a constant or static method of the same name. + + +class Foo { + const Bar = 'bar'; + class Bar(public string $message); + + // Fatal error: Uncaught Error: Cannot redeclare Foo::Bar +} + +class Foo { + static function Bar() {} + class Bar(public string $message); + + // Fatal error: Uncaught Error: Cannot redeclare Foo::Bar +} + + +These rules are to prevent developer confusion because these instantiations all look similar, but without this rule, they would all work: + + +new (Foo::Bar); // create a new class from the name stored in Foo::Bar +new (Foo::Bar()); // create a new instance from the name returned by Foo::Bar() +new Foo::Bar(); // create a new instance of the class Foo::Bar + + +===== Backward Incompatible Changes ===== + +What breaks, and what is the justification for it? + +===== Proposed PHP Version(s) ===== + +List the proposed PHP versions that the feature will be included in. Use relative versions such as "next PHP 8.x" or "next PHP 8.x.y". + +===== RFC Impact ===== + +==== To SAPIs ==== + +Describe the impact to CLI, Development web server, embedded PHP etc. + +==== To Existing Extensions ==== + +Will existing extensions be affected? + +==== To Opcache ==== + +It is necessary to develop RFC's with opcache in mind, since opcache is a core extension distributed with PHP. + +Please explain how you have verified your RFC's compatibility with opcache. + +==== New Constants ==== + +Describe any new constants so they can be accurately and comprehensively explained in the PHP documentation. + +==== php.ini Defaults ==== + +If there are any php.ini settings then list: * hardcoded default values * php.ini-development values * php.ini-production values + +===== Open Issues ===== + +Make sure there are no open issues when the vote starts! + +===== Unaffected PHP Functionality ===== + +List existing areas/features of PHP that will not be changed by the RFC. + +This helps avoid any ambiguity, shows that you have thought deeply about the RFC's impact, and helps reduces mail list noise. + +===== Future Scope ===== + +This section details areas where the feature might be improved in future, but that are not currently proposed in this RFC. + +===== Proposed Voting Choices ===== + +Include these so readers know where you are heading and can discuss the proposed voting options. + +===== Patches and Tests ===== + +Links to any external patches and tests go here. + +If there is no patch, make it clear who will create a patch, or whether a volunteer to help with implementation is needed. + +Make it clear if the patch is intended to be the final patch, or is just a prototype. + +For changes affecting the core language, you should also provide a patch for the language specification. + +===== Implementation ===== + +After the project is implemented, this section should contain - the version(s) it was merged into - a link to the git commit(s) - a link to the PHP manual entry for the feature - a link to the language specification section (if any) + +===== References ===== + +Links to external references, discussions or RFCs + +===== Rejected Features ===== + +Keep this updated with features that were discussed on the mail lists. diff --git a/published/template.ptxt b/published/template.ptxt index 3f379b3..fe97dc4 100644 --- a/published/template.ptxt +++ b/published/template.ptxt @@ -1,17 +1,21 @@ ====== PHP RFC: Your Title Here ====== -* Version: 0.9 * Date: 2013-02-24 (use today's date here) * Author: Your Name, * Status: Draft (or Under Discussion or Accepted or Declined) * First Published at: http://wiki.php.net/rfc/your_rfc_name + * Version: 0.9 + * Date: 2013-02-24 (use today's date here) + * Author: Your Name, + * Status: Draft (or Under Discussion or Accepted or Declined) + * First Published at: http://wiki.php.net/rfc/your_rfc_name This is a suggested template for PHP Request for Comments (RFCs). Change this template to suit your RFC. Not all RFCs need to be tightly specified. Not all RFCs need all the sections below. Read https://wiki.php.net/rfc/howto carefully! Quoting [[http://news.php.net/php.internals/71525|Rasmus]]: > PHP is and should remain: -> + > 1) a pragmatic web-focused language -> + > 2) a loosely typed language -> + > 3) a language which caters to the skill-levels and platforms of a wide range of users Your RFC should move PHP forward following his vision. As [[http://news.php.net/php.internals/66065|said by Zeev Suraski]] "Consider only features which have significant traction to a large chunk of our userbase, and not something that could be useful in some extremely specialized edge cases [...] Make sure you think about the full context, the huge audience out there, the consequences of making the learning curve steeper with every new feature, and the scope of the goodness that those new features bring." From 07b16ca490cd6038d54fd5178622b94a9ab5925f Mon Sep 17 00:00:00 2001 From: Robert Landers Date: Mon, 17 Feb 2025 20:51:33 +0100 Subject: [PATCH 03/13] add short enums --- drafts/short-class.md | 46 ++++++++++++++++++++++++++++++++++--------- 1 file changed, 37 insertions(+), 9 deletions(-) diff --git a/drafts/short-class.md b/drafts/short-class.md index 66942ba..93e0538 100644 --- a/drafts/short-class.md +++ b/drafts/short-class.md @@ -1,4 +1,4 @@ -# PHP RFC: Short and Inner Classes +# PHP RFC: Inner Classes with Short Syntax * Version: 0.1 * Date: 2025-02-08 @@ -8,7 +8,7 @@ ## Introduction -This RFC proposes a new short syntax for class definitions in PHP and the ability to embed these classes within other +This RFC proposes a new short syntax for class/enum definitions in PHP and the ability to embed these classes within other classes. ## Proposal @@ -73,6 +73,25 @@ Attributes may also be used with short classes: class Password(#[SensitiveParameter] string $password); ``` +### Short enums + +Enums are a common pattern in PHP applications and are usually simple data structures that hold a set of constants. +This RFC includes a proposal for short enums: + +```php +enum Color(Red, Green, Blue); +``` + +This is equivalent to the following full enum definition: + +```php +enum Color { + case Red; + case Green; + case Blue; +} +``` + ### Inner Classes Inner classes are classes that are defined within another class. @@ -80,6 +99,7 @@ Inner classes are classes that are defined within another class. ```php class Foo { class Bar(public string $message); + enum Baz(One, Two, Three); private class Baz { public function __construct(public string $message) {} @@ -160,7 +180,7 @@ class Foo { #### Names -Inner classes may not have any name that conflicts with a constant or static method of the same name. +Inner classes may not have any name that conflicts with a constant or static property of the same name. ```php class Foo { @@ -171,7 +191,7 @@ class Foo { } class Foo { - static function Bar() {} + static $Bar = 'bar'; class Bar(public string $message); // Fatal error: Uncaught Error: Cannot redeclare Foo::Bar @@ -179,17 +199,25 @@ class Foo { ``` These rules are to prevent developer confusion because these instantiations all look similar, -but without this rule, they would all work: +however, the following all result in the same inner class being instantiated: ```php -new (Foo::Bar); // create a new class from the name stored in Foo::Bar -new (Foo::Bar()); // create a new instance from the name returned by Foo::Bar() -new Foo::Bar(); // create a new instance of the class Foo::Bar +new (Foo::Bar); +new (Foo::$Bar); +new Foo::Bar(); ``` ## Backward Incompatible Changes -What breaks, and what is the justification for it? +Creating a new instance from an existing static member is now allowed: + +```php +class Foo { + public const Bar = 'bar'; +} + +new Foo::Bar(); // previously this is a syntax error, but now results in creating a new "bar" object. +``` ## Proposed PHP Version(s) From 6664e9f75cadbedfb4919f75bb41a02eb8ee951c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 17 Feb 2025 19:52:26 +0000 Subject: [PATCH 04/13] Automated changes from GitHub Actions --- published/short-class.ptxt | 45 ++++++++++++++++++++++++++++++-------- 1 file changed, 36 insertions(+), 9 deletions(-) diff --git a/published/short-class.ptxt b/published/short-class.ptxt index 6cf5d85..9769e9f 100644 --- a/published/short-class.ptxt +++ b/published/short-class.ptxt @@ -1,4 +1,4 @@ -====== PHP RFC: Short and Inner Classes ====== +====== PHP RFC: Inner Classes with Short Syntax ====== * Version: 0.1 * Date: 2025-02-08 @@ -8,7 +8,7 @@ ===== Introduction ===== -This RFC proposes a new short syntax for class definitions in PHP and the ability to embed these classes within other classes. +This RFC proposes a new short syntax for class/enum definitions in PHP and the ability to embed these classes within other classes. ===== Proposal ===== @@ -66,6 +66,24 @@ Attributes may also be used with short classes: class Password(#[SensitiveParameter] string $password); +==== Short enums ==== + +Enums are a common pattern in PHP applications and are usually simple data structures that hold a set of constants. This RFC includes a proposal for short enums: + + +enum Color(Red, Green, Blue); + + +This is equivalent to the following full enum definition: + + +enum Color { + case Red; + case Green; + case Blue; +} + + ==== Inner Classes ==== Inner classes are classes that are defined within another class. @@ -73,6 +91,7 @@ Inner classes are classes that are defined within another class. class Foo { class Bar(public string $message); + enum Baz(One, Two, Three); private class Baz { public function __construct(public string $message) {} @@ -147,7 +166,7 @@ class Foo { === Names === -Inner classes may not have any name that conflicts with a constant or static method of the same name. +Inner classes may not have any name that conflicts with a constant or static property of the same name. class Foo { @@ -158,24 +177,32 @@ class Foo { } class Foo { - static function Bar() {} + static $Bar = 'bar'; class Bar(public string $message); // Fatal error: Uncaught Error: Cannot redeclare Foo::Bar } -These rules are to prevent developer confusion because these instantiations all look similar, but without this rule, they would all work: +These rules are to prevent developer confusion because these instantiations all look similar, however, the following all result in the same inner class being instantiated: -new (Foo::Bar); // create a new class from the name stored in Foo::Bar -new (Foo::Bar()); // create a new instance from the name returned by Foo::Bar() -new Foo::Bar(); // create a new instance of the class Foo::Bar +new (Foo::Bar); +new (Foo::$Bar); +new Foo::Bar(); ===== Backward Incompatible Changes ===== -What breaks, and what is the justification for it? +Creating a new instance from an existing static member is now allowed: + + +class Foo { + public const Bar = 'bar'; +} + +new Foo::Bar(); // previously this is a syntax error, but now results in creating a new "bar" object. + ===== Proposed PHP Version(s) ===== From 30297e498fe32415ef590cbd7cbf2d32d70ac1c1 Mon Sep 17 00:00:00 2001 From: Robert Landers Date: Tue, 18 Feb 2025 20:46:50 +0100 Subject: [PATCH 05/13] remove short enums for now --- drafts/short-class.md | 21 +-------------------- 1 file changed, 1 insertion(+), 20 deletions(-) diff --git a/drafts/short-class.md b/drafts/short-class.md index 93e0538..96804c8 100644 --- a/drafts/short-class.md +++ b/drafts/short-class.md @@ -8,7 +8,7 @@ ## Introduction -This RFC proposes a new short syntax for class/enum definitions in PHP and the ability to embed these classes within other +This RFC proposes a new short syntax for class definitions in PHP and the ability to embed these classes within other classes. ## Proposal @@ -73,25 +73,6 @@ Attributes may also be used with short classes: class Password(#[SensitiveParameter] string $password); ``` -### Short enums - -Enums are a common pattern in PHP applications and are usually simple data structures that hold a set of constants. -This RFC includes a proposal for short enums: - -```php -enum Color(Red, Green, Blue); -``` - -This is equivalent to the following full enum definition: - -```php -enum Color { - case Red; - case Green; - case Blue; -} -``` - ### Inner Classes Inner classes are classes that are defined within another class. From ee67ec94231e9982c2fdc9cc9d78bd1fcfb1efa7 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 18 Feb 2025 19:48:36 +0000 Subject: [PATCH 06/13] Automated changes from GitHub Actions --- published/short-class.ptxt | 20 +------------------- 1 file changed, 1 insertion(+), 19 deletions(-) diff --git a/published/short-class.ptxt b/published/short-class.ptxt index 9769e9f..48a0e12 100644 --- a/published/short-class.ptxt +++ b/published/short-class.ptxt @@ -8,7 +8,7 @@ ===== Introduction ===== -This RFC proposes a new short syntax for class/enum definitions in PHP and the ability to embed these classes within other classes. +This RFC proposes a new short syntax for class definitions in PHP and the ability to embed these classes within other classes. ===== Proposal ===== @@ -66,24 +66,6 @@ Attributes may also be used with short classes: class Password(#[SensitiveParameter] string $password); -==== Short enums ==== - -Enums are a common pattern in PHP applications and are usually simple data structures that hold a set of constants. This RFC includes a proposal for short enums: - - -enum Color(Red, Green, Blue); - - -This is equivalent to the following full enum definition: - - -enum Color { - case Red; - case Green; - case Blue; -} - - ==== Inner Classes ==== Inner classes are classes that are defined within another class. From 4b9192ba410b21cf55149175ee0146731652a155 Mon Sep 17 00:00:00 2001 From: Robert Landers Date: Sat, 22 Feb 2025 21:18:57 +0100 Subject: [PATCH 07/13] rewrite some sections --- drafts/short-class.md | 278 ++++++++++++++++++++++++----------------- published/support.ptxt | 58 +++++++++ 2 files changed, 223 insertions(+), 113 deletions(-) create mode 100644 published/support.ptxt diff --git a/drafts/short-class.md b/drafts/short-class.md index 96804c8..6f4f926 100644 --- a/drafts/short-class.md +++ b/drafts/short-class.md @@ -4,29 +4,48 @@ * Date: 2025-02-08 * Author: Rob Landers, rob@bottled.codes * Status: Draft (or Under Discussion or Accepted or Declined) -* First Published at: +* First Published at: ## Introduction -This RFC proposes a new short syntax for class definitions in PHP and the ability to embed these classes within other -classes. +PHP has steadily evolved to enhance developer productivity and expressiveness, +introducing new features such as typed properties, constructor property promotion, and first-class callable syntax. +However, defining simple data structures and organizing classes remains verbose. -## Proposal +This RFC proposes two related enhancements to PHP: + +**Short class syntax**, which allows for defining simple or data-oriented classes in a single line: + +```php +class Point(int $x, int $y); +``` -Data transfer objects (DTOs) are a common pattern in PHP applications and are usually simple data structures that hold -data and have no behavior. -With this RFC, -we propose a simple and concise syntax for defining these classes, looking almost like named anonymous classes, as well as -the ability to embed them within other classes. +This syntax serves as a shorthand for defining classes with constructor property promotion, +reducing boilerplate while maintaining clarity. + +**Inner classes**, which allows for the ability to define classes within other classes and control the use of inner classes through visibility: + +```php +class Foo { + public class Bar(public string $message); +} +``` + +## Proposal ### Short Class Syntax The proposed syntax for a short class definition is as follows: a keyword `class`, -followed by the class name, then a list of public properties enclosed in parentheses. +followed by the class name, then a list of properties enclosed in parentheses. Optionally, a list of traits, interfaces, and a parent class may be defined. ```php + +// a simple class with two public properties: x and y class Point(int $x, int $y); + +// a more complex readonly class with a parent class, interface, and traits. +readonly class Vector(int $x, int $y) extends BaseVector implements JsonSerializable use PointTrait, Evolvable; ``` This is equivalent to the following full class definition: @@ -35,13 +54,25 @@ This is equivalent to the following full class definition: class Point { public function __construct(public int $x, public int $y) {} } + +readonly public class Vector extends BaseVector implements JsonSerializable { + use PointTrait, Evolvable; + + public function __construct(public int $x, public int $y) {} +} ``` -Any properties defined within the parenthesis are defined as a public property of the class. +Any properties defined within the parenthesis are defined as a property on the class +and are automatically `public` unless specified otherwise. + +```php +// declare $shapes as a private property +class Geometry(private $shapes) use GeometryTrait; +``` #### Default Values -Default values may be provided for properties: +Default values may be provided for properties but only for properties with type hints: ```php class Point(int $x = 0, int $y = 0); @@ -70,94 +101,154 @@ class Point() extends BasePoint use PointTrait; Attributes may also be used with short classes: ```php +#[MyAttribute] class Password(#[SensitiveParameter] string $password); ``` +#### Modifiers + +Short classes support modifiers such as `readonly`, `final` and `abstract`: + +```php +readonly class User(int $id, string $name); + +final class Config(string $key, mixed $value); + +abstract class Shape(float $area); +``` + +#### How it works + +Short classes are implemented as pure syntax sugar and are compiled as full class definitions. + ### Inner Classes Inner classes are classes that are defined within another class. ```php -class Foo { - class Bar(public string $message); - enum Baz(One, Two, Three); +class Outer { + class Inner(public string $message); - private class Baz { + private class PrivateInner { public function __construct(public string $message) {} } } -$foo = new Foo::Bar('Hello, world!'); +$foo = new Outer::Inner('Hello, world!'); echo $foo->message; // outputs: Hello, world! -$baz = new Foo::Baz('Hello, world!'); -// Fatal error: Uncaught Error: Cannot access private class Foo::Baz +$baz = new Outer::PrivateInner('Hello, world!'); +// Fatal error: Uncaught Error: Cannot access private inner class Outer::PrivateInner ``` -Inner classes have scope similar to properties, which applies to parameters and returns types as well. +Inner classes have inheritance similar to static properties, allowing you to define rich class hierarchies: + +```php +readonly class Point(int $x, int $y); + +class Geometry { + public array $points; + protected function __construct(Point ...$points) { + $this->points = $points; + } + + public class FromPoints extends Geometry { + public function __construct(Point ...$points) { + parent::__construct(...$points); + } + } + + public class FromCoordinates extends Geometry { + public function __construct(int ...$coordinates) { + $points = []; + for ($i = 0; $i < count($coordinates); $i += 2) { + $points[] = new Point($coordinates[$i], $coordinates[$i + 1]); + } + parent::__construct(...$points); + } + } +} + +class Triangle extends Geometry { + protected function __construct(public Point $p1, public Point $p2, public Point $p3) { + parent::__construct($p1, $p2, $p3); + } + + public class FromPoints extends Triangle { + public function __construct(Point $p1, Point $p2, Point $p3) { + parent::__construct($p1, $p2, $p3); + } + } + + public class FromCoordinates extends Triangle { + public function __construct(int $x1, int $y1, int $x2, int $y2, int $x3, int $y3) { + parent::__construct(new Point($x1, $y1), new Point($x2, $y2), new Point($x3, $y3)); + } + } +} + +$t = new Triangle::FromCoordinates(0, 0, 1, 1, 2, 2); + +var_dump($t instanceof Triangle); // true +var_dump($t instanceof Geometry); // true +var_dump($t instanceof Triangle::FromCoordinates); // true +``` #### Modifiers -Properties support modifiers such as `public`, `protected`, and `private` as well as `static`, `final` and `readonly`. +Inner classes support modifiers such as `public`, `protected`, `private`, `final` and `readonly`. When using these as modifiers on an inner class, there are some intuitive rules: -- `public`, `private`, and `protected` apply to the visibility of the class. -- `static`, `final`, and `readonly` apply to the class itself. +- `public`, `private`, and `protected` apply to the visibility of the inner class. +- `final`, and `readonly` apply to the class itself. Thus, an inner class with the modifier `private readonly` is only accessible within the class and any instances are readonly. +`static` is not allowed as a modifier on an inner class because there is currently no such thing as a `static` class in PHP. + #### Visibility -A `private` or `protected` inner class is only accessible within the class it is defined in +A `private` or `protected` inner class type is only accessible within the class it is defined in (or its subclasses in the case of protected classes). -This also applies to methods and return types. +This is similar to C#, so you may return a private type from a public method, +but not use it as a type hint from outside the outer class: ```php -class Foo { - private class Bar(public string $message); +class Outer { + private class PrivateInner(string $message); - // Fatal error: Uncaught Error: Cannot return private class Foo::Bar - public function getMessage(): Bar { - return new Bar('Hello, world!'); + public function getInner(): self::PrivateInner { + return new self::PrivateInner('Hello, world!'); } } -``` -#### Accessing Inner Classes +// using a private inner class from outside the outer class, as a type hint is forbidden +function doSomething(Outer::PrivateInner $inner) { + echo $inner->message; +} -From outside the class, public inner classes may be accessed using the `::` operator: +// this is ok: +$inner = new Outer()->getInner(); -```php -new Foo::Bar('Hello, world!'); +// but this is not: +doSomething($inner); +// Fatal error: Private inner class Outer::Inner cannot be used in the global scope ``` -This may also be used from inside the class or in subclasses at the developer’s discretion. Alternatively, inner classes -may be accessed using `self::` or `static::` from inside the class, or just using the name itself: +Just like with other languages that support inner classes, +it is better to return an interface or a base class from a method instead of exposing a private/protected class. -```php - -private function getBar(): Foo::Bar { - $a = new Bar('Hello, world!'); - $b = new self::Bar('Hello, world!'); - $c = new static::Bar('Hello, world!'); - $d = new Foo::Bar('Hello, world!'); -} +You may also not instantiate a private/protected class from outside the outer class’s scope: +```php +$x = new Outer::PrivateInner(); +// Fatal error: Uncaught Error: Cannot access private inner class Outer::Inner ``` -Note that inner classes effectively "shadow" outer classes of the same name: - -```php -readonly class Vect(int $x, int $y); +#### Inheritance -class Foo { - class Vect(int $x, int $y, int $z); - - // Vect is Foo::Vect not \Vect - public function __construct(public Vect $vect) {} -} -``` +Classes may not inherit from inner classes. Inner classes may inherit from other classes, including the outer class. #### Names @@ -175,97 +266,58 @@ class Foo { static $Bar = 'bar'; class Bar(public string $message); - // Fatal error: Uncaught Error: Cannot redeclare Foo::Bar + // Fatal error: Uncaught Error: Cannot redeclare Foo::$Bar } ``` -These rules are to prevent developer confusion because these instantiations all look similar, -however, the following all result in the same inner class being instantiated: - -```php -new (Foo::Bar); -new (Foo::$Bar); -new Foo::Bar(); -``` - ## Backward Incompatible Changes -Creating a new instance from an existing static member is now allowed: - -```php -class Foo { - public const Bar = 'bar'; -} - -new Foo::Bar(); // previously this is a syntax error, but now results in creating a new "bar" object. -``` +This RFC introduces new syntax and behavior to PHP, which does not conflict with existing syntax. +However, tooling utilizing AST or tokenization may need to be updated to support the new syntax. ## Proposed PHP Version(s) -List the proposed PHP versions that the feature will be included in. Use -relative versions such as "next PHP 8.x" or "next PHP 8.x.y". +This RFC targets the next version of PHP. ## RFC Impact ### To SAPIs -Describe the impact to CLI, Development web server, embedded PHP etc. +None. ### To Existing Extensions -Will existing extensions be affected? +Extensions accepting class names may need to be updated to support `::` in class names. +None were discovered during testing, but it is possible there are extensions that may be affected. ### To Opcache -It is necessary to develop RFC's with opcache in mind, since opcache is -a core extension distributed with PHP. - -Please explain how you have verified your RFC's compatibility with -opcache. - -### New Constants - -Describe any new constants so they can be accurately and comprehensively -explained in the PHP documentation. - -### php.ini Defaults - -If there are any php.ini settings then list: \* hardcoded default values -\* php.ini-development values \* php.ini-production values +Most of the changes are in compilation and AST, so the impact to opcache is minimal. ## Open Issues -Make sure there are no open issues when the vote starts! +Pending discussion. ## Unaffected PHP Functionality -List existing areas/features of PHP that will not be changed by the RFC. - -This helps avoid any ambiguity, shows that you have thought deeply about -the RFC's impact, and helps reduces mail list noise. +There should be no change to existing PHP functionality. ## Future Scope -This section details areas where the feature might be improved in -future, but that are not currently proposed in this RFC. +- inner enums ## Proposed Voting Choices -Include these so readers know where you are heading and can discuss the -proposed voting options. + + + * Yes + * No + + ## Patches and Tests -Links to any external patches and tests go here. - -If there is no patch, make it clear who will create a patch, or whether -a volunteer to help with implementation is needed. - -Make it clear if the patch is intended to be the final patch, or is just -a prototype. - -For changes affecting the core language, you should also provide a patch -for the language specification. +A complete implementation is available [on GitHub](https://github.com/php/php-src/compare/master...bottledcode:php-src:rfc/short-class2?expand=1). ## Implementation diff --git a/published/support.ptxt b/published/support.ptxt new file mode 100644 index 0000000..2b7560a --- /dev/null +++ b/published/support.ptxt @@ -0,0 +1,58 @@ +====== Supporting a transition from V2 to V3 ====== + +===== Introduction ===== + +On July 1, 2025, we aim to shut down V2 forever. With enough of a warning, we can delay the shutdown until December 2025; however, this incurs additional costs to us that we would like to avoid. We need to know this by May 31, 2025. + +There are a number of impacted products and services: + + * white-label apps + * API customers + * in-gym solutions + +===== White-label app options ===== + +Tenants still on V2 have several options available to them: + + - Stop using Funxtion. + - Upsell via SDK and upgrade to V3. + - Purchase our V3 whitelabel app via Leisure Labs. + +==== Option 1: Stop using Funxtion ==== + +Tenants may opt to stop using Funxtion altogether. This is the simplest solution but also the most drastic. + +==== Option 2: Upsell via SDK and upgrade to V3 ==== + +Tenants may opt to migrate to V3 through using our SDK. They will be responsible for their own development and maintenance. + +See the migration section for more details. + +==== Option 3: Purchase our V3 whitelabel app via Leisure Labs ==== + +Tenants may opt to purchase our V3 whitelabel app via Leisure Labs. + +===== API customer options ===== + +API customers have a limited set of options available to them: + + - Stop using Funxtion. + - Manually migrate content to V3 and use our SDK. + +==== Option 1: Stop using Funxtion ==== + +API customers may opt to stop using Funxtion altogether. This is the simplest solution but also the most drastic. + +==== Option 2: Manually migrate content to V3 and use our SDK ==== + +No automated migration options exist for API customers. They must manually migrate their content to V3 and use our SDK. + +===== In-gym solutions ===== + +In-gym solutions use the V2 API and are not compatible with V3. The development team aims to have a solution in-place by July 1, 2025, and expects that it will be a seamless transition by working with Notice Media to provide V3 compatibility and/or other means. + +===== Migration ===== + +Through the migration of GoodLife to V3, we have some basic tooling available to assist with migrations from V2 to V3. However, this still requires a lot of manual work to get it right per-tenant. Thus, if a tenant requests that we migrate their content, please ensure that we bill for the hours spent working on this. + +Alternatively, the tenant can migrate their content themselves through the use of the Portal. From 218f05e7f5ceac33a1e2c98bc809ec7b470e6500 Mon Sep 17 00:00:00 2001 From: Robert Landers Date: Sat, 22 Feb 2025 23:08:46 +0100 Subject: [PATCH 08/13] rephrase things --- drafts/short-class.md | 142 ++++++++++++++++++++---------------------- 1 file changed, 66 insertions(+), 76 deletions(-) diff --git a/drafts/short-class.md b/drafts/short-class.md index 6f4f926..d4b206b 100644 --- a/drafts/short-class.md +++ b/drafts/short-class.md @@ -9,21 +9,21 @@ ## Introduction PHP has steadily evolved to enhance developer productivity and expressiveness, -introducing new features such as typed properties, constructor property promotion, and first-class callable syntax. +introducing features such as typed properties, constructor property promotion, and first-class callable syntax. However, defining simple data structures and organizing classes remains verbose. This RFC proposes two related enhancements to PHP: -**Short class syntax**, which allows for defining simple or data-oriented classes in a single line: +**Short class syntax**, allowing simple or data-oriented classes to be defined in a single line: ```php class Point(int $x, int $y); ``` -This syntax serves as a shorthand for defining classes with constructor property promotion, +This syntax acts as a shorthand for defining classes with constructor property promotion, reducing boilerplate while maintaining clarity. -**Inner classes**, which allows for the ability to define classes within other classes and control the use of inner classes through visibility: +**Inner classes**, enabling the definition of classes within other classes with visibility control: ```php class Foo { @@ -35,16 +35,16 @@ class Foo { ### Short Class Syntax -The proposed syntax for a short class definition is as follows: a keyword `class`, -followed by the class name, then a list of properties enclosed in parentheses. -Optionally, a list of traits, interfaces, and a parent class may be defined. +The proposed syntax for defining a short class consists of the class keyword, +followed by the class name, and a list of properties in parentheses. +Optionally, traits, interfaces, and a parent class can be specified. ```php -// a simple class with two public properties: x and y +// a simple class with two public properties class Point(int $x, int $y); -// a more complex readonly class with a parent class, interface, and traits. +// A readonly class with a parent class, interface, and traits readonly class Vector(int $x, int $y) extends BaseVector implements JsonSerializable use PointTrait, Evolvable; ``` @@ -62,8 +62,8 @@ readonly public class Vector extends BaseVector implements JsonSerializable { } ``` -Any properties defined within the parenthesis are defined as a property on the class -and are automatically `public` unless specified otherwise. +Properties inside parentheses are automatically declared as class properties +and default to public unless explicitly specified: ```php // declare $shapes as a private property @@ -72,7 +72,7 @@ class Geometry(private $shapes) use GeometryTrait; #### Default Values -Default values may be provided for properties but only for properties with type hints: +Properties with type hints may have default values: ```php class Point(int $x = 0, int $y = 0); @@ -80,17 +80,17 @@ class Point(int $x = 0, int $y = 0); #### Inheritance and Behavior -With class short syntax, no behavior may be defined, yet it can still utilize traits, interfaces, and other classes. +Short classes can extend other classes, implement interfaces, +and use traits, but they cannot define additional methods. +The parent class constructor is overridden and not automatically called. ```php class Point(int $x, int $y) extends BasePoint implements JsonSerializable use PointTrait, Evolvable; ``` -Note that the original constructor from any parent class is overridden and not called by the short syntax. - #### Empty Classes -Short classes may also be empty: +Short classes may be empty: ```php class Point() extends BasePoint use PointTrait; @@ -98,7 +98,7 @@ class Point() extends BasePoint use PointTrait; #### Attributes -Attributes may also be used with short classes: +Attributes can be used with short classes: ```php #[MyAttribute] @@ -107,7 +107,7 @@ class Password(#[SensitiveParameter] string $password); #### Modifiers -Short classes support modifiers such as `readonly`, `final` and `abstract`: +Short classes support readonly, final, and abstract: ```php readonly class User(int $id, string $name); @@ -119,11 +119,11 @@ abstract class Shape(float $area); #### How it works -Short classes are implemented as pure syntax sugar and are compiled as full class definitions. +Short classes are purely syntactic sugar and compile into standard class definitions. ### Inner Classes -Inner classes are classes that are defined within another class. +Inner classes allow defining classes within other classes, following visibility rules: ```php class Outer { @@ -141,7 +141,50 @@ $baz = new Outer::PrivateInner('Hello, world!'); // Fatal error: Uncaught Error: Cannot access private inner class Outer::PrivateInner ``` -Inner classes have inheritance similar to static properties, allowing you to define rich class hierarchies: +#### Modifiers + +Inner classes support modifiers such as `public`, `protected`, `private`, `final` and `readonly`. +When using these as modifiers on an inner class, there are some intuitive rules: + +- `public`, `private`, and `protected` apply to the visibility of the inner class. +- `final`, and `readonly` apply to the class itself. +- `static` is not allowed as a modifier since PHP does not support static classes. +- `abstract` is not allowed as an inner class cannot be parent classes. + +#### Visibility Rules + +Private and protected inner classes are only instantiatable within their outer class +(or subclasses for protected) and may not be used as type hints outside of their outer class. + +```php +class Outer { + private class PrivateInner(string $message); + + public function getInner(): self::PrivateInner { + return new self::PrivateInner('Hello, world!'); + } +} + +// using a private inner class from outside the outer class, as a type hint is forbidden +function doSomething(Outer::PrivateInner $inner) { + echo $inner->message; +} + +// this is ok: +$inner = new Outer()->getInner(); + +// but this is not: +doSomething($inner); +// Fatal error: Private inner class Outer::Inner cannot be used in the global scope +``` + +Just like with other languages that support inner classes, +it is better to return an interface or a base class from a method instead of exposing a private/protected class. + +#### Inheritance + +Inner classes have inheritance similar to static properties; +this allows you to redefine an inner class in a subclass, allowing rich hierarchies. ```php readonly class Point(int $x, int $y); @@ -194,61 +237,8 @@ var_dump($t instanceof Geometry); // true var_dump($t instanceof Triangle::FromCoordinates); // true ``` -#### Modifiers - -Inner classes support modifiers such as `public`, `protected`, `private`, `final` and `readonly`. -When using these as modifiers on an inner class, there are some intuitive rules: - -- `public`, `private`, and `protected` apply to the visibility of the inner class. -- `final`, and `readonly` apply to the class itself. - -Thus, an inner class with the modifier `private readonly` is only accessible within the class -and any instances are readonly. - -`static` is not allowed as a modifier on an inner class because there is currently no such thing as a `static` class in PHP. - -#### Visibility - -A `private` or `protected` inner class type is only accessible within the class it is defined in -(or its subclasses in the case of protected classes). -This is similar to C#, so you may return a private type from a public method, -but not use it as a type hint from outside the outer class: - -```php -class Outer { - private class PrivateInner(string $message); - - public function getInner(): self::PrivateInner { - return new self::PrivateInner('Hello, world!'); - } -} - -// using a private inner class from outside the outer class, as a type hint is forbidden -function doSomething(Outer::PrivateInner $inner) { - echo $inner->message; -} - -// this is ok: -$inner = new Outer()->getInner(); - -// but this is not: -doSomething($inner); -// Fatal error: Private inner class Outer::Inner cannot be used in the global scope -``` - -Just like with other languages that support inner classes, -it is better to return an interface or a base class from a method instead of exposing a private/protected class. - -You may also not instantiate a private/protected class from outside the outer class’s scope: - -```php -$x = new Outer::PrivateInner(); -// Fatal error: Uncaught Error: Cannot access private inner class Outer::Inner -``` - -#### Inheritance - -Classes may not inherit from inner classes. Inner classes may inherit from other classes, including the outer class. +However, no classes may not inherit from inner classes, +but inner classes may inherit from other classes, including the outer class. #### Names From fe910ea733affd6ba7bb49eb61128cbe320f8116 Mon Sep 17 00:00:00 2001 From: Robert Landers Date: Sat, 22 Feb 2025 23:10:23 +0100 Subject: [PATCH 09/13] rephrase things --- published/short-class.ptxt | 246 ++++++++++++++++++++++--------------- 1 file changed, 146 insertions(+), 100 deletions(-) diff --git a/published/short-class.ptxt b/published/short-class.ptxt index 48a0e12..516d167 100644 --- a/published/short-class.ptxt +++ b/published/short-class.ptxt @@ -4,22 +4,43 @@ * Date: 2025-02-08 * Author: Rob Landers, * Status: Draft (or Under Discussion or Accepted or Declined) - * First Published at: http://wiki.php.net/rfc/short-class + * First Published at: http://wiki.php.net/rfc/short-and-inner-classes ===== Introduction ===== -This RFC proposes a new short syntax for class definitions in PHP and the ability to embed these classes within other classes. +PHP has steadily evolved to enhance developer productivity and expressiveness, introducing features such as typed properties, constructor property promotion, and first-class callable syntax. However, defining simple data structures and organizing classes remains verbose. -===== Proposal ===== +This RFC proposes two related enhancements to PHP: + +**Short class syntax**, allowing simple or data-oriented classes to be defined in a single line: + + +class Point(int $x, int $y); + -Data transfer objects (DTOs) are a common pattern in PHP applications and are usually simple data structures that hold data and have no behavior. With this RFC, we propose a simple and concise syntax for defining these classes, looking almost like named anonymous classes, as well as the ability to embed them within other classes. +This syntax acts as a shorthand for defining classes with constructor property promotion, reducing boilerplate while maintaining clarity. + +**Inner classes**, enabling the definition of classes within other classes with visibility control: + + +class Foo { + public class Bar(public string $message); +} + + +===== Proposal ===== ==== Short Class Syntax ==== -The proposed syntax for a short class definition is as follows: a keyword ''%%class%%'', followed by the class name, then a list of public properties enclosed in parentheses. Optionally, a list of traits, interfaces, and a parent class may be defined. +The proposed syntax for defining a short class consists of the class keyword, followed by the class name, and a list of properties in parentheses. Optionally, traits, interfaces, and a parent class can be specified. + +// a simple class with two public properties class Point(int $x, int $y); + +// A readonly class with a parent class, interface, and traits +readonly class Vector(int $x, int $y) extends BaseVector implements JsonSerializable use PointTrait, Evolvable; This is equivalent to the following full class definition: @@ -28,13 +49,24 @@ This is equivalent to the following full class definition: class Point { public function __construct(public int $x, public int $y) {} } + +readonly public class Vector extends BaseVector implements JsonSerializable { + use PointTrait, Evolvable; + + public function __construct(public int $x, public int $y) {} +} -Any properties defined within the parenthesis are defined as a public property of the class. +Properties inside parentheses are automatically declared as class properties and default to public unless explicitly specified: + + +// declare $shapes as a private property +class Geometry(private $shapes) use GeometryTrait; + === Default Values === -Default values may be provided for properties: +Properties with type hints may have default values: class Point(int $x = 0, int $y = 0); @@ -42,17 +74,15 @@ class Point(int $x = 0, int $y = 0); === Inheritance and Behavior === -With class short syntax, no behavior may be defined, yet it can still utilize traits, interfaces, and other classes. +Short classes can extend other classes, implement interfaces, and use traits, but they cannot define additional methods. The parent class constructor is overridden and not automatically called. class Point(int $x, int $y) extends BasePoint implements JsonSerializable use PointTrait, Evolvable; -Note that the original constructor from any parent class is overridden and not called by the short syntax. - === Empty Classes === -Short classes may also be empty: +Short classes may be empty: class Point() extends BasePoint use PointTrait; @@ -60,92 +90,143 @@ class Point() extends BasePoint use PointTrait; === Attributes === -Attributes may also be used with short classes: +Attributes can be used with short classes: +#[MyAttribute] class Password(#[SensitiveParameter] string $password); +=== Modifiers === + +Short classes support readonly, final, and abstract: + + +readonly class User(int $id, string $name); + +final class Config(string $key, mixed $value); + +abstract class Shape(float $area); + + +=== How it works === + +Short classes are purely syntactic sugar and compile into standard class definitions. + ==== Inner Classes ==== -Inner classes are classes that are defined within another class. +Inner classes allow defining classes within other classes, following visibility rules: -class Foo { - class Bar(public string $message); - enum Baz(One, Two, Three); +class Outer { + class Inner(public string $message); - private class Baz { + private class PrivateInner { public function __construct(public string $message) {} } } -$foo = new Foo::Bar('Hello, world!'); +$foo = new Outer::Inner('Hello, world!'); echo $foo->message; // outputs: Hello, world! -$baz = new Foo::Baz('Hello, world!'); -// Fatal error: Uncaught Error: Cannot access private class Foo::Baz +$baz = new Outer::PrivateInner('Hello, world!'); +// Fatal error: Uncaught Error: Cannot access private inner class Outer::PrivateInner -Inner classes have scope similar to properties, which applies to parameters and returns types as well. - === Modifiers === -Properties support modifiers such as ''%%public%%'', ''%%protected%%'', and ''%%private%%'' as well as ''%%static%%'', ''%%final%%'' and ''%%readonly%%''. When using these as modifiers on an inner class, there are some intuitive rules: - - * ''%%public%%'', ''%%private%%'', and ''%%protected%%'' apply to the visibility of the class. - * ''%%static%%'', ''%%final%%'', and ''%%readonly%%'' apply to the class itself. +Inner classes support modifiers such as ''%%public%%'', ''%%protected%%'', ''%%private%%'', ''%%final%%'' and ''%%readonly%%''. When using these as modifiers on an inner class, there are some intuitive rules: -Thus, an inner class with the modifier ''%%private readonly%%'' is only accessible within the class and any instances are readonly. + * ''%%public%%'', ''%%private%%'', and ''%%protected%%'' apply to the visibility of the inner class. + * ''%%final%%'', and ''%%readonly%%'' apply to the class itself. + * ''%%static%%'' is not allowed as a modifier since PHP does not support static classes. + * ''%%abstract%%'' is not allowed as an inner class cannot be parent classes. -=== Visibility === +=== Visibility Rules === -A ''%%private%%'' or ''%%protected%%'' inner class is only accessible within the class it is defined in (or its subclasses in the case of protected classes). This also applies to methods and return types. +Private and protected inner classes are only instantiatable within their outer class (or subclasses for protected) and may not be used as type hints outside of their outer class. -class Foo { - private class Bar(public string $message); +class Outer { + private class PrivateInner(string $message); - // Fatal error: Uncaught Error: Cannot return private class Foo::Bar - public function getMessage(): Bar { - return new Bar('Hello, world!'); + public function getInner(): self::PrivateInner { + return new self::PrivateInner('Hello, world!'); } } - -=== Accessing Inner Classes === +// using a private inner class from outside the outer class, as a type hint is forbidden +function doSomething(Outer::PrivateInner $inner) { + echo $inner->message; +} -From outside the class, public inner classes may be accessed using the ''%%::%%'' operator: +// this is ok: +$inner = new Outer()->getInner(); - -new Foo::Bar('Hello, world!'); +// but this is not: +doSomething($inner); +// Fatal error: Private inner class Outer::Inner cannot be used in the global scope -This may also be used from inside the class or in subclasses at the developer’s discretion. Alternatively, inner classes may be accessed using ''%%self::%%'' or ''%%static::%%'' from inside the class, or just using the name itself: - - +Just like with other languages that support inner classes, it is better to return an interface or a base class from a method instead of exposing a private/protected class. -private function getBar(): Foo::Bar { - $a = new Bar('Hello, world!'); - $b = new self::Bar('Hello, world!'); - $c = new static::Bar('Hello, world!'); - $d = new Foo::Bar('Hello, world!'); -} - +=== Inheritance === -Note that inner classes effectively "shadow" outer classes of the same name: +Inner classes have inheritance similar to static properties; this allows you to redefine an inner class in a subclass, allowing rich hierarchies. -readonly class Vect(int $x, int $y); +readonly class Point(int $x, int $y); -class Foo { - class Vect(int $x, int $y, int $z); +class Geometry { + public array $points; + protected function __construct(Point ...$points) { + $this->points = $points; + } + + public class FromPoints extends Geometry { + public function __construct(Point ...$points) { + parent::__construct(...$points); + } + } + + public class FromCoordinates extends Geometry { + public function __construct(int ...$coordinates) { + $points = []; + for ($i = 0; $i < count($coordinates); $i += 2) { + $points[] = new Point($coordinates[$i], $coordinates[$i + 1]); + } + parent::__construct(...$points); + } + } +} + +class Triangle extends Geometry { + protected function __construct(public Point $p1, public Point $p2, public Point $p3) { + parent::__construct($p1, $p2, $p3); + } + + public class FromPoints extends Triangle { + public function __construct(Point $p1, Point $p2, Point $p3) { + parent::__construct($p1, $p2, $p3); + } + } - // Vect is Foo::Vect not \Vect - public function __construct(public Vect $vect) {} + public class FromCoordinates extends Triangle { + public function __construct(int $x1, int $y1, int $x2, int $y2, int $x3, int $y3) { + parent::__construct(new Point($x1, $y1), new Point($x2, $y2), new Point($x3, $y3)); + } + } } + +$t = new Triangle::FromCoordinates(0, 0, 1, 1, 2, 2); + +var_dump($t instanceof Triangle); // true +var_dump($t instanceof Geometry); // true +var_dump($t instanceof Triangle::FromCoordinates); // true +However, no classes may not inherit from inner classes, but inner classes may inherit from other classes, including the outer class. + === Names === Inner classes may not have any name that conflicts with a constant or static property of the same name. @@ -162,85 +243,50 @@ class Foo { static $Bar = 'bar'; class Bar(public string $message); - // Fatal error: Uncaught Error: Cannot redeclare Foo::Bar + // Fatal error: Uncaught Error: Cannot redeclare Foo::$Bar } -These rules are to prevent developer confusion because these instantiations all look similar, however, the following all result in the same inner class being instantiated: - - -new (Foo::Bar); -new (Foo::$Bar); -new Foo::Bar(); - - ===== Backward Incompatible Changes ===== -Creating a new instance from an existing static member is now allowed: - - -class Foo { - public const Bar = 'bar'; -} - -new Foo::Bar(); // previously this is a syntax error, but now results in creating a new "bar" object. - +This RFC introduces new syntax and behavior to PHP, which does not conflict with existing syntax. However, tooling utilizing AST or tokenization may need to be updated to support the new syntax. ===== Proposed PHP Version(s) ===== -List the proposed PHP versions that the feature will be included in. Use relative versions such as "next PHP 8.x" or "next PHP 8.x.y". +This RFC targets the next version of PHP. ===== RFC Impact ===== ==== To SAPIs ==== -Describe the impact to CLI, Development web server, embedded PHP etc. +None. ==== To Existing Extensions ==== -Will existing extensions be affected? +Extensions accepting class names may need to be updated to support ''%%::%%'' in class names. None were discovered during testing, but it is possible there are extensions that may be affected. ==== To Opcache ==== -It is necessary to develop RFC's with opcache in mind, since opcache is a core extension distributed with PHP. - -Please explain how you have verified your RFC's compatibility with opcache. - -==== New Constants ==== - -Describe any new constants so they can be accurately and comprehensively explained in the PHP documentation. - -==== php.ini Defaults ==== - -If there are any php.ini settings then list: * hardcoded default values * php.ini-development values * php.ini-production values +Most of the changes are in compilation and AST, so the impact to opcache is minimal. ===== Open Issues ===== -Make sure there are no open issues when the vote starts! +Pending discussion. ===== Unaffected PHP Functionality ===== -List existing areas/features of PHP that will not be changed by the RFC. - -This helps avoid any ambiguity, shows that you have thought deeply about the RFC's impact, and helps reduces mail list noise. +There should be no change to existing PHP functionality. ===== Future Scope ===== -This section details areas where the feature might be improved in future, but that are not currently proposed in this RFC. + * inner enums ===== Proposed Voting Choices ===== -Include these so readers know where you are heading and can discuss the proposed voting options. ===== Patches and Tests ===== -Links to any external patches and tests go here. - -If there is no patch, make it clear who will create a patch, or whether a volunteer to help with implementation is needed. - -Make it clear if the patch is intended to be the final patch, or is just a prototype. - -For changes affecting the core language, you should also provide a patch for the language specification. +A complete implementation is available [[https://github.com/php/php-src/compare/master...bottledcode:php-src:rfc/short-class2?expand=1|on GitHub]]. ===== Implementation ===== From 83d7835f29ab569a286015d6e34d9ac44014ce23 Mon Sep 17 00:00:00 2001 From: Robert Landers Date: Fri, 14 Mar 2025 18:58:41 +0100 Subject: [PATCH 10/13] update as is on the website --- drafts/short-class.md | 487 ++++++++++++++++++++++++------------- published/short-class.ptxt | 414 +++++++++++++++++++------------ src/convert-from-md.sh | 2 +- 3 files changed, 585 insertions(+), 318 deletions(-) diff --git a/drafts/short-class.md b/drafts/short-class.md index d4b206b..53265cc 100644 --- a/drafts/short-class.md +++ b/drafts/short-class.md @@ -1,269 +1,417 @@ -# PHP RFC: Inner Classes with Short Syntax +# PHP RFC: Inner Classes * Version: 0.1 * Date: 2025-02-08 * Author: Rob Landers, rob@bottled.codes -* Status: Draft (or Under Discussion or Accepted or Declined) +* Status: Under Discussion (Accepted or Declined) * First Published at: ## Introduction -PHP has steadily evolved to enhance developer productivity and expressiveness, -introducing features such as typed properties, constructor property promotion, and first-class callable syntax. -However, defining simple data structures and organizing classes remains verbose. +This RFC proposes a significant enhancement to the language: **Inner Classes**. +Inner classes enable the definition of classes within other classes, +introducing a new level of encapsulation and organization within PHP applications. -This RFC proposes two related enhancements to PHP: +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. -**Short class syntax**, allowing simple or data-oriented classes to be defined in a single line: - -```php -class Point(int $x, int $y); -``` - -This syntax acts as a shorthand for defining classes with constructor property promotion, -reducing boilerplate while maintaining clarity. +## Proposal -**Inner classes**, enabling the definition of classes within other classes with visibility control: +Inner classes allow defining classes within other classes, following standard visibility rules. +This allows developers to declare a class as `private` or `protected` and restrict its usage to the outer class. +They are accessed via a new operator: `:>` which is a mixture of `->` and `::`. ```php -class Foo { - public class Bar(public string $message); +class Outer { + public class Inner { + public function __construct(public string $message) {} + } + + private class PrivateInner { + public function __construct(public string $message) {} + } } -``` -## Proposal +$foo = new Outer:>Inner('Hello, world!'); +echo $foo->message; +// outputs: Hello, world! +$baz = new Outer:>PrivateInner('Hello, world!'); +// Fatal error: Class 'Outer:>PrivateInner' is private +``` -### Short Class Syntax +### Modifiers -The proposed syntax for defining a short class consists of the class keyword, -followed by the class name, and a list of properties in parentheses. -Optionally, traits, interfaces, and a parent class can be specified. +Inner classes support modifiers such as `public`, `protected`, `private`, `final`, `readonly`, and `abstract`. +When using these as modifiers on an inner class, there are some intuitive rules: -```php +- `public`, `private`, and `protected` apply to the **visibility** of the inner class. +- `final`, `readonly`, and `abstract` apply **to the inner class itself**. +- `static` is **not allowed** as a modifier since PHP does not support static classes and inner classes are not a + property. -// a simple class with two public properties -class Point(int $x, int $y); +If an inner class does not have any modifiers defined, it is assumed to be `public` by default. -// A readonly class with a parent class, interface, and traits -readonly class Vector(int $x, int $y) extends BaseVector implements JsonSerializable use PointTrait, Evolvable; -``` +### Binding -This is equivalent to the following full class definition: +Inner classes are **strongly** bound to their outer class. +This means that if you extend an outer class and want to "redefine" an inner class, +the child’s inner class is distinct. +The following example best shows this: ```php -class Point { - public function __construct(public int $x, public int $y) {} +class Outer { + protected class Inner {} } -readonly public class Vector extends BaseVector implements JsonSerializable { - use PointTrait, Evolvable; - - public function __construct(public int $x, public int $y) {} +class OuterV2 extends Outer { + protected class Inner {} } ``` -Properties inside parentheses are automatically declared as class properties -and default to public unless explicitly specified: +In the above listing, `OuterV2:>Inner` is a distinct class from `Outer:>Inner`. + +#### static resolution + +The `static` keyword is **not** supported to resolve inner classes. +Attempting to do so results in an error, depending on where it is used: ```php -// declare $shapes as a private property -class Geometry(private $shapes) use GeometryTrait; +class Outer { + class Inner {} + + // Fatal error: Cannot use the static modifier on a parameter + public function foo(static:>Inner $bar) {} + + // Parse error: syntax error, unexpected token ":>", expecting ";" or "{" + public function bar(): static:>Inner {} + + // Fatal error: Cannot use "static" as class name, as it is reserved + class Baz extends static:>Inner {} + + public function foobar() { + return new static:>Inner(); // Fatal error: Cannot use the static modifier on an inner class + } +} ``` -#### Default Values +This is to prevent casual LSP violations of inheritance and to maintain the strong binding of inner classes. + +#### parent resolution -Properties with type hints may have default values: +The `parent` keyword is supported to resolve inner classes. Which parent it resolves to depends on the context: ```php -class Point(int $x = 0, int $y = 0); +class Foo { + class Bar {} +} + +class Baz extends Foo { + // parent:>Bar resolves to Foo:>Bar + class Bar extends parent:>Bar { + // inside the class body, parent refers to Foo:>Bar + public function doSomething(): parent {} + } +} ``` -#### Inheritance and Behavior +`parent` explicitly resolves to the parent class of the current class body it is written in and helps with writing more +concise code. -Short classes can extend other classes, implement interfaces, -and use traits, but they cannot define additional methods. -The parent class constructor is overridden and not automatically called. +#### self resolution + +The `self` keyword is supported to resolve inner classes. Which `self` it resolves to depends on the context: ```php -class Point(int $x, int $y) extends BasePoint implements JsonSerializable use PointTrait, Evolvable; +class Outer { + class Middle { + class Other {} + + // extends Outer:>Middle:>Other + class Inner extends self:>Other { + public function foo(): self {} // returns Outer:>Middle:>Inner + } + } +} ``` -#### Empty Classes - -Short classes may be empty: +`self` explicitly resolves to the current class body it is written in, just like with `parent`. +On `inner` classes, `self` may be used as a standalone keyword to refer to the current outer class in `extends`. ```php -class Point() extends BasePoint use PointTrait; +// this is currently an error +class Outer extends self { + // this extends Outer + class Inner extends self {} +} ``` -#### Attributes - -Attributes can be used with short classes: +When using self inside a class body to refer to an inner class, +if the inner class is not found in the current class, it will fail with an error. ```php -#[MyAttribute] -class Password(#[SensitiveParameter] string $password); +class OuterParent { + class Inner {} + class Other {} +} + +class MiddleChild extends OuterParent { + // Fatal Error: cannot find class MiddleChild:>InnerParent + class Inner extends self:>InnerParent {} +} + +class OuterChild extends OuterParent { + class Inner {} + public function foo() { + $inner = new self:>Inner(); // resolves to OuterChild:>Inner + $inner = new parent:>Inner(); // resolves to OuterParent:>Inner + $inner = new self:>Other(); // Fatal Error: cannot find class OuterChild:>Other + $inner = new parent:>Other(); // resolves to OuterParent:>Other + } +} ``` -#### Modifiers +#### Dynamic resolution -Short classes support readonly, final, and abstract: +Just as with `::`, developers may use variables to resolve inner classes, +or refer to them by name directly via a string: ```php -readonly class User(int $id, string $name); - -final class Config(string $key, mixed $value); +new $outer:>$inner -abstract class Shape(float $area); +$dynamic = "Outer:>Inner"; +new $dynamic(); ``` -#### How it works +This provides flexibility and backwards compatibility for dynamic code that may not expect an inner class. -Short classes are purely syntactic sugar and compile into standard class definitions. +### Visibility Rules -### Inner Classes +Inner classes follow the same visibility rules as properties and methods. +This means that a class extending a public inner class may be declared as private or protected, +but a class extending another inner class may not increase the visibility of its own parent. -Inner classes allow defining classes within other classes, following visibility rules: +#### Instantiation + +Private and protected inner classes are only instantiatable within their outer class +(or subclasses for protected). +Since inner classes are inside outer classes, +they can instantiate other private, protected, or public classes of the outer class if it is visible to them. ```php class Outer { - class Inner(public string $message); - - private class PrivateInner { - public function __construct(public string $message) {} + private class Other {} + protected class Inner { + public function Foo() { + $bar = new self(); // allowed + $bar = new Outer:>Inner(); // allowed + $bar = new Outer:>Other(); // allowed + } } } -$foo = new Outer::Inner('Hello, world!'); -echo $foo->message; -// outputs: Hello, world! -$baz = new Outer::PrivateInner('Hello, world!'); -// Fatal error: Uncaught Error: Cannot access private inner class Outer::PrivateInner +class SubOuter extends Outer { + public function Foo() { + $bar = new self:>Inner(); // allowed to access protected inner class + $bar = new self:>Other(); // Fatal error: Class 'Outer:>Other' is private + } +} ``` -#### Modifiers +Attempting to instantiate a private or protected inner class outside its outer class will result in a fatal error: -Inner classes support modifiers such as `public`, `protected`, `private`, `final` and `readonly`. -When using these as modifiers on an inner class, there are some intuitive rules: +```php +new Outer:>Inner(); // Fatal error: Class 'Outer:>Inner' is private +``` -- `public`, `private`, and `protected` apply to the visibility of the inner class. -- `final`, and `readonly` apply to the class itself. -- `static` is not allowed as a modifier since PHP does not support static classes. -- `abstract` is not allowed as an inner class cannot be parent classes. +#### Method return type and argument declarations -#### Visibility Rules +Inner classes may only be used as a return type or argument declarations for methods +that have the same visibility or lesser. +Thus returning a `protected` class type from a `public` method is not allowed, +but is allowed from a `protected` or `private` method. -Private and protected inner classes are only instantiatable within their outer class -(or subclasses for protected) and may not be used as type hints outside of their outer class. +| Inner Class Visibility | Method Visibility | Allowed | +|------------------------|-------------------|---------| +| `public` | `public` | Yes | +| `public` | `protected` | Yes | +| `public` | `private` | Yes | +| `protected` | `public` | No | +| `protected` | `protected` | Yes | +| `protected` | `private` | Yes | +| `private` | `public` | No | +| `private` | `protected` | No | +| `private` | `private` | Yes | + +Methods and functions outside the outer class are considered `public` by default. +Attempting to declare a return of a non-visible type will result in a `TypeError`: ```php class Outer { - private class PrivateInner(string $message); + private class Inner {} - public function getInner(): self::PrivateInner { - return new self::PrivateInner('Hello, world!'); + public function getInner(): self:>Inner { + return new self:>Inner(); } } -// using a private inner class from outside the outer class, as a type hint is forbidden -function doSomething(Outer::PrivateInner $inner) { - echo $inner->message; -} - -// this is ok: -$inner = new Outer()->getInner(); - -// but this is not: -doSomething($inner); -// Fatal error: Private inner class Outer::Inner cannot be used in the global scope +// Fatal error: Uncaught TypeError: Method getInner is public but returns a private class: Outer:>Inner +new Outer()->getInner(); ``` -Just like with other languages that support inner classes, -it is better to return an interface or a base class from a method instead of exposing a private/protected class. +#### Properties -#### Inheritance +The visibility of a type declaration on a property must also match the declared type. +Thus, a public property cannot declare a private type. -Inner classes have inheritance similar to static properties; -this allows you to redefine an inner class in a subclass, allowing rich hierarchies. +This gives a great deal of control to developers, preventing accidental misuse of inner classes. +However, this **does not** preclude developers from returning a private/protected inner class, +only from using them as a type declaration. +The developer can use an interface or abstract class type declaration, +or use a broader type such as `object`, `mixed`, or nothing at all: ```php -readonly class Point(int $x, int $y); - -class Geometry { - public array $points; - protected function __construct(Point ...$points) { - $this->points = $points; - } +class Outer { + private class Inner implements FooBar {} - public class FromPoints extends Geometry { - public function __construct(Point ...$points) { - parent::__construct(...$points); - } - } - - public class FromCoordinates extends Geometry { - public function __construct(int ...$coordinates) { - $points = []; - for ($i = 0; $i < count($coordinates); $i += 2) { - $points[] = new Point($coordinates[$i], $coordinates[$i + 1]); - } - parent::__construct(...$points); - } + public function getInner(): FooBar { + return new self:>Inner(); // not an error } } +``` -class Triangle extends Geometry { - protected function __construct(public Point $p1, public Point $p2, public Point $p3) { - parent::__construct($p1, $p2, $p3); - } - - public class FromPoints extends Triangle { - public function __construct(Point $p1, Point $p2, Point $p3) { - parent::__construct($p1, $p2, $p3); +#### Accessing outer classes + +There is no direct access to outer class properties or methods, such as an `outer` keyword. +While each class is distinct from the others, +inner classes may access outer class private/protected methods and properties if given an instance, +and outer classes may access private/protected methods of inner classes. + +```php +class Parser { + public class ParseError extends Exception { + private function __construct(Parser $parser) { + // we can access $parser->state here + parent::__construct(/* ... */); } } - public class FromCoordinates extends Triangle { - public function __construct(int $x1, int $y1, int $x2, int $y2, int $x3, int $y3) { - parent::__construct(new Point($x1, $y1), new Point($x2, $y2), new Point($x3, $y3)); - } + private class ParserState {} + + private self:>ParserState $state; + + private function throwParserError(): never { + throw new self:>ParseError($this); } } +``` + +This is allowed because inner classes are strongly bound to their outer class, +and inner classes are considered as members of their outer class. +This allows for better encapsulation and organization of code, especially in the realm of helper classes and DTOs. -$t = new Triangle::FromCoordinates(0, 0, 1, 1, 2, 2); +### Reflection -var_dump($t instanceof Triangle); // true -var_dump($t instanceof Geometry); // true -var_dump($t instanceof Triangle::FromCoordinates); // true +Several new methods are added to `ReflectionClass` to help support inspection of inner classes: + +```php +$reflection = new ReflectionClass('\a\namespace\Outer:>Inner'); + +$reflection->isInnerClass(); // true +$reflection->isPublic() || $reflection->isProtected() || $reflection->isPrivate(); // true +$reflection->getName(); // \a\namespace\Outer:>Inner +$reflection->getShortName(); // Outer:>Inner ``` -However, no classes may not inherit from inner classes, -but inner classes may inherit from other classes, including the outer class. +When these methods are called on outer classes, `isPublic` is always `true` and `isInnerClass` is always `false`. + +### Autoloading + +Inner classes are never autoloaded, only their outermost class is autoloaded. +If the outermost class does not exist, then their inner classes do not exist. + +### Inner Class Features + +Inner classes support all features of regular classes, including: + +- Properties: both static and instanced +- Methods: both static and instanced +- Property hooks +- Magic methods +- Traits +- Interfaces +- Abstract classes +- Final classes +- Readonly classes +- Class constants + +### Usage + +Inner classes may be defined in the following structures: + +- in a class body +- in an anonymous class body + +They explicitly cannot be declared inside an interface, which is in the realm of inner interfaces (see: future scope). -#### Names +There was also a consideration to use them in traits, but this was deemed out of scope for this RFC. +There are some challenges with using traits that contain inner classes, +which need to be addressed in a future RFC. -Inner classes may not have any name that conflicts with a constant or static property of the same name. +Enums are not classes but are class-like and will be addressed in a future RFC. + +### Outer class effects + +Outer class declarations do not affect inner classes. +It is worth going over some common class formations. +For example, what happens when an `abstract class` contains a non-abstract inner class? +What if a `readonly class` contains a non-readonly inner class? + +It’s important to remember +that inner classes are distinct from outer classes and other inner classes and are tightly bound to their outer class. +This means that an abstract outer class need not only define abstract inner classes. +This also means that a final outer class need not only define final inner classes, +or a readonly outer class only readonly inner classes. +Since the inner class is fully distinct from the outer class, it can be quite flexible. + +For example, +a readonly class may contain mutable inner classes +to assist in its implementation while providing an immutable outward API. + +This is very similar to other languages that support inner classes, such as Java or C#. + +### Abstract inner classes + +It is worth exploring what an `abstract` inner class means and how it works. +Abstract inner classes are allowed to be the parent of any class that can see them. +For example, a private abstract class may only be inherited by subclasses in the same outer class. +A protected abstract class may be inherited by an inner class in a subclass of the outer class or an inner class in the same outer class. +However, this is not required by subclasses of the outer class. + +Abstract inner classes may not be instantiated, just as abstract outer classes may not be instantiated. ```php -class Foo { - const Bar = 'bar'; - class Bar(public string $message); - - // Fatal error: Uncaught Error: Cannot redeclare Foo::Bar +class OuterParent { + protected abstract class Inner {} } -class Foo { - static $Bar = 'bar'; - class Bar(public string $message); - - // Fatal error: Uncaught Error: Cannot redeclare Foo::$Bar +// Middle is not required to also implement OuterParent:>Inner +class Middle extends OuterParent {} + +class Last extends OuterParent { + // OuterParent:>Inner is distinct from Last:>Inner but can "see" OuterParent:>Inner and extend it. + private abstract class Inner extends OuterParent:>Inner {} } ``` ## Backward Incompatible Changes -This RFC introduces new syntax and behavior to PHP, which does not conflict with existing syntax. -However, tooling utilizing AST or tokenization may need to be updated to support the new syntax. +- This RFC introduces new syntax and behavior to PHP, which does not conflict with existing syntax. +- Some error messages will be updated to reflect inner classes, and tests that depend on these error messages are likely + to fail. +- Tooling using AST or tokenization may need to be updated to support the new syntax. ## Proposed PHP Version(s) @@ -277,12 +425,13 @@ None. ### To Existing Extensions -Extensions accepting class names may need to be updated to support `::` in class names. -None were discovered during testing, but it is possible there are extensions that may be affected. +Extensions accepting class names may need to be updated to support `:>` in class names. +None were discovered during testing, but it is possible there are unbundled extensions that may be affected. ### To Opcache -Most of the changes are in compilation and AST, so the impact to opcache is minimal. +This change introduces a new opcode, AST, and other changes that affect opcache. +These changes are included as part of the PR that implements this feature. ## Open Issues @@ -290,14 +439,19 @@ Pending discussion. ## Unaffected PHP Functionality -There should be no change to existing PHP functionality. +There should be no change to any existing PHP syntax. ## Future Scope -- inner enums +- Inner enums +- Inner interfaces +- Inner traits +- `Outer` keyword for easily accessing outer classes ## Proposed Voting Choices +As this is a significant change to the language, a 2/3 majority is required. + * Yes @@ -307,7 +461,7 @@ There should be no change to existing PHP functionality. ## Patches and Tests -A complete implementation is available [on GitHub](https://github.com/php/php-src/compare/master...bottledcode:php-src:rfc/short-class2?expand=1). +To be completed. ## Implementation @@ -318,8 +472,9 @@ specification section (if any) ## References -Links to external references, discussions or RFCs +- [email that inspired this RFC](https://externals.io/message/125975#125977) +- [RFC: Records](https://wiki.php.net/rfc/records) ## Rejected Features -Keep this updated with features that were discussed on the mail lists. +TBD diff --git a/published/short-class.ptxt b/published/short-class.ptxt index 516d167..dbedcb9 100644 --- a/published/short-class.ptxt +++ b/published/short-class.ptxt @@ -1,255 +1,361 @@ -====== PHP RFC: Inner Classes with Short Syntax ====== +====== PHP RFC: Inner Classes ====== * Version: 0.1 * Date: 2025-02-08 * Author: Rob Landers, - * Status: Draft (or Under Discussion or Accepted or Declined) + * Status: Under Discussion (Accepted or Declined) * First Published at: http://wiki.php.net/rfc/short-and-inner-classes ===== Introduction ===== -PHP has steadily evolved to enhance developer productivity and expressiveness, introducing features such as typed properties, constructor property promotion, and first-class callable syntax. However, defining simple data structures and organizing classes remains verbose. +This RFC proposes a significant enhancement to the language: **Inner Classes**. Inner classes enable the definition of classes within other classes, introducing a new level of encapsulation and organization within PHP applications. -This RFC proposes two related enhancements to PHP: +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. -**Short class syntax**, allowing simple or data-oriented classes to be defined in a single line: - - -class Point(int $x, int $y); - - -This syntax acts as a shorthand for defining classes with constructor property promotion, reducing boilerplate while maintaining clarity. +===== Proposal ===== -**Inner classes**, enabling the definition of classes within other classes with visibility control: +Inner classes allow defining classes within other classes, following standard visibility rules. This allows developers to declare a class as ''%%private%%'' or ''%%protected%%'' and restrict its usage to the outer class. They are accessed via a new operator: ''%%:>%%'' which is a mixture of ''%%->%%'' and ''%%::%%''. -class Foo { - public class Bar(public string $message); +class Outer { + public class Inner { + public function __construct(public string $message) {} + } + + private class PrivateInner { + public function __construct(public string $message) {} + } } - -===== Proposal ===== +$foo = new Outer:>Inner('Hello, world!'); +echo $foo->message; +// outputs: Hello, world! +$baz = new Outer:>PrivateInner('Hello, world!'); +// Fatal error: Class 'Outer:>PrivateInner' is private + -==== Short Class Syntax ==== +==== Modifiers ==== -The proposed syntax for defining a short class consists of the class keyword, followed by the class name, and a list of properties in parentheses. Optionally, traits, interfaces, and a parent class can be specified. +Inner classes support modifiers such as ''%%public%%'', ''%%protected%%'', ''%%private%%'', ''%%final%%'', ''%%readonly%%'', and ''%%abstract%%''. When using these as modifiers on an inner class, there are some intuitive rules: - + * ''%%public%%'', ''%%private%%'', and ''%%protected%%'' apply to the **visibility** of the inner class. + * ''%%final%%'', ''%%readonly%%'', and ''%%abstract%%'' apply **to the inner class itself**. + * ''%%static%%'' is **not allowed** as a modifier since PHP does not support static classes and inner classes are not a property. -// a simple class with two public properties -class Point(int $x, int $y); +If an inner class does not have any modifiers defined, it is assumed to be ''%%public%%'' by default. -// A readonly class with a parent class, interface, and traits -readonly class Vector(int $x, int $y) extends BaseVector implements JsonSerializable use PointTrait, Evolvable; - +==== Binding ==== -This is equivalent to the following full class definition: +Inner classes are **strongly** bound to their outer class. This means that if you extend an outer class and want to "redefine" an inner class, the child’s inner class is distinct. The following example best shows this: -class Point { - public function __construct(public int $x, public int $y) {} +class Outer { + protected class Inner {} } -readonly public class Vector extends BaseVector implements JsonSerializable { - use PointTrait, Evolvable; - - public function __construct(public int $x, public int $y) {} +class OuterV2 extends Outer { + protected class Inner {} } -Properties inside parentheses are automatically declared as class properties and default to public unless explicitly specified: +In the above listing, ''%%OuterV2:>Inner%%'' is a distinct class from ''%%Outer:>Inner%%''. + +=== static resolution === + +The ''%%static%%'' keyword is **not** supported to resolve inner classes. Attempting to do so results in an error, depending on where it is used: -// declare $shapes as a private property -class Geometry(private $shapes) use GeometryTrait; +class Outer { + class Inner {} + + // Fatal error: Cannot use the static modifier on a parameter + public function foo(static:>Inner $bar) {} + + // Parse error: syntax error, unexpected token ":>", expecting ";" or "{" + public function bar(): static:>Inner {} + + // Fatal error: Cannot use "static" as class name, as it is reserved + class Baz extends static:>Inner {} + + public function foobar() { + return new static:>Inner(); // Fatal error: Cannot use the static modifier on an inner class + } +} -=== Default Values === +This is to prevent casual LSP violations of inheritance and to maintain the strong binding of inner classes. + +=== parent resolution === -Properties with type hints may have default values: +The ''%%parent%%'' keyword is supported to resolve inner classes. Which parent it resolves to depends on the context: -class Point(int $x = 0, int $y = 0); +class Foo { + class Bar {} +} + +class Baz extends Foo { + // parent:>Bar resolves to Foo:>Bar + class Bar extends parent:>Bar { + // inside the class body, parent refers to Foo:>Bar + public function doSomething(): parent {} + } +} -=== Inheritance and Behavior === +''%%parent%%'' explicitly resolves to the parent class of the current class body it is written in and helps with writing more concise code. -Short classes can extend other classes, implement interfaces, and use traits, but they cannot define additional methods. The parent class constructor is overridden and not automatically called. +=== self resolution === + +The ''%%self%%'' keyword is supported to resolve inner classes. Which ''%%self%%'' it resolves to depends on the context: -class Point(int $x, int $y) extends BasePoint implements JsonSerializable use PointTrait, Evolvable; +class Outer { + class Middle { + class Other {} + + // extends Outer:>Middle:>Other + class Inner extends self:>Other { + public function foo(): self {} // returns Outer:>Middle:>Inner + } + } +} -=== Empty Classes === - -Short classes may be empty: +''%%self%%'' explicitly resolves to the current class body it is written in, just like with ''%%parent%%''. On ''%%inner%%'' classes, ''%%self%%'' may be used as a standalone keyword to refer to the current outer class in ''%%extends%%''. -class Point() extends BasePoint use PointTrait; +// this is currently an error +class Outer extends self { + // this extends Outer + class Inner extends self {} +} -=== Attributes === - -Attributes can be used with short classes: +When using self inside a class body to refer to an inner class, if the inner class is not found in the current class, it will fail with an error. -#[MyAttribute] -class Password(#[SensitiveParameter] string $password); +class OuterParent { + class Inner {} + class Other {} +} + +class MiddleChild extends OuterParent { + // Fatal Error: cannot find class MiddleChild:>InnerParent + class Inner extends self:>InnerParent {} +} + +class OuterChild extends OuterParent { + class Inner {} + public function foo() { + $inner = new self:>Inner(); // resolves to OuterChild:>Inner + $inner = new parent:>Inner(); // resolves to OuterParent:>Inner + $inner = new self:>Other(); // Fatal Error: cannot find class OuterChild:>Other + $inner = new parent:>Other(); // resolves to OuterParent:>Other + } +} -=== Modifiers === +=== Dynamic resolution === -Short classes support readonly, final, and abstract: +Just as with ''%%::%%'', developers may use variables to resolve inner classes, or refer to them by name directly via a string: -readonly class User(int $id, string $name); - -final class Config(string $key, mixed $value); +new $outer:>$inner -abstract class Shape(float $area); +$dynamic = "Outer:>Inner"; +new $dynamic(); -=== How it works === +This provides flexibility and backwards compatibility for dynamic code that may not expect an inner class. + +==== Visibility Rules ==== -Short classes are purely syntactic sugar and compile into standard class definitions. +Inner classes follow the same visibility rules as properties and methods. This means that a class extending a public inner class may be declared as private or protected, but a class extending another inner class may not increase the visibility of its own parent. -==== Inner Classes ==== +=== Instantiation === -Inner classes allow defining classes within other classes, following visibility rules: +Private and protected inner classes are only instantiatable within their outer class (or subclasses for protected). Since inner classes are inside outer classes, they can instantiate other private, protected, or public classes of the outer class if it is visible to them. class Outer { - class Inner(public string $message); - - private class PrivateInner { - public function __construct(public string $message) {} + private class Other {} + protected class Inner { + public function Foo() { + $bar = new self(); // allowed + $bar = new Outer:>Inner(); // allowed + $bar = new Outer:>Other(); // allowed + } } } -$foo = new Outer::Inner('Hello, world!'); -echo $foo->message; -// outputs: Hello, world! -$baz = new Outer::PrivateInner('Hello, world!'); -// Fatal error: Uncaught Error: Cannot access private inner class Outer::PrivateInner +class SubOuter extends Outer { + public function Foo() { + $bar = new self:>Inner(); // allowed to access protected inner class + $bar = new self:>Other(); // Fatal error: Class 'Outer:>Other' is private + } +} -=== Modifiers === +Attempting to instantiate a private or protected inner class outside its outer class will result in a fatal error: + + +new Outer:>Inner(); // Fatal error: Class 'Outer:>Inner' is private + -Inner classes support modifiers such as ''%%public%%'', ''%%protected%%'', ''%%private%%'', ''%%final%%'' and ''%%readonly%%''. When using these as modifiers on an inner class, there are some intuitive rules: +=== Method return type and argument declarations === - * ''%%public%%'', ''%%private%%'', and ''%%protected%%'' apply to the visibility of the inner class. - * ''%%final%%'', and ''%%readonly%%'' apply to the class itself. - * ''%%static%%'' is not allowed as a modifier since PHP does not support static classes. - * ''%%abstract%%'' is not allowed as an inner class cannot be parent classes. +Inner classes may only be used as a return type or argument declarations for methods that have the same visibility or lesser. Thus returning a ''%%protected%%'' class type from a ''%%public%%'' method is not allowed, but is allowed from a ''%%protected%%'' or ''%%private%%'' method. -=== Visibility Rules === +^Inner Class Visibility^Method Visibility^Allowed^ +|''%%public%%'' |''%%public%%'' |Yes | +|''%%public%%'' |''%%protected%%''|Yes | +|''%%public%%'' |''%%private%%'' |Yes | +|''%%protected%%'' |''%%public%%'' |No | +|''%%protected%%'' |''%%protected%%''|Yes | +|''%%protected%%'' |''%%private%%'' |Yes | +|''%%private%%'' |''%%public%%'' |No | +|''%%private%%'' |''%%protected%%''|No | +|''%%private%%'' |''%%private%%'' |Yes | -Private and protected inner classes are only instantiatable within their outer class (or subclasses for protected) and may not be used as type hints outside of their outer class. +Methods and functions outside the outer class are considered ''%%public%%'' by default. Attempting to declare a return of a non-visible type will result in a ''%%TypeError%%'': class Outer { - private class PrivateInner(string $message); + private class Inner {} - public function getInner(): self::PrivateInner { - return new self::PrivateInner('Hello, world!'); + public function getInner(): self:>Inner { + return new self:>Inner(); } } -// using a private inner class from outside the outer class, as a type hint is forbidden -function doSomething(Outer::PrivateInner $inner) { - echo $inner->message; -} - -// this is ok: -$inner = new Outer()->getInner(); - -// but this is not: -doSomething($inner); -// Fatal error: Private inner class Outer::Inner cannot be used in the global scope +// Fatal error: Uncaught TypeError: Method getInner is public but returns a private class: Outer:>Inner +new Outer()->getInner(); -Just like with other languages that support inner classes, it is better to return an interface or a base class from a method instead of exposing a private/protected class. +=== Properties === -=== Inheritance === +The visibility of a type declaration on a property must also match the declared type. Thus, a public property cannot declare a private type. -Inner classes have inheritance similar to static properties; this allows you to redefine an inner class in a subclass, allowing rich hierarchies. +This gives a great deal of control to developers, preventing accidental misuse of inner classes. However, this **does not** preclude developers from returning a private/protected inner class, only from using them as a type declaration. The developer can use an interface or abstract class type declaration, or use a broader type such as ''%%object%%'', ''%%mixed%%'', or nothing at all: -readonly class Point(int $x, int $y); - -class Geometry { - public array $points; - protected function __construct(Point ...$points) { - $this->points = $points; - } - - public class FromPoints extends Geometry { - public function __construct(Point ...$points) { - parent::__construct(...$points); - } - } +class Outer { + private class Inner implements FooBar {} - public class FromCoordinates extends Geometry { - public function __construct(int ...$coordinates) { - $points = []; - for ($i = 0; $i < count($coordinates); $i += 2) { - $points[] = new Point($coordinates[$i], $coordinates[$i + 1]); - } - parent::__construct(...$points); - } + public function getInner(): FooBar { + return new self:>Inner(); // not an error } } + -class Triangle extends Geometry { - protected function __construct(public Point $p1, public Point $p2, public Point $p3) { - parent::__construct($p1, $p2, $p3); - } - - public class FromPoints extends Triangle { - public function __construct(Point $p1, Point $p2, Point $p3) { - parent::__construct($p1, $p2, $p3); +=== Accessing outer classes === + +There is no direct access to outer class properties or methods, such as an ''%%outer%%'' keyword. While each class is distinct from the others, inner classes may access outer class private/protected methods and properties if given an instance, and outer classes may access private/protected methods of inner classes. + + +class Parser { + public class ParseError extends Exception { + private function __construct(Parser $parser) { + // we can access $parser->state here + parent::__construct(/* ... */); } } - public class FromCoordinates extends Triangle { - public function __construct(int $x1, int $y1, int $x2, int $y2, int $x3, int $y3) { - parent::__construct(new Point($x1, $y1), new Point($x2, $y2), new Point($x3, $y3)); - } + private class ParserState {} + + private self:>ParserState $state; + + private function throwParserError(): never { + throw new self:>ParseError($this); } } + -$t = new Triangle::FromCoordinates(0, 0, 1, 1, 2, 2); +This is allowed because inner classes are strongly bound to their outer class, and inner classes are considered as members of their outer class. This allows for better encapsulation and organization of code, especially in the realm of helper classes and DTOs. -var_dump($t instanceof Triangle); // true -var_dump($t instanceof Geometry); // true -var_dump($t instanceof Triangle::FromCoordinates); // true +==== Reflection ==== + +Several new methods are added to ''%%ReflectionClass%%'' to help support inspection of inner classes: + + +$reflection = new ReflectionClass('\a\namespace\Outer:>Inner'); + +$reflection->isInnerClass(); // true +$reflection->isPublic() || $reflection->isProtected() || $reflection->isPrivate(); // true +$reflection->getName(); // \a\namespace\Outer:>Inner +$reflection->getShortName(); // Outer:>Inner -However, no classes may not inherit from inner classes, but inner classes may inherit from other classes, including the outer class. +When these methods are called on outer classes, ''%%isPublic%%'' is always ''%%true%%'' and ''%%isInnerClass%%'' is always ''%%false%%''. + +==== Autoloading ==== + +Inner classes are never autoloaded, only their outermost class is autoloaded. If the outermost class does not exist, then their inner classes do not exist. + +==== Inner Class Features ==== + +Inner classes support all features of regular classes, including: + + * Properties: both static and instanced + * Methods: both static and instanced + * Property hooks + * Magic methods + * Traits + * Interfaces + * Abstract classes + * Final classes + * Readonly classes + * Class constants + +==== Usage ==== + +Inner classes may be defined in the following structures: + + * in a class body + * in an anonymous class body + +They explicitly cannot be declared inside an interface, which is in the realm of inner interfaces (see: future scope). + +There was also a consideration to use them in traits, but this was deemed out of scope for this RFC. There are some challenges with using traits that contain inner classes, which need to be addressed in a future RFC. -=== Names === +Enums are not classes but are class-like and will be addressed in a future RFC. -Inner classes may not have any name that conflicts with a constant or static property of the same name. +==== Outer class effects ==== + +Outer class declarations do not affect inner classes. It is worth going over some common class formations. For example, what happens when an ''%%abstract class%%'' contains a non-abstract inner class? What if a ''%%readonly class%%'' contains a non-readonly inner class? + +It’s important to remember that inner classes are distinct from outer classes and other inner classes and are tightly bound to their outer class. This means that an abstract outer class need not only define abstract inner classes. This also means that a final outer class need not only define final inner classes, or a readonly outer class only readonly inner classes. Since the inner class is fully distinct from the outer class, it can be quite flexible. + +For example, a readonly class may contain mutable inner classes to assist in its implementation while providing an immutable outward API. + +This is very similar to other languages that support inner classes, such as Java or C#. + +==== Abstract inner classes ==== + +It is worth exploring what an ''%%abstract%%'' inner class means and how it works. Abstract inner classes are allowed to be the parent of any class that can see them. For example, a private abstract class may only be inherited by subclasses in the same outer class. A protected abstract class may be inherited by an inner class in a subclass of the outer class or an inner class in the same outer class. However, this is not required by subclasses of the outer class. + +Abstract inner classes may not be instantiated, just as abstract outer classes may not be instantiated. -class Foo { - const Bar = 'bar'; - class Bar(public string $message); - - // Fatal error: Uncaught Error: Cannot redeclare Foo::Bar +class OuterParent { + protected abstract class Inner {} } -class Foo { - static $Bar = 'bar'; - class Bar(public string $message); - - // Fatal error: Uncaught Error: Cannot redeclare Foo::$Bar +// Middle is not required to also implement OuterParent:>Inner +class Middle extends OuterParent {} + +class Last extends OuterParent { + // OuterParent:>Inner is distinct from Last:>Inner but can "see" OuterParent:>Inner and extend it. + private abstract class Inner extends OuterParent:>Inner {} } ===== Backward Incompatible Changes ===== -This RFC introduces new syntax and behavior to PHP, which does not conflict with existing syntax. However, tooling utilizing AST or tokenization may need to be updated to support the new syntax. + * This RFC introduces new syntax and behavior to PHP, which does not conflict with existing syntax. + * Some error messages will be updated to reflect inner classes, and tests that depend on these error messages are likely to fail. + * Tooling using AST or tokenization may need to be updated to support the new syntax. ===== Proposed PHP Version(s) ===== @@ -263,11 +369,11 @@ None. ==== To Existing Extensions ==== -Extensions accepting class names may need to be updated to support ''%%::%%'' in class names. None were discovered during testing, but it is possible there are extensions that may be affected. +Extensions accepting class names may need to be updated to support ''%%:>%%'' in class names. None were discovered during testing, but it is possible there are unbundled extensions that may be affected. ==== To Opcache ==== -Most of the changes are in compilation and AST, so the impact to opcache is minimal. +This change introduces a new opcode, AST, and other changes that affect opcache. These changes are included as part of the PR that implements this feature. ===== Open Issues ===== @@ -275,18 +381,23 @@ Pending discussion. ===== Unaffected PHP Functionality ===== -There should be no change to existing PHP functionality. +There should be no change to any existing PHP syntax. ===== Future Scope ===== - * inner enums + * Inner enums + * Inner interfaces + * Inner traits + * ''%%Outer%%'' keyword for easily accessing outer classes ===== Proposed Voting Choices ===== +As this is a significant change to the language, a 2/3 majority is required. + ===== Patches and Tests ===== -A complete implementation is available [[https://github.com/php/php-src/compare/master...bottledcode:php-src:rfc/short-class2?expand=1|on GitHub]]. +To be completed. ===== Implementation ===== @@ -294,8 +405,9 @@ After the project is implemented, this section should contain - the version(s) i ===== References ===== -Links to external references, discussions or RFCs + * [[https://externals.io/message/125975#125977|email that inspired this RFC]] + * [[https://wiki.php.net/rfc/records|RFC: Records]] ===== Rejected Features ===== -Keep this updated with features that were discussed on the mail lists. +TBD diff --git a/src/convert-from-md.sh b/src/convert-from-md.sh index d98c6c5..5bf4e72 100755 --- a/src/convert-from-md.sh +++ b/src/convert-from-md.sh @@ -1,6 +1,6 @@ #!/bin/sh -docker run -v "$(pwd)":/data --pull always --user "$(id -u)":"$(id -g)" pandoc/latex -t dokuwiki -f gfm "$1" -o "$2" +docker run --rm -v "$(pwd)":/data --pull always --user "$(id -u)":"$(id -g)" pandoc/latex -t dokuwiki -f gfm "$1" -o "$2" # remove all and tags sed -i 's///g' "$2" sed -i 's/<\/HTML>//g' "$2" From 9b46a82726d1eb6dab2879f620a3c396cfb52f93 Mon Sep 17 00:00:00 2001 From: Robert Landers Date: Sat, 15 Mar 2025 11:27:26 +0100 Subject: [PATCH 11/13] reword and clarify text --- drafts/short-class.md | 333 ++++++++++++++++++++++++------------- published/short-class.ptxt | 265 +++++++++++++++++++---------- 2 files changed, 399 insertions(+), 199 deletions(-) diff --git a/drafts/short-class.md b/drafts/short-class.md index 53265cc..f230e5a 100644 --- a/drafts/short-class.md +++ b/drafts/short-class.md @@ -1,6 +1,6 @@ # PHP RFC: Inner Classes -* Version: 0.1 +* Version: 0.4 * Date: 2025-02-08 * Author: Rob Landers, rob@bottled.codes * Status: Under Discussion (Accepted or Declined) @@ -12,11 +12,18 @@ This RFC proposes a significant enhancement to the language: **Inner Classes**. Inner classes enable the definition of classes within other classes, introducing a new level of encapsulation and organization within PHP applications. -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. +**Inner Classes** allows for the ability to define classes within other classes and control the use of inner classes +through visibility. +Many languages allow the encapsulation of behavior through inner classes (sometimes called **nested classes**), +such as Java, C#, and many others. + +PHP developers currently rely heavily on annotations (e.g., @internal) or naming conventions to indicate encapsulation +of Data Transfer Objects (DTOs) and related internal classes. +These approaches offer limited enforcement, causing potential misuse or unintended coupling. + +This RFC introduces Inner Classes to clearly communicate and enforce boundaries around encapsulated classes, +facilitating well-known design patterns (e.g., Builder, DTO, serialization patterns) with native language support. +This reduces boilerplate, enhances clarity, and ensures internal types remain truly internal. ## Proposal @@ -42,36 +49,73 @@ $baz = new Outer:>PrivateInner('Hello, world!'); // Fatal error: Class 'Outer:>PrivateInner' is private ``` -### Modifiers +Inner classes are just regular class definitions with a couple additional features. +That means, +except where otherwise noted, "how would inner classes work in situation X?" +can be answered with "the same as any other class/object." + +### Declaration Syntax + +Declaring an inner class is just like declaring any other class. +You can define `readonly`, `final`, and `abstract` inner classes -- or combinations, for example. +However, when defining an inner class, you may also define it as `private`, `protected`, or `public`. + +If an inner class does not explicitly declare visibility (private, protected, or public), +it is implicitly considered public. +This matches PHP’s existing class behavior for methods and properties. -Inner classes support modifiers such as `public`, `protected`, `private`, `final`, `readonly`, and `abstract`. -When using these as modifiers on an inner class, there are some intuitive rules: +```php +class Outer { + private class Inner {} + protected readonly class ReadOnlyInner {} + public abstract class AbstractInner {} + public final class FinalInner {} +} +``` -- `public`, `private`, and `protected` apply to the **visibility** of the inner class. -- `final`, `readonly`, and `abstract` apply **to the inner class itself**. -- `static` is **not allowed** as a modifier since PHP does not support static classes and inner classes are not a - property. +If an inner class is not defined as `private`, `protected`, or `public`, it is assumed to be `public`. -If an inner class does not have any modifiers defined, it is assumed to be `public` by default. +### Syntax rationale + +The `:>` syntax was selected after evaluating alternatives: + +- Using `::` was cumbersome and visually ambiguous due to existing static resolution syntax. +- Using `\` or `\\` was rejected due to potential conflicts with namespaces and escaping, causing confusion. + +`:>` communicates "inner membership," visually linking the inner class to the outer class. ### Binding -Inner classes are **strongly** bound to their outer class. -This means that if you extend an outer class and want to "redefine" an inner class, -the child’s inner class is distinct. -The following example best shows this: +Inner classes are explicitly bound to their outer classes and are intentionally not inherited through subclassing, +traits, +or interfaces. +This prevents ambiguity or complexity arising from inheriting tightly bound encapsulated logic. ```php class Outer { - protected class Inner {} + class OuterInner {} } -class OuterV2 extends Outer { - protected class Inner {} +interface Foo { + class FooInner {} } -``` -In the above listing, `OuterV2:>Inner` is a distinct class from `Outer:>Inner`. +trait Bar { + class BarInner {} +} + +class All extends Outer implements Foo { + use Bar; + + public function does_not_work() { + new self:>OuterInner(); // Fatal error: Class 'All:>OuterInner' not found + new self:>FooInner(); // Fatal error: Class 'All:>FooInner' not found + new self:>BarInner(); // Fatal error: Class 'All:>BarInner' not found + + new parent:>OuterInner(); // This does work because it resolves to Outer:>OuterInner + } +} +``` #### static resolution @@ -138,15 +182,7 @@ class Outer { ``` `self` explicitly resolves to the current class body it is written in, just like with `parent`. -On `inner` classes, `self` may be used as a standalone keyword to refer to the current outer class in `extends`. - -```php -// this is currently an error -class Outer extends self { - // this extends Outer - class Inner extends self {} -} -``` +On `inner` classes. When using self inside a class body to refer to an inner class, if the inner class is not found in the current class, it will fail with an error. @@ -179,33 +215,66 @@ Just as with `::`, developers may use variables to resolve inner classes, or refer to them by name directly via a string: ```php -new $outer:>$inner +// Using variables to dynamically instantiate inner classes: +$outer = "Outer"; +$inner = "Inner"; +$instance = new $outer:>$inner(); + +// Instantiating inner class dynamically via a fully qualified class string: +$dynamicClassName = "Outer:>Inner"; +$instance = new $dynamicClassName(); -$dynamic = "Outer:>Inner"; -new $dynamic(); ``` This provides flexibility and backwards compatibility for dynamic code that may not expect an inner class. -### Visibility Rules +### Visibility from inner classes + +Inner classes have access to their outer class’s private and protected methods, properties, and inner classes. +However, they do not have access to their siblings’ private and protected methods, properties, and inner classes. + +```php +class User { + public private(set) string $name; + public private(set) string $email; + + private function __construct(self:>Builder $builder) { + $this->name = $builder->name; + $this->email = $builder->email; + } + + public readonly final class Builder { + public function __construct(public private(set) string|null $name = null, public private(set) string|null $email = null) {} + + public function withEmail(string $email): self { + return new self($this->name, $email); + } + + public function withName(string $name): self { + return new self($name, $this->email); + } + + public function build(): User { + return new User($this); + } + } +} + +$user = new User:>Builder()->withName('Rob')->withEmail('rob@bottled.codes')->build(); +``` -Inner classes follow the same visibility rules as properties and methods. -This means that a class extending a public inner class may be declared as private or protected, -but a class extending another inner class may not increase the visibility of its own parent. +This enables usages such as builder patterns and other helper classes to succinctly encapsulate behavior. -#### Instantiation +### Visibility from outside the outer class -Private and protected inner classes are only instantiatable within their outer class -(or subclasses for protected). -Since inner classes are inside outer classes, -they can instantiate other private, protected, or public classes of the outer class if it is visible to them. +Inner classes are not visible outside their outer class unless they are public. +Protected classes may be used and accessed from within their child classes. ```php class Outer { private class Other {} protected class Inner { public function Foo() { - $bar = new self(); // allowed $bar = new Outer:>Inner(); // allowed $bar = new Outer:>Other(); // allowed } @@ -214,8 +283,8 @@ class Outer { class SubOuter extends Outer { public function Foo() { - $bar = new self:>Inner(); // allowed to access protected inner class - $bar = new self:>Other(); // Fatal error: Class 'Outer:>Other' is private + $bar = new parent:>Inner(); // allowed to access protected inner class + $bar = new parent:>Other(); // Cannot access private inner class 'Outer:>Other' } } ``` @@ -223,12 +292,12 @@ class SubOuter extends Outer { Attempting to instantiate a private or protected inner class outside its outer class will result in a fatal error: ```php -new Outer:>Inner(); // Fatal error: Class 'Outer:>Inner' is private +new Outer:>Inner(); // Cannot access protected inner class 'Outer:>Inner' ``` #### Method return type and argument declarations -Inner classes may only be used as a return type or argument declarations for methods +Inner classes may only be used as a return type or argument declarations for methods and functions that have the same visibility or lesser. Thus returning a `protected` class type from a `public` method is not allowed, but is allowed from a `protected` or `private` method. @@ -245,7 +314,7 @@ but is allowed from a `protected` or `private` method. | `private` | `protected` | No | | `private` | `private` | Yes | -Methods and functions outside the outer class are considered `public` by default. +Methods and functions outside the outer class are considered public from the perspective of an inner class. Attempting to declare a return of a non-visible type will result in a `TypeError`: ```php @@ -257,13 +326,13 @@ class Outer { } } -// Fatal error: Uncaught TypeError: Method getInner is public but returns a private class: Outer:>Inner +// Fatal error: Uncaught TypeError: Public method getInner cannot return private class Outer:>Inner new Outer()->getInner(); ``` #### Properties -The visibility of a type declaration on a property must also match the declared type. +The visibility of a type declaration on a property must also not increase the visibility of an inner class. Thus, a public property cannot declare a private type. This gives a great deal of control to developers, preventing accidental misuse of inner classes. @@ -282,36 +351,34 @@ class Outer { } ``` -#### Accessing outer classes +### Accessing outer classes + +Inner classes do not implicitly have access to an outer class instance. +This design choice avoids implicit coupling between classes and keeps inner classes simpler, +facilitating potential future extensions +(such as adding an explicit outer keyword) without backward compatibility issues. -There is no direct access to outer class properties or methods, such as an `outer` keyword. -While each class is distinct from the others, -inner classes may access outer class private/protected methods and properties if given an instance, -and outer classes may access private/protected methods of inner classes. +Passing an outer instance explicitly remains an available option for accessing protected/private methods or properties. +Thus inner classes may access outer class private/protected methods and properties if given an instance. ```php -class Parser { - public class ParseError extends Exception { - private function __construct(Parser $parser) { - // we can access $parser->state here - parent::__construct(/* ... */); - } - } - - private class ParserState {} +class Message { + public function __construct(private string $message, private string $from, private string $to) {} - private self:>ParserState $state; + public function Serialize(): string { + return new Message:>SerializedMessage($this)->message; + } - private function throwParserError(): never { - throw new self:>ParseError($this); + private class SerializedMessage { + public string $message; + + public function __construct(Message $message) { + $this->message = strlen($message->from) . $message->from . strlen($message->to) . $message->to . strlen($message->message) . $message->message; + } } } ``` -This is allowed because inner classes are strongly bound to their outer class, -and inner classes are considered as members of their outer class. -This allows for better encapsulation and organization of code, especially in the realm of helper classes and DTOs. - ### Reflection Several new methods are added to `ReflectionClass` to help support inspection of inner classes: @@ -325,85 +392,116 @@ $reflection->getName(); // \a\namespace\Outer:>Inner $reflection->getShortName(); // Outer:>Inner ``` -When these methods are called on outer classes, `isPublic` is always `true` and `isInnerClass` is always `false`. +For non-inner classes, `ReflectionClass::isInnerClass()` returns false. ### Autoloading Inner classes are never autoloaded, only their outermost class is autoloaded. If the outermost class does not exist, then their inner classes do not exist. -### Inner Class Features - -Inner classes support all features of regular classes, including: - -- Properties: both static and instanced -- Methods: both static and instanced -- Property hooks -- Magic methods -- Traits -- Interfaces -- Abstract classes -- Final classes -- Readonly classes -- Class constants - ### Usage -Inner classes may be defined in the following structures: +Inner classes may be defined in the body of any class-like structure, including but not limited to: - in a class body - in an anonymous class body +- in an enum body +- in a trait body +- in an interface body -They explicitly cannot be declared inside an interface, which is in the realm of inner interfaces (see: future scope). +Note: +While traits and interfaces may define inner classes, +classes using these traits or implementing these interfaces do not inherit their inner classes. +Inner classes remain strictly scoped and bound to their defining context only. -There was also a consideration to use them in traits, but this was deemed out of scope for this RFC. -There are some challenges with using traits that contain inner classes, -which need to be addressed in a future RFC. +### Outer class effects -Enums are not classes but are class-like and will be addressed in a future RFC. +Inner classes are fully independent of their outer class’s declaration modifiers. +Outer class modifiers such as abstract, final, +or readonly do not implicitly cascade down or affect the inner classes defined within them. -### Outer class effects +Specifically: -Outer class declarations do not affect inner classes. -It is worth going over some common class formations. -For example, what happens when an `abstract class` contains a non-abstract inner class? -What if a `readonly class` contains a non-readonly inner class? +- An abstract outer class can define concrete (non-abstract) inner classes. Inner classes remain instantiable, + independent of the outer class’s abstractness. +- A final outer class does not force its inner classes to be final. Inner classes within a final class can be extended + internally, providing flexibility within encapsulation boundaries. +- A readonly outer class can define mutable inner classes. This supports internal flexibility, allowing the outer class + to maintain immutability in its external API while managing state changes internally via inner classes. -It’s important to remember -that inner classes are distinct from outer classes and other inner classes and are tightly bound to their outer class. -This means that an abstract outer class need not only define abstract inner classes. -This also means that a final outer class need not only define final inner classes, -or a readonly outer class only readonly inner classes. -Since the inner class is fully distinct from the outer class, it can be quite flexible. +Examples: -For example, -a readonly class may contain mutable inner classes -to assist in its implementation while providing an immutable outward API. +```php +abstract class Service { + // Valid: Inner class is not abstract, despite the outer class being abstract. + public class Implementation { + public function run(): void {} + } +} + +// Allowed; abstract outer class does not force inner classes to be abstract. +new Service:>Implementation(); +``` -This is very similar to other languages that support inner classes, such as Java or C#. +```php +readonly class ImmutableCollection { + private array $items; + + public function __construct(array $items) { + $this->items = $items; + } + + public function getMutableBuilder(): self:>Builder { + return new self:>Builder($this->items); + } + + public class Builder { + private array $items; + + public function __construct(array $items) { + $this->items = $items; + } + + public function add(mixed $item): self { + $this->items[] = $item; + return $this; + } + + public function build(): ImmutableCollection { + return new ImmutableCollection($this->items); + } + } +} +``` + +In this example, even though ImmutableBuilder is a mutable inner class within a readonly outer class, +the outer class maintains its immutability externally, +while inner classes help internally with state management or transitional operations. ### Abstract inner classes It is worth exploring what an `abstract` inner class means and how it works. Abstract inner classes are allowed to be the parent of any class that can see them. For example, a private abstract class may only be inherited by subclasses in the same outer class. -A protected abstract class may be inherited by an inner class in a subclass of the outer class or an inner class in the same outer class. +A protected abstract class may be inherited by an inner class in a subclass of the outer class or an inner class in the +same outer class. However, this is not required by subclasses of the outer class. Abstract inner classes may not be instantiated, just as abstract outer classes may not be instantiated. ```php class OuterParent { - protected abstract class Inner {} + protected abstract class InnerBase {} } -// Middle is not required to also implement OuterParent:>Inner +// Middle is allowed, does not have to implement InnerBase class Middle extends OuterParent {} +// Last demonstrates extending the abstract inner class explicitly. class Last extends OuterParent { - // OuterParent:>Inner is distinct from Last:>Inner but can "see" OuterParent:>Inner and extend it. - private abstract class Inner extends OuterParent:>Inner {} + private abstract class InnerExtended extends OuterParent:>InnerBase {} } + ``` ## Backward Incompatible Changes @@ -433,6 +531,11 @@ None were discovered during testing, but it is possible there are unbundled exte This change introduces a new opcode, AST, and other changes that affect opcache. These changes are included as part of the PR that implements this feature. +### To Tooling + +Tooling that uses AST or tokenization may need to be updated to support the new syntax, +such as syntax highlighting, static analysis, and code generation tools. + ## Open Issues Pending discussion. diff --git a/published/short-class.ptxt b/published/short-class.ptxt index dbedcb9..6662175 100644 --- a/published/short-class.ptxt +++ b/published/short-class.ptxt @@ -1,6 +1,6 @@ ====== PHP RFC: Inner Classes ====== - * Version: 0.1 + * Version: 0.4 * Date: 2025-02-08 * Author: Rob Landers, * Status: Under Discussion (Accepted or Declined) @@ -10,7 +10,11 @@ This RFC proposes a significant enhancement to the language: **Inner Classes**. Inner classes enable the definition of classes within other classes, introducing a new level of encapsulation and organization within PHP applications. -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. +**Inner Classes** allows for the ability to define classes within other classes and control the use of inner classes through visibility. Many languages allow the encapsulation of behavior through inner classes (sometimes called **nested classes**), such as Java, C#, and many others. + +PHP developers currently rely heavily on annotations (e.g., @internal) or naming conventions to indicate encapsulation of Data Transfer Objects (DTOs) and related internal classes. These approaches offer limited enforcement, causing potential misuse or unintended coupling. + +This RFC introduces Inner Classes to clearly communicate and enforce boundaries around encapsulated classes, facilitating well-known design patterns (e.g., Builder, DTO, serialization patterns) with native language support. This reduces boilerplate, enhances clarity, and ensures internal types remain truly internal. ===== Proposal ===== @@ -34,31 +38,63 @@ $baz = new Outer:>PrivateInner('Hello, world!'); // Fatal error: Class 'Outer:>PrivateInner' is private -==== Modifiers ==== +Inner classes are just regular class definitions with a couple additional features. That means, except where otherwise noted, "how would inner classes work in situation X?" can be answered with "the same as any other class/object." + +==== Declaration Syntax ==== + +Declaring an inner class is just like declaring any other class. You can define ''%%readonly%%'', ''%%final%%'', and ''%%abstract%%'' inner classes -- or combinations, for example. However, when defining an inner class, you may also define it as ''%%private%%'', ''%%protected%%'', or ''%%public%%''. + +If an inner class does not explicitly declare visibility (private, protected, or public), it is implicitly considered public. This matches PHP’s existing class behavior for methods and properties. -Inner classes support modifiers such as ''%%public%%'', ''%%protected%%'', ''%%private%%'', ''%%final%%'', ''%%readonly%%'', and ''%%abstract%%''. When using these as modifiers on an inner class, there are some intuitive rules: + +class Outer { + private class Inner {} + protected readonly class ReadOnlyInner {} + public abstract class AbstractInner {} + public final class FinalInner {} +} + - * ''%%public%%'', ''%%private%%'', and ''%%protected%%'' apply to the **visibility** of the inner class. - * ''%%final%%'', ''%%readonly%%'', and ''%%abstract%%'' apply **to the inner class itself**. - * ''%%static%%'' is **not allowed** as a modifier since PHP does not support static classes and inner classes are not a property. +If an inner class is not defined as ''%%private%%'', ''%%protected%%'', or ''%%public%%'', it is assumed to be ''%%public%%''. -If an inner class does not have any modifiers defined, it is assumed to be ''%%public%%'' by default. +==== Syntax rationale ==== + +The ''%%:>%%'' syntax was selected after evaluating alternatives: + + * Using ''%%::%%'' was cumbersome and visually ambiguous due to existing static resolution syntax. + * Using ''%%\%%'' or ''%%\\%%'' was rejected due to potential conflicts with namespaces and escaping, causing confusion. + +''%%:>%%'' communicates "inner membership," visually linking the inner class to the outer class. ==== Binding ==== -Inner classes are **strongly** bound to their outer class. This means that if you extend an outer class and want to "redefine" an inner class, the child’s inner class is distinct. The following example best shows this: +Inner classes are explicitly bound to their outer classes and are intentionally not inherited through subclassing, traits, or interfaces. This prevents ambiguity or complexity arising from inheriting tightly bound encapsulated logic. class Outer { - protected class Inner {} + class OuterInner {} } -class OuterV2 extends Outer { - protected class Inner {} +interface Foo { + class FooInner {} } - -In the above listing, ''%%OuterV2:>Inner%%'' is a distinct class from ''%%Outer:>Inner%%''. +trait Bar { + class BarInner {} +} + +class All extends Outer implements Foo { + use Bar; + + public function does_not_work() { + new self:>OuterInner(); // Fatal error: Class 'All:>OuterInner' not found + new self:>FooInner(); // Fatal error: Class 'All:>FooInner' not found + new self:>BarInner(); // Fatal error: Class 'All:>BarInner' not found + + new parent:>OuterInner(); // This does work because it resolves to Outer:>OuterInner + } +} + === static resolution === @@ -122,15 +158,7 @@ class Outer { } -''%%self%%'' explicitly resolves to the current class body it is written in, just like with ''%%parent%%''. On ''%%inner%%'' classes, ''%%self%%'' may be used as a standalone keyword to refer to the current outer class in ''%%extends%%''. - - -// this is currently an error -class Outer extends self { - // this extends Outer - class Inner extends self {} -} - +''%%self%%'' explicitly resolves to the current class body it is written in, just like with ''%%parent%%''. On ''%%inner%%'' classes. When using self inside a class body to refer to an inner class, if the inner class is not found in the current class, it will fail with an error. @@ -161,28 +189,63 @@ class OuterChild extends OuterParent { Just as with ''%%::%%'', developers may use variables to resolve inner classes, or refer to them by name directly via a string: -new $outer:>$inner - -$dynamic = "Outer:>Inner"; -new $dynamic(); +// Using variables to dynamically instantiate inner classes: +$outer = "Outer"; +$inner = "Inner"; +$instance = new $outer:>$inner(); + +// Instantiating inner class dynamically via a fully qualified class string: +$dynamicClassName = "Outer:>Inner"; +$instance = new $dynamicClassName(); This provides flexibility and backwards compatibility for dynamic code that may not expect an inner class. -==== Visibility Rules ==== +==== Visibility from inner classes ==== + +Inner classes have access to their outer class’s private and protected methods, properties, and inner classes. However, they do not have access to their siblings’ private and protected methods, properties, and inner classes. + + +class User { + public private(set) string $name; + public private(set) string $email; + + private function __construct(self:>Builder $builder) { + $this->name = $builder->name; + $this->email = $builder->email; + } + + public readonly final class Builder { + public function __construct(public private(set) string|null $name = null, public private(set) string|null $email = null) {} + + public function withEmail(string $email): self { + return new self($this->name, $email); + } + + public function withName(string $name): self { + return new self($name, $this->email); + } + + public function build(): User { + return new User($this); + } + } +} -Inner classes follow the same visibility rules as properties and methods. This means that a class extending a public inner class may be declared as private or protected, but a class extending another inner class may not increase the visibility of its own parent. +$user = new User:>Builder()->withName('Rob')->withEmail('rob@bottled.codes')->build(); + -=== Instantiation === +This enables usages such as builder patterns and other helper classes to succinctly encapsulate behavior. -Private and protected inner classes are only instantiatable within their outer class (or subclasses for protected). Since inner classes are inside outer classes, they can instantiate other private, protected, or public classes of the outer class if it is visible to them. +==== Visibility from outside the outer class ==== + +Inner classes are not visible outside their outer class unless they are public. Protected classes may be used and accessed from within their child classes. class Outer { private class Other {} protected class Inner { public function Foo() { - $bar = new self(); // allowed $bar = new Outer:>Inner(); // allowed $bar = new Outer:>Other(); // allowed } @@ -191,8 +254,8 @@ class Outer { class SubOuter extends Outer { public function Foo() { - $bar = new self:>Inner(); // allowed to access protected inner class - $bar = new self:>Other(); // Fatal error: Class 'Outer:>Other' is private + $bar = new parent:>Inner(); // allowed to access protected inner class + $bar = new parent:>Other(); // Cannot access private inner class 'Outer:>Other' } } @@ -200,12 +263,12 @@ class SubOuter extends Outer { Attempting to instantiate a private or protected inner class outside its outer class will result in a fatal error: -new Outer:>Inner(); // Fatal error: Class 'Outer:>Inner' is private +new Outer:>Inner(); // Cannot access protected inner class 'Outer:>Inner' === Method return type and argument declarations === -Inner classes may only be used as a return type or argument declarations for methods that have the same visibility or lesser. Thus returning a ''%%protected%%'' class type from a ''%%public%%'' method is not allowed, but is allowed from a ''%%protected%%'' or ''%%private%%'' method. +Inner classes may only be used as a return type or argument declarations for methods and functions that have the same visibility or lesser. Thus returning a ''%%protected%%'' class type from a ''%%public%%'' method is not allowed, but is allowed from a ''%%protected%%'' or ''%%private%%'' method. ^Inner Class Visibility^Method Visibility^Allowed^ |''%%public%%'' |''%%public%%'' |Yes | @@ -218,7 +281,7 @@ Inner classes may only be used as a return type or argument declarations for met |''%%private%%'' |''%%protected%%''|No | |''%%private%%'' |''%%private%%'' |Yes | -Methods and functions outside the outer class are considered ''%%public%%'' by default. Attempting to declare a return of a non-visible type will result in a ''%%TypeError%%'': +Methods and functions outside the outer class are considered public from the perspective of an inner class. Attempting to declare a return of a non-visible type will result in a ''%%TypeError%%'': class Outer { @@ -229,13 +292,13 @@ class Outer { } } -// Fatal error: Uncaught TypeError: Method getInner is public but returns a private class: Outer:>Inner +// Fatal error: Uncaught TypeError: Public method getInner cannot return private class Outer:>Inner new Outer()->getInner(); === Properties === -The visibility of a type declaration on a property must also match the declared type. Thus, a public property cannot declare a private type. +The visibility of a type declaration on a property must also not increase the visibility of an inner class. Thus, a public property cannot declare a private type. This gives a great deal of control to developers, preventing accidental misuse of inner classes. However, this **does not** preclude developers from returning a private/protected inner class, only from using them as a type declaration. The developer can use an interface or abstract class type declaration, or use a broader type such as ''%%object%%'', ''%%mixed%%'', or nothing at all: @@ -249,31 +312,30 @@ class Outer { } -=== Accessing outer classes === +==== Accessing outer classes ==== + +Inner classes do not implicitly have access to an outer class instance. This design choice avoids implicit coupling between classes and keeps inner classes simpler, facilitating potential future extensions (such as adding an explicit outer keyword) without backward compatibility issues. -There is no direct access to outer class properties or methods, such as an ''%%outer%%'' keyword. While each class is distinct from the others, inner classes may access outer class private/protected methods and properties if given an instance, and outer classes may access private/protected methods of inner classes. +Passing an outer instance explicitly remains an available option for accessing protected/private methods or properties. Thus inner classes may access outer class private/protected methods and properties if given an instance. -class Parser { - public class ParseError extends Exception { - private function __construct(Parser $parser) { - // we can access $parser->state here - parent::__construct(/* ... */); - } - } - - private class ParserState {} +class Message { + public function __construct(private string $message, private string $from, private string $to) {} - private self:>ParserState $state; + public function Serialize(): string { + return new Message:>SerializedMessage($this)->message; + } - private function throwParserError(): never { - throw new self:>ParseError($this); + private class SerializedMessage { + public string $message; + + public function __construct(Message $message) { + $this->message = strlen($message->from) . $message->from . strlen($message->to) . $message->to . strlen($message->message) . $message->message; + } } } -This is allowed because inner classes are strongly bound to their outer class, and inner classes are considered as members of their outer class. This allows for better encapsulation and organization of code, especially in the realm of helper classes and DTOs. - ==== Reflection ==== Several new methods are added to ''%%ReflectionClass%%'' to help support inspection of inner classes: @@ -287,49 +349,80 @@ $reflection->getName(); // \a\namespace\Outer:>Inner $reflection->getShortName(); // Outer:>Inner -When these methods are called on outer classes, ''%%isPublic%%'' is always ''%%true%%'' and ''%%isInnerClass%%'' is always ''%%false%%''. +For non-inner classes, ''%%ReflectionClass::isInnerClass()%%'' returns false. ==== Autoloading ==== Inner classes are never autoloaded, only their outermost class is autoloaded. If the outermost class does not exist, then their inner classes do not exist. -==== Inner Class Features ==== - -Inner classes support all features of regular classes, including: - - * Properties: both static and instanced - * Methods: both static and instanced - * Property hooks - * Magic methods - * Traits - * Interfaces - * Abstract classes - * Final classes - * Readonly classes - * Class constants - ==== Usage ==== -Inner classes may be defined in the following structures: +Inner classes may be defined in the body of any class-like structure, including but not limited to: * in a class body * in an anonymous class body + * in an enum body + * in a trait body + * in an interface body -They explicitly cannot be declared inside an interface, which is in the realm of inner interfaces (see: future scope). +Note: While traits and interfaces may define inner classes, classes using these traits or implementing these interfaces do not inherit their inner classes. Inner classes remain strictly scoped and bound to their defining context only. -There was also a consideration to use them in traits, but this was deemed out of scope for this RFC. There are some challenges with using traits that contain inner classes, which need to be addressed in a future RFC. +==== Outer class effects ==== -Enums are not classes but are class-like and will be addressed in a future RFC. +Inner classes are fully independent of their outer class’s declaration modifiers. Outer class modifiers such as abstract, final, or readonly do not implicitly cascade down or affect the inner classes defined within them. -==== Outer class effects ==== +Specifically: + + * An abstract outer class can define concrete (non-abstract) inner classes. Inner classes remain instantiable, independent of the outer class’s abstractness. + * A final outer class does not force its inner classes to be final. Inner classes within a final class can be extended internally, providing flexibility within encapsulation boundaries. + * A readonly outer class can define mutable inner classes. This supports internal flexibility, allowing the outer class to maintain immutability in its external API while managing state changes internally via inner classes. + +Examples: + + +abstract class Service { + // Valid: Inner class is not abstract, despite the outer class being abstract. + public class Implementation { + public function run(): void {} + } +} + +// Allowed; abstract outer class does not force inner classes to be abstract. +new Service:>Implementation(); + + + +readonly class ImmutableCollection { + private array $items; + + public function __construct(array $items) { + $this->items = $items; + } + + public function getMutableBuilder(): self:>Builder { + return new self:>Builder($this->items); + } -Outer class declarations do not affect inner classes. It is worth going over some common class formations. For example, what happens when an ''%%abstract class%%'' contains a non-abstract inner class? What if a ''%%readonly class%%'' contains a non-readonly inner class? + public class Builder { + private array $items; -It’s important to remember that inner classes are distinct from outer classes and other inner classes and are tightly bound to their outer class. This means that an abstract outer class need not only define abstract inner classes. This also means that a final outer class need not only define final inner classes, or a readonly outer class only readonly inner classes. Since the inner class is fully distinct from the outer class, it can be quite flexible. + public function __construct(array $items) { + $this->items = $items; + } -For example, a readonly class may contain mutable inner classes to assist in its implementation while providing an immutable outward API. + public function add(mixed $item): self { + $this->items[] = $item; + return $this; + } -This is very similar to other languages that support inner classes, such as Java or C#. + public function build(): ImmutableCollection { + return new ImmutableCollection($this->items); + } + } +} + + +In this example, even though ImmutableBuilder is a mutable inner class within a readonly outer class, the outer class maintains its immutability externally, while inner classes help internally with state management or transitional operations. ==== Abstract inner classes ==== @@ -339,15 +432,15 @@ Abstract inner classes may not be instantiated, just as abstract outer classes m class OuterParent { - protected abstract class Inner {} + protected abstract class InnerBase {} } -// Middle is not required to also implement OuterParent:>Inner +// Middle is allowed, does not have to implement InnerBase class Middle extends OuterParent {} +// Last demonstrates extending the abstract inner class explicitly. class Last extends OuterParent { - // OuterParent:>Inner is distinct from Last:>Inner but can "see" OuterParent:>Inner and extend it. - private abstract class Inner extends OuterParent:>Inner {} + private abstract class InnerExtended extends OuterParent:>InnerBase {} } @@ -375,6 +468,10 @@ Extensions accepting class names may need to be updated to support ''%%:>%%'' in This change introduces a new opcode, AST, and other changes that affect opcache. These changes are included as part of the PR that implements this feature. +==== To Tooling ==== + +Tooling that uses AST or tokenization may need to be updated to support the new syntax, such as syntax highlighting, static analysis, and code generation tools. + ===== Open Issues ===== Pending discussion. From 6d4deec77708b0a2912ff16374d0b80392ae6883 Mon Sep 17 00:00:00 2001 From: Robert Landers Date: Mon, 31 Mar 2025 08:20:58 +0200 Subject: [PATCH 12/13] rewrite nearly the entire RFC --- drafts/short-class.md | 529 ++++++++++++++++++++---------------------- 1 file changed, 254 insertions(+), 275 deletions(-) diff --git a/drafts/short-class.md b/drafts/short-class.md index f230e5a..3624d2c 100644 --- a/drafts/short-class.md +++ b/drafts/short-class.md @@ -1,6 +1,6 @@ -# PHP RFC: Inner Classes +# PHP RFC: Nested Classes -* Version: 0.4 +* Version: 0.5 * Date: 2025-02-08 * Author: Rob Landers, rob@bottled.codes * Status: Under Discussion (Accepted or Declined) @@ -8,28 +8,20 @@ ## Introduction -This RFC proposes a significant enhancement to the language: **Inner Classes**. -Inner classes enable the definition of classes within other classes, -introducing a new level of encapsulation and organization within PHP applications. - -**Inner Classes** allows for the ability to define classes within other classes and control the use of inner classes -through visibility. -Many languages allow the encapsulation of behavior through inner classes (sometimes called **nested classes**), -such as Java, C#, and many others. - -PHP developers currently rely heavily on annotations (e.g., @internal) or naming conventions to indicate encapsulation -of Data Transfer Objects (DTOs) and related internal classes. -These approaches offer limited enforcement, causing potential misuse or unintended coupling. - -This RFC introduces Inner Classes to clearly communicate and enforce boundaries around encapsulated classes, -facilitating well-known design patterns (e.g., Builder, DTO, serialization patterns) with native language support. -This reduces boilerplate, enhances clarity, and ensures internal types remain truly internal. +This RFC proposes a significant enhancement to the PHP language: **Nested Classes**. +Nested classes allow the definition of one class within another, +enabling tighter encapsulation and better organization of related logic. +This feature naturally supports common design patterns such as Builders, DTOs, and serialization helpers, +reducing boilerplate, minimizing namespace clutter, and clarifying code structure. +Unlike some other languages, PHP nested classes do not implicitly capture the outer class instance, +preserving explicit data flow and minimizing hidden coupling. ## Proposal -Inner classes allow defining classes within other classes, following standard visibility rules. -This allows developers to declare a class as `private` or `protected` and restrict its usage to the outer class. -They are accessed via a new operator: `:>` which is a mixture of `->` and `::`. +Nested classes allow defining classes (including enums) within other classes, +interfaces, or enums, +following standard visibility rules. +This allows developers to declare a class as `private` or `protected` and restrict its usage to an outer class. ```php class Outer { @@ -42,264 +34,198 @@ class Outer { } } -$foo = new Outer:>Inner('Hello, world!'); +$foo = new Outer\Inner('Hello, world!'); echo $foo->message; // outputs: Hello, world! -$baz = new Outer:>PrivateInner('Hello, world!'); -// Fatal error: Class 'Outer:>PrivateInner' is private +$baz = new Outer\PrivateInner('Hello, world!'); +// Uncaught TypeError: Cannot instantiate class Outer\PrivateInner from the global scope ``` -Inner classes are just regular class definitions with a couple additional features. +Nested classes are just regular class definitions with visibility. That means, -except where otherwise noted, "how would inner classes work in situation X?" +except where otherwise noted, "how would nested classes work in situation X?" can be answered with "the same as any other class/object." -### Declaration Syntax +### Use-cases and examples -Declaring an inner class is just like declaring any other class. -You can define `readonly`, `final`, and `abstract` inner classes -- or combinations, for example. -However, when defining an inner class, you may also define it as `private`, `protected`, or `public`. +Nested classes are a powerful tool for organizing code and encapsulating functionality. Some use-cases include: -If an inner class does not explicitly declare visibility (private, protected, or public), -it is implicitly considered public. -This matches PHP’s existing class behavior for methods and properties. +- **Builder patterns**: Nested classes can be used to create complex objects step-by-step, encapsulating the construction logic within the outer class. +- **Data Transfer Objects (DTOs)**: Nested classes can be used to define data structures that are closely related to the outer class, improving code organization and readability. +- **Serialization helpers**: Nested classes can be used to define custom serialization logic for the outer class, encapsulating the serialization details within the outer class. +- **Encapsulation**: Nested classes can be used to encapsulate functionality that is only relevant to the outer class, reducing the visibility of the nested class and improving code organization. +- **Namespace management**: Nested classes can help reduce the number of top-level classes in a namespace, improving code organization and reducing clutter. + +#### Example: Serialization DTOs ```php -class Outer { - private class Inner {} - protected readonly class ReadOnlyInner {} - public abstract class AbstractInner {} - public final class FinalInner {} +class Message { + public function __construct(private string $message, private string $from, private string $to) {} + + public function Serialize(): string { + return serialize(new SerializedMessage($this)); + } + + private class SerializedMessage { + public string $message; + public string $from; + public string $to; + + public function __construct(Message $message) { + $this->message = $message->message; + $this->from = $message->from; + $this->to = $message->to; + } + } } ``` -If an inner class is not defined as `private`, `protected`, or `public`, it is assumed to be `public`. - -### Syntax rationale - -The `:>` syntax was selected after evaluating alternatives: - -- Using `::` was cumbersome and visually ambiguous due to existing static resolution syntax. -- Using `\` or `\\` was rejected due to potential conflicts with namespaces and escaping, causing confusion. - -`:>` communicates "inner membership," visually linking the inner class to the outer class. - -### Binding - -Inner classes are explicitly bound to their outer classes and are intentionally not inherited through subclassing, -traits, -or interfaces. -This prevents ambiguity or complexity arising from inheriting tightly bound encapsulated logic. +#### Example: Builder Pattern ```php -class Outer { - class OuterInner {} -} - -interface Foo { - class FooInner {} -} - -trait Bar { - class BarInner {} -} - -class All extends Outer implements Foo { - use Bar; +class Person { + public class Builder { + use AgeCalculator; - public function does_not_work() { - new self:>OuterInner(); // Fatal error: Class 'All:>OuterInner' not found - new self:>FooInner(); // Fatal error: Class 'All:>FooInner' not found - new self:>BarInner(); // Fatal error: Class 'All:>BarInner' not found + private string $name; + private DateTimeInterface $age; + + public function setName(string $name): self { + $this->name = $name; + return $this; + } - new parent:>OuterInner(); // This does work because it resolves to Outer:>OuterInner + public function setBirthday(DateTimeInterface $age): self { + $this->age = $age; + return $this; + } + + public function build(): Person { + return new Person($this->name, $this->calculateAge($this->age)); + } } + + public function __construct(private string $name, private int $age) {} } ``` -#### static resolution - -The `static` keyword is **not** supported to resolve inner classes. -Attempting to do so results in an error, depending on where it is used: +#### Example: DTOs ```php -class Outer { - class Inner {} - - // Fatal error: Cannot use the static modifier on a parameter - public function foo(static:>Inner $bar) {} - - // Parse error: syntax error, unexpected token ":>", expecting ";" or "{" - public function bar(): static:>Inner {} - - // Fatal error: Cannot use "static" as class name, as it is reserved - class Baz extends static:>Inner {} - - public function foobar() { - return new static:>Inner(); // Fatal error: Cannot use the static modifier on an inner class +enum Status { + case Active; + case Inactive; + + private enum Processing { + case Pending; + case Completed; } -} -``` -This is to prevent casual LSP violations of inheritance and to maintain the strong binding of inner classes. + public class Message { -#### parent resolution + private Processing $processing = Processing::Pending; -The `parent` keyword is supported to resolve inner classes. Which parent it resolves to depends on the context: + public function __construct(private Status $status) {} -```php -class Foo { - class Bar {} -} + public function getMessage(): string { + return match ([$this->status, $this->processing]) { + [Status::Active, Processing::Pending] => "Active and Pending", + [Status::Active, Processing::Completed] => "Active and Completed", + [Status::Inactive, Processing::Pending] => "Inactive and Pending", + [Status::Inactive, Processing::Completed] => throw new LogicException('should not happen'), + }; + } + } -class Baz extends Foo { - // parent:>Bar resolves to Foo:>Bar - class Bar extends parent:>Bar { - // inside the class body, parent refers to Foo:>Bar - public function doSomething(): parent {} + public function createMessage(): Message { + return new Message($this); } } ``` -`parent` explicitly resolves to the parent class of the current class body it is written in and helps with writing more -concise code. - -#### self resolution - -The `self` keyword is supported to resolve inner classes. Which `self` it resolves to depends on the context: +#### Example: class shared via interface ```php -class Outer { - class Middle { - class Other {} - - // extends Outer:>Middle:>Other - class Inner extends self:>Other { - public function foo(): self {} // returns Outer:>Middle:>Inner +interface Reader { + public class Buffer { + public string $bytes; } - } + + public function read(): void; + public function getBuffer(): Buffer; } -``` -`self` explicitly resolves to the current class body it is written in, just like with `parent`. -On `inner` classes. +// in another file: +use Reader\Buffer; -When using self inside a class body to refer to an inner class, -if the inner class is not found in the current class, it will fail with an error. +class FileReader implements Reader { + protected Buffer $buffer; -```php -class OuterParent { - class Inner {} - class Other {} -} + public function __construct() { + $this->buffer = new Buffer(); + } -class MiddleChild extends OuterParent { - // Fatal Error: cannot find class MiddleChild:>InnerParent - class Inner extends self:>InnerParent {} -} + public function read(): void { + $this->buffer->bytes = random_bytes(5); + } -class OuterChild extends OuterParent { - class Inner {} - public function foo() { - $inner = new self:>Inner(); // resolves to OuterChild:>Inner - $inner = new parent:>Inner(); // resolves to OuterParent:>Inner - $inner = new self:>Other(); // Fatal Error: cannot find class OuterChild:>Other - $inner = new parent:>Other(); // resolves to OuterParent:>Other + public function getBuffer(): Buffer { + return $this->buffer; } } ``` -#### Dynamic resolution - -Just as with `::`, developers may use variables to resolve inner classes, -or refer to them by name directly via a string: +### Why PHP needs nested classes -```php -// Using variables to dynamically instantiate inner classes: -$outer = "Outer"; -$inner = "Inner"; -$instance = new $outer:>$inner(); +PHP developers today often simulate nested classes through long namespace chains or anonymous classes +embedded in methods. +While these patterns work, they come with visibility challenges, +verbosity, and a lack of cohesion between related types. +Nested classes solve these problems natively, +offering better encapsulation without requiring new runtime behavior or complex inheritance tricks. -// Instantiating inner class dynamically via a fully qualified class string: -$dynamicClassName = "Outer:>Inner"; -$instance = new $dynamicClassName(); - -``` - -This provides flexibility and backwards compatibility for dynamic code that may not expect an inner class. +### Declaration Syntax -### Visibility from inner classes +Declaring a nested class uses the same syntax as declaring a top-level class. +You can define `readonly`, `final`, and `abstract` nested classes. +In addition, nested classes may be declared with visibility modifiers (`private`, `protected`, `public`), +which determine where the nested class can be referenced or instantiated. -Inner classes have access to their outer class’s private and protected methods, properties, and inner classes. -However, they do not have access to their siblings’ private and protected methods, properties, and inner classes. +If a nested class does not explicitly declare visibility (private, protected, or public), +it is implicitly considered public. +This matches PHP’s existing class behavior for methods and properties. ```php -class User { - public private(set) string $name; - public private(set) string $email; - - private function __construct(self:>Builder $builder) { - $this->name = $builder->name; - $this->email = $builder->email; - } - - public readonly final class Builder { - public function __construct(public private(set) string|null $name = null, public private(set) string|null $email = null) {} - - public function withEmail(string $email): self { - return new self($this->name, $email); - } - - public function withName(string $name): self { - return new self($name, $this->email); - } - - public function build(): User { - return new User($this); - } - } +class Outer { + private class Inner {} + protected readonly class ReadOnlyInner {} + public abstract class AbstractInner {} + public final class FinalInner {} } - -$user = new User:>Builder()->withName('Rob')->withEmail('rob@bottled.codes')->build(); ``` -This enables usages such as builder patterns and other helper classes to succinctly encapsulate behavior. +Outer classes remain "public" (accessible and visible to all), and visibility modifiers cannot be applied to them. -### Visibility from outside the outer class +### Visibility -Inner classes are not visible outside their outer class unless they are public. -Protected classes may be used and accessed from within their child classes. - -```php -class Outer { - private class Other {} - protected class Inner { - public function Foo() { - $bar = new Outer:>Inner(); // allowed - $bar = new Outer:>Other(); // allowed - } - } -} +Visibility refers to whether a method, class, or property is accessible from outside its defining context. +A nested class may be declared as `private`, `protected`, or `public`. +A private class will _only_ be visible within the lexical outer class, +while a protected class is visible both inside its lexical scope and any child classes. +A public class has no restrictions and will be visible from anywhere in the codebase. -class SubOuter extends Outer { - public function Foo() { - $bar = new parent:>Inner(); // allowed to access protected inner class - $bar = new parent:>Other(); // Cannot access private inner class 'Outer:>Other' - } -} -``` +#### Properties and Methods -Attempting to instantiate a private or protected inner class outside its outer class will result in a fatal error: - -```php -new Outer:>Inner(); // Cannot access protected inner class 'Outer:>Inner' -``` +All outer class’s properties and methods are visible from nested classes, +as from the perspective of the nested class, it is a member of the outer class. +However, unlike some languages with nested classes, +an outer class does not have direct access to a nested class’s private or protected members. #### Method return type and argument declarations -Inner classes may only be used as a return type or argument declarations for methods and functions -that have the same visibility or lesser. -Thus returning a `protected` class type from a `public` method is not allowed, +Nested classes may only be used as a return type or argument declarations for methods and functions +that have the same visibility or less. +Thus returning a `protected` type from a `public` method is not allowed, but is allowed from a `protected` or `private` method. | Inner Class Visibility | Method Visibility | Allowed | @@ -314,59 +240,68 @@ but is allowed from a `protected` or `private` method. | `private` | `protected` | No | | `private` | `private` | Yes | -Methods and functions outside the outer class are considered public from the perspective of an inner class. Attempting to declare a return of a non-visible type will result in a `TypeError`: ```php class Outer { private class Inner {} - public function getInner(): self:>Inner { - return new self:>Inner(); + public function getInner(): Inner { + return new Inner(); } } -// Fatal error: Uncaught TypeError: Public method getInner cannot return private class Outer:>Inner +// Fatal error: Uncaught TypeError: Public method getInner cannot return private class Outer\Inner new Outer()->getInner(); ``` +This does not prevent the programmer from returning a hidden type, only from declaring it. +The programmer may choose to use an interface or public type instead: + +```php +interface FooBar {} + +class Outer { + private class Inner implements FooBar {} + + public function getInner(): FooBar { + return new Inner(); + } +} +``` + #### Properties -The visibility of a type declaration on a property must also not increase the visibility of an inner class. +The visibility of a type declaration on a property must also not increase the visibility of a nested class. Thus, a public property cannot declare a private type. -This gives a great deal of control to developers, preventing accidental misuse of inner classes. -However, this **does not** preclude developers from returning a private/protected inner class, -only from using them as a type declaration. -The developer can use an interface or abstract class type declaration, -or use a broader type such as `object`, `mixed`, or nothing at all: - ```php class Outer { private class Inner implements FooBar {} public function getInner(): FooBar { - return new self:>Inner(); // not an error + return new Inner(); // not an error } } ``` ### Accessing outer classes -Inner classes do not implicitly have access to an outer class instance. -This design choice avoids implicit coupling between classes and keeps inner classes simpler, +Nested classes do not implicitly have access to an outer class instance. +This design choice avoids implicit coupling between classes and keeps nested classes simpler, facilitating potential future extensions (such as adding an explicit outer keyword) without backward compatibility issues. -Passing an outer instance explicitly remains an available option for accessing protected/private methods or properties. -Thus inner classes may access outer class private/protected methods and properties if given an instance. +However, a programmer may pass an instance of an outer class (or another nested class) to a nested class. +If a class has access to the outer class, +it may use the outer class’s private, protected, and public properties and methods. ```php class Message { public function __construct(private string $message, private string $from, private string $to) {} public function Serialize(): string { - return new Message:>SerializedMessage($this)->message; + return new SerializedMessage($this)->message; } private class SerializedMessage { @@ -379,68 +314,106 @@ class Message { } ``` +### Resolution + +From a resolution perspective, outer classes behave as a pseudo-namespace. +Using a short name of a nested class will resolve to the nested class. +To disambiguate between different classes with the same name in the namespace and the nested class, +the programmer can use fully qualified names. + +```php +class Inner {} + +abstract class Outer { + class Inner {} + public function foo(Inner $i); // resolves to Outer\Inner + public function bar(\Inner $i); // resolves to global Inner +} +``` + +This applies to deeply nested classes as well: + +```php +class Outer { + class Middle { + class Inner {} + } + public function foo(Middle\Inner $i); // resolves to Outer\Middle\Inner +} +``` + +This can lead to accidentally shadowing a class in the same namespace, +thus requiring the use of `use` statements or fully qualified names to disambiguate. + ### Reflection -Several new methods are added to `ReflectionClass` to help support inspection of inner classes: +Several new methods are added to `ReflectionClass` to help support inspection of nested classes: ```php -$reflection = new ReflectionClass('\a\namespace\Outer:>Inner'); +$reflection = new ReflectionClass('\a\namespace\Outer\Inner'); -$reflection->isInnerClass(); // true +$reflection->isNestedClass(); // true $reflection->isPublic() || $reflection->isProtected() || $reflection->isPrivate(); // true -$reflection->getName(); // \a\namespace\Outer:>Inner -$reflection->getShortName(); // Outer:>Inner +$reflection->getName(); // \a\namespace\Outer\Inner +$reflection->getShortName(); // Inner ``` -For non-inner classes, `ReflectionClass::isInnerClass()` returns false. +For non-nested classes, `ReflectionClass::isNestedClass()` returns `false`. + +A future addition to `ReflectionClass::getNestedClasses()` may allow enumeration of nested class declarations. ### Autoloading -Inner classes are never autoloaded, only their outermost class is autoloaded. -If the outermost class does not exist, then their inner classes do not exist. +Since nested classes are referenced using standard \-separated names, +the engine treats them like any other class name during resolution. +If the outer class has not yet been loaded, PHP will attempt to autoload the nested class name, +which may result in fallback behavior depending on the autoloader. +Implementers are encouraged to structure autoloaders to load the outer class when a nested class is referenced. ### Usage -Inner classes may be defined in the body of any class-like structure, including but not limited to: +Nested classes may be defined in the body of any class-like structure, except traits: - in a class body - in an anonymous class body - in an enum body -- in a trait body -- in an interface body +- in an interface body (public nested classes only) -Note: -While traits and interfaces may define inner classes, -classes using these traits or implementing these interfaces do not inherit their inner classes. -Inner classes remain strictly scoped and bound to their defining context only. +Traits are not allowed to contain nested classes and are left to a future RFC. +Traits are excluded from this RFC +because nested class resolution via use statements introduces ambiguities about scope and visibility. +These concerns are deferred to a future RFC. ### Outer class effects -Inner classes are fully independent of their outer class’s declaration modifiers. +Nested classes are fully independent of their outer class’s declaration modifiers. Outer class modifiers such as abstract, final, -or readonly do not implicitly cascade down or affect the inner classes defined within them. +or readonly do not implicitly cascade down or affect the nested classes defined within them. Specifically: -- An abstract outer class can define concrete (non-abstract) inner classes. Inner classes remain instantiable, +- An abstract outer class can define concrete (non-abstract) nested classes. + Nested classes remain instantiable, independent of the outer class’s abstractness. -- A final outer class does not force its inner classes to be final. Inner classes within a final class can be extended +- A final outer class does not force its nested classes to be final. + Nested classes within a final class can be extended internally, providing flexibility within encapsulation boundaries. -- A readonly outer class can define mutable inner classes. This supports internal flexibility, allowing the outer class - to maintain immutability in its external API while managing state changes internally via inner classes. +- A readonly outer class can define mutable nested classes. + This supports internal flexibility, allowing the outer class + to maintain immutability in its external API while managing state changes internally via nested classes. Examples: ```php abstract class Service { - // Valid: Inner class is not abstract, despite the outer class being abstract. + // Valid: Nested class is not abstract, despite the outer class being abstract. public class Implementation { public function run(): void {} } } -// Allowed; abstract outer class does not force inner classes to be abstract. -new Service:>Implementation(); +// Allowed; abstract outer class does not force nested classes to be abstract. +new Service\Implementation(); ``` ```php @@ -451,8 +424,8 @@ readonly class ImmutableCollection { $this->items = $items; } - public function getMutableBuilder(): self:>Builder { - return new self:>Builder($this->items); + public function getMutableBuilder(): Builder { + return new Builder($this->items); } public class Builder { @@ -474,20 +447,25 @@ readonly class ImmutableCollection { } ``` -In this example, even though ImmutableBuilder is a mutable inner class within a readonly outer class, +In this example, even though `Builder` is a mutable nested class within a readonly outer class, the outer class maintains its immutability externally, -while inner classes help internally with state management or transitional operations. +while nested classes help internally with state management or transitional operations. + +#### Interfaces -### Abstract inner classes +Nested classes (including enums) may be defined in an interface body; however, they must be public classes. +This is because an interface always defines public methods and properties, so this is to maintain consistency. -It is worth exploring what an `abstract` inner class means and how it works. -Abstract inner classes are allowed to be the parent of any class that can see them. +### Abstract nested classes + +It is worth exploring what an `abstract` nested class means and how it works. +Abstract nested classes are allowed to be the parent of any class that can see them. For example, a private abstract class may only be inherited by subclasses in the same outer class. -A protected abstract class may be inherited by an inner class in a subclass of the outer class or an inner class in the +A protected abstract class may be inherited by a nested class in a subclass of the outer class or a nested class in the same outer class. However, this is not required by subclasses of the outer class. -Abstract inner classes may not be instantiated, just as abstract outer classes may not be instantiated. +Abstract nested classes may not be instantiated, just as abstract outer classes may not be instantiated. ```php class OuterParent { @@ -499,7 +477,7 @@ class Middle extends OuterParent {} // Last demonstrates extending the abstract inner class explicitly. class Last extends OuterParent { - private abstract class InnerExtended extends OuterParent:>InnerBase {} + private abstract class InnerExtended extends OuterParent\InnerBase {} } ``` @@ -507,10 +485,13 @@ class Last extends OuterParent { ## Backward Incompatible Changes - This RFC introduces new syntax and behavior to PHP, which does not conflict with existing syntax. -- Some error messages will be updated to reflect inner classes, and tests that depend on these error messages are likely +- Some error messages will be updated to reflect nested classes, and tests that depend on these error messages are likely to fail. - Tooling using AST or tokenization may need to be updated to support the new syntax. +Note: Applying visibility modifiers to class declarations is new syntax, but only valid in nested contexts. +Top-level classes will continue to behave as before, and this change introduces no ambiguity in existing code. + ## Proposed PHP Version(s) This RFC targets the next version of PHP. @@ -523,7 +504,6 @@ None. ### To Existing Extensions -Extensions accepting class names may need to be updated to support `:>` in class names. None were discovered during testing, but it is possible there are unbundled extensions that may be affected. ### To Opcache @@ -546,10 +526,12 @@ There should be no change to any existing PHP syntax. ## Future Scope -- Inner enums - Inner interfaces - Inner traits -- `Outer` keyword for easily accessing outer classes + +This RFC focuses on class and enum nesting. +Interfaces and traits are excluded for now due to their unique visibility and resolution semantics, +but may be considered in follow-up RFCs. ## Proposed Voting Choices @@ -568,10 +550,7 @@ To be completed. ## Implementation -After the project is implemented, this section should contain - the -version(s) it was merged into - a link to the git commit(s) - a link to -the PHP manual entry for the feature - a link to the language -specification section (if any) + ## References From acd1e0febcdaf2c93252c382763e91d2959da0d4 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 7 Nov 2025 16:39:12 +0000 Subject: [PATCH 13/13] Automated changes from GitHub Actions --- published/short-class.ptxt | 428 +++++++++++++++++-------------------- 1 file changed, 199 insertions(+), 229 deletions(-) diff --git a/published/short-class.ptxt b/published/short-class.ptxt index 6662175..860f7e9 100644 --- a/published/short-class.ptxt +++ b/published/short-class.ptxt @@ -1,6 +1,6 @@ -====== PHP RFC: Inner Classes ====== +====== PHP RFC: Nested Classes ====== - * Version: 0.4 + * Version: 0.5 * Date: 2025-02-08 * Author: Rob Landers, * Status: Under Discussion (Accepted or Declined) @@ -8,17 +8,11 @@ ===== Introduction ===== -This RFC proposes a significant enhancement to the language: **Inner Classes**. Inner classes enable the definition of classes within other classes, introducing a new level of encapsulation and organization within PHP applications. - -**Inner Classes** allows for the ability to define classes within other classes and control the use of inner classes through visibility. Many languages allow the encapsulation of behavior through inner classes (sometimes called **nested classes**), such as Java, C#, and many others. - -PHP developers currently rely heavily on annotations (e.g., @internal) or naming conventions to indicate encapsulation of Data Transfer Objects (DTOs) and related internal classes. These approaches offer limited enforcement, causing potential misuse or unintended coupling. - -This RFC introduces Inner Classes to clearly communicate and enforce boundaries around encapsulated classes, facilitating well-known design patterns (e.g., Builder, DTO, serialization patterns) with native language support. This reduces boilerplate, enhances clarity, and ensures internal types remain truly internal. +This RFC proposes a significant enhancement to the PHP language: **Nested Classes**. Nested classes allow the definition of one class within another, enabling tighter encapsulation and better organization of related logic. This feature naturally supports common design patterns such as Builders, DTOs, and serialization helpers, reducing boilerplate, minimizing namespace clutter, and clarifying code structure. Unlike some other languages, PHP nested classes do not implicitly capture the outer class instance, preserving explicit data flow and minimizing hidden coupling. ===== Proposal ===== -Inner classes allow defining classes within other classes, following standard visibility rules. This allows developers to declare a class as ''%%private%%'' or ''%%protected%%'' and restrict its usage to the outer class. They are accessed via a new operator: ''%%:>%%'' which is a mixture of ''%%->%%'' and ''%%::%%''. +Nested classes allow defining classes (including enums) within other classes, interfaces, or enums, following standard visibility rules. This allows developers to declare a class as ''%%private%%'' or ''%%protected%%'' and restrict its usage to an outer class. class Outer { @@ -31,244 +25,176 @@ class Outer { } } -$foo = new Outer:>Inner('Hello, world!'); +$foo = new Outer\Inner('Hello, world!'); echo $foo->message; // outputs: Hello, world! -$baz = new Outer:>PrivateInner('Hello, world!'); -// Fatal error: Class 'Outer:>PrivateInner' is private +$baz = new Outer\PrivateInner('Hello, world!'); +// Uncaught TypeError: Cannot instantiate class Outer\PrivateInner from the global scope -Inner classes are just regular class definitions with a couple additional features. That means, except where otherwise noted, "how would inner classes work in situation X?" can be answered with "the same as any other class/object." +Nested classes are just regular class definitions with visibility. That means, except where otherwise noted, "how would nested classes work in situation X?" can be answered with "the same as any other class/object." -==== Declaration Syntax ==== +==== Use-cases and examples ==== + +Nested classes are a powerful tool for organizing code and encapsulating functionality. Some use-cases include: -Declaring an inner class is just like declaring any other class. You can define ''%%readonly%%'', ''%%final%%'', and ''%%abstract%%'' inner classes -- or combinations, for example. However, when defining an inner class, you may also define it as ''%%private%%'', ''%%protected%%'', or ''%%public%%''. + * **Builder patterns**: Nested classes can be used to create complex objects step-by-step, encapsulating the construction logic within the outer class. + * **Data Transfer Objects (DTOs)**: Nested classes can be used to define data structures that are closely related to the outer class, improving code organization and readability. + * **Serialization helpers**: Nested classes can be used to define custom serialization logic for the outer class, encapsulating the serialization details within the outer class. + * **Encapsulation**: Nested classes can be used to encapsulate functionality that is only relevant to the outer class, reducing the visibility of the nested class and improving code organization. + * **Namespace management**: Nested classes can help reduce the number of top-level classes in a namespace, improving code organization and reducing clutter. -If an inner class does not explicitly declare visibility (private, protected, or public), it is implicitly considered public. This matches PHP’s existing class behavior for methods and properties. +=== Example: Serialization DTOs === -class Outer { - private class Inner {} - protected readonly class ReadOnlyInner {} - public abstract class AbstractInner {} - public final class FinalInner {} +class Message { + public function __construct(private string $message, private string $from, private string $to) {} + + public function Serialize(): string { + return serialize(new SerializedMessage($this)); + } + + private class SerializedMessage { + public string $message; + public string $from; + public string $to; + + public function __construct(Message $message) { + $this->message = $message->message; + $this->from = $message->from; + $this->to = $message->to; + } + } } -If an inner class is not defined as ''%%private%%'', ''%%protected%%'', or ''%%public%%'', it is assumed to be ''%%public%%''. - -==== Syntax rationale ==== - -The ''%%:>%%'' syntax was selected after evaluating alternatives: - - * Using ''%%::%%'' was cumbersome and visually ambiguous due to existing static resolution syntax. - * Using ''%%\%%'' or ''%%\\%%'' was rejected due to potential conflicts with namespaces and escaping, causing confusion. - -''%%:>%%'' communicates "inner membership," visually linking the inner class to the outer class. - -==== Binding ==== - -Inner classes are explicitly bound to their outer classes and are intentionally not inherited through subclassing, traits, or interfaces. This prevents ambiguity or complexity arising from inheriting tightly bound encapsulated logic. +=== Example: Builder Pattern === -class Outer { - class OuterInner {} -} - -interface Foo { - class FooInner {} -} - -trait Bar { - class BarInner {} -} - -class All extends Outer implements Foo { - use Bar; +class Person { + public class Builder { + use AgeCalculator; - public function does_not_work() { - new self:>OuterInner(); // Fatal error: Class 'All:>OuterInner' not found - new self:>FooInner(); // Fatal error: Class 'All:>FooInner' not found - new self:>BarInner(); // Fatal error: Class 'All:>BarInner' not found + private string $name; + private DateTimeInterface $age; - new parent:>OuterInner(); // This does work because it resolves to Outer:>OuterInner + public function setName(string $name): self { + $this->name = $name; + return $this; + } + + public function setBirthday(DateTimeInterface $age): self { + $this->age = $age; + return $this; + } + + public function build(): Person { + return new Person($this->name, $this->calculateAge($this->age)); + } } + + public function __construct(private string $name, private int $age) {} } -=== static resolution === - -The ''%%static%%'' keyword is **not** supported to resolve inner classes. Attempting to do so results in an error, depending on where it is used: +=== Example: DTOs === -class Outer { - class Inner {} - - // Fatal error: Cannot use the static modifier on a parameter - public function foo(static:>Inner $bar) {} - - // Parse error: syntax error, unexpected token ":>", expecting ";" or "{" - public function bar(): static:>Inner {} - - // Fatal error: Cannot use "static" as class name, as it is reserved - class Baz extends static:>Inner {} - - public function foobar() { - return new static:>Inner(); // Fatal error: Cannot use the static modifier on an inner class +enum Status { + case Active; + case Inactive; + + private enum Processing { + case Pending; + case Completed; } -} - -This is to prevent casual LSP violations of inheritance and to maintain the strong binding of inner classes. + public class Message { -=== parent resolution === + private Processing $processing = Processing::Pending; -The ''%%parent%%'' keyword is supported to resolve inner classes. Which parent it resolves to depends on the context: + public function __construct(private Status $status) {} - -class Foo { - class Bar {} -} + public function getMessage(): string { + return match ([$this->status, $this->processing]) { + [Status::Active, Processing::Pending] => "Active and Pending", + [Status::Active, Processing::Completed] => "Active and Completed", + [Status::Inactive, Processing::Pending] => "Inactive and Pending", + [Status::Inactive, Processing::Completed] => throw new LogicException('should not happen'), + }; + } + } -class Baz extends Foo { - // parent:>Bar resolves to Foo:>Bar - class Bar extends parent:>Bar { - // inside the class body, parent refers to Foo:>Bar - public function doSomething(): parent {} + public function createMessage(): Message { + return new Message($this); } } -''%%parent%%'' explicitly resolves to the parent class of the current class body it is written in and helps with writing more concise code. - -=== self resolution === - -The ''%%self%%'' keyword is supported to resolve inner classes. Which ''%%self%%'' it resolves to depends on the context: +=== Example: class shared via interface === -class Outer { - class Middle { - class Other {} - - // extends Outer:>Middle:>Other - class Inner extends self:>Other { - public function foo(): self {} // returns Outer:>Middle:>Inner +interface Reader { + public class Buffer { + public string $bytes; } - } + + public function read(): void; + public function getBuffer(): Buffer; } - -''%%self%%'' explicitly resolves to the current class body it is written in, just like with ''%%parent%%''. On ''%%inner%%'' classes. +// in another file: +use Reader\Buffer; -When using self inside a class body to refer to an inner class, if the inner class is not found in the current class, it will fail with an error. +class FileReader implements Reader { + protected Buffer $buffer; - -class OuterParent { - class Inner {} - class Other {} -} + public function __construct() { + $this->buffer = new Buffer(); + } -class MiddleChild extends OuterParent { - // Fatal Error: cannot find class MiddleChild:>InnerParent - class Inner extends self:>InnerParent {} -} + public function read(): void { + $this->buffer->bytes = random_bytes(5); + } -class OuterChild extends OuterParent { - class Inner {} - public function foo() { - $inner = new self:>Inner(); // resolves to OuterChild:>Inner - $inner = new parent:>Inner(); // resolves to OuterParent:>Inner - $inner = new self:>Other(); // Fatal Error: cannot find class OuterChild:>Other - $inner = new parent:>Other(); // resolves to OuterParent:>Other + public function getBuffer(): Buffer { + return $this->buffer; } } -=== Dynamic resolution === - -Just as with ''%%::%%'', developers may use variables to resolve inner classes, or refer to them by name directly via a string: +==== Why PHP needs nested classes ==== - -// Using variables to dynamically instantiate inner classes: -$outer = "Outer"; -$inner = "Inner"; -$instance = new $outer:>$inner(); - -// Instantiating inner class dynamically via a fully qualified class string: -$dynamicClassName = "Outer:>Inner"; -$instance = new $dynamicClassName(); - +PHP developers today often simulate nested classes through long namespace chains or anonymous classes embedded in methods. While these patterns work, they come with visibility challenges, verbosity, and a lack of cohesion between related types. Nested classes solve these problems natively, offering better encapsulation without requiring new runtime behavior or complex inheritance tricks. -This provides flexibility and backwards compatibility for dynamic code that may not expect an inner class. +==== Declaration Syntax ==== -==== Visibility from inner classes ==== +Declaring a nested class uses the same syntax as declaring a top-level class. You can define ''%%readonly%%'', ''%%final%%'', and ''%%abstract%%'' nested classes. In addition, nested classes may be declared with visibility modifiers (''%%private%%'', ''%%protected%%'', ''%%public%%''), which determine where the nested class can be referenced or instantiated. -Inner classes have access to their outer class’s private and protected methods, properties, and inner classes. However, they do not have access to their siblings’ private and protected methods, properties, and inner classes. +If a nested class does not explicitly declare visibility (private, protected, or public), it is implicitly considered public. This matches PHP’s existing class behavior for methods and properties. -class User { - public private(set) string $name; - public private(set) string $email; - - private function __construct(self:>Builder $builder) { - $this->name = $builder->name; - $this->email = $builder->email; - } - - public readonly final class Builder { - public function __construct(public private(set) string|null $name = null, public private(set) string|null $email = null) {} - - public function withEmail(string $email): self { - return new self($this->name, $email); - } - - public function withName(string $name): self { - return new self($name, $this->email); - } - - public function build(): User { - return new User($this); - } - } +class Outer { + private class Inner {} + protected readonly class ReadOnlyInner {} + public abstract class AbstractInner {} + public final class FinalInner {} } - -$user = new User:>Builder()->withName('Rob')->withEmail('rob@bottled.codes')->build(); -This enables usages such as builder patterns and other helper classes to succinctly encapsulate behavior. +Outer classes remain "public" (accessible and visible to all), and visibility modifiers cannot be applied to them. -==== Visibility from outside the outer class ==== +==== Visibility ==== -Inner classes are not visible outside their outer class unless they are public. Protected classes may be used and accessed from within their child classes. +Visibility refers to whether a method, class, or property is accessible from outside its defining context. A nested class may be declared as ''%%private%%'', ''%%protected%%'', or ''%%public%%''. A private class will //only// be visible within the lexical outer class, while a protected class is visible both inside its lexical scope and any child classes. A public class has no restrictions and will be visible from anywhere in the codebase. - -class Outer { - private class Other {} - protected class Inner { - public function Foo() { - $bar = new Outer:>Inner(); // allowed - $bar = new Outer:>Other(); // allowed - } - } -} - -class SubOuter extends Outer { - public function Foo() { - $bar = new parent:>Inner(); // allowed to access protected inner class - $bar = new parent:>Other(); // Cannot access private inner class 'Outer:>Other' - } -} - - -Attempting to instantiate a private or protected inner class outside its outer class will result in a fatal error: +=== Properties and Methods === - -new Outer:>Inner(); // Cannot access protected inner class 'Outer:>Inner' - +All outer class’s properties and methods are visible from nested classes, as from the perspective of the nested class, it is a member of the outer class. However, unlike some languages with nested classes, an outer class does not have direct access to a nested class’s private or protected members. === Method return type and argument declarations === -Inner classes may only be used as a return type or argument declarations for methods and functions that have the same visibility or lesser. Thus returning a ''%%protected%%'' class type from a ''%%public%%'' method is not allowed, but is allowed from a ''%%protected%%'' or ''%%private%%'' method. +Nested classes may only be used as a return type or argument declarations for methods and functions that have the same visibility or less. Thus returning a ''%%protected%%'' type from a ''%%public%%'' method is not allowed, but is allowed from a ''%%protected%%'' or ''%%private%%'' method. ^Inner Class Visibility^Method Visibility^Allowed^ |''%%public%%'' |''%%public%%'' |Yes | @@ -281,49 +207,61 @@ Inner classes may only be used as a return type or argument declarations for met |''%%private%%'' |''%%protected%%''|No | |''%%private%%'' |''%%private%%'' |Yes | -Methods and functions outside the outer class are considered public from the perspective of an inner class. Attempting to declare a return of a non-visible type will result in a ''%%TypeError%%'': +Attempting to declare a return of a non-visible type will result in a ''%%TypeError%%'': class Outer { private class Inner {} - public function getInner(): self:>Inner { - return new self:>Inner(); + public function getInner(): Inner { + return new Inner(); } } -// Fatal error: Uncaught TypeError: Public method getInner cannot return private class Outer:>Inner +// Fatal error: Uncaught TypeError: Public method getInner cannot return private class Outer\Inner new Outer()->getInner(); -=== Properties === +This does not prevent the programmer from returning a hidden type, only from declaring it. The programmer may choose to use an interface or public type instead: -The visibility of a type declaration on a property must also not increase the visibility of an inner class. Thus, a public property cannot declare a private type. + +interface FooBar {} -This gives a great deal of control to developers, preventing accidental misuse of inner classes. However, this **does not** preclude developers from returning a private/protected inner class, only from using them as a type declaration. The developer can use an interface or abstract class type declaration, or use a broader type such as ''%%object%%'', ''%%mixed%%'', or nothing at all: +class Outer { + private class Inner implements FooBar {} + + public function getInner(): FooBar { + return new Inner(); + } +} + + +=== Properties === + +The visibility of a type declaration on a property must also not increase the visibility of a nested class. Thus, a public property cannot declare a private type. class Outer { private class Inner implements FooBar {} public function getInner(): FooBar { - return new self:>Inner(); // not an error + return new Inner(); // not an error } } ==== Accessing outer classes ==== -Inner classes do not implicitly have access to an outer class instance. This design choice avoids implicit coupling between classes and keeps inner classes simpler, facilitating potential future extensions (such as adding an explicit outer keyword) without backward compatibility issues. +Nested classes do not implicitly have access to an outer class instance. This design choice avoids implicit coupling between classes and keeps nested classes simpler, facilitating potential future extensions (such as adding an explicit outer keyword) without backward compatibility issues. -Passing an outer instance explicitly remains an available option for accessing protected/private methods or properties. Thus inner classes may access outer class private/protected methods and properties if given an instance. +However, a programmer may pass an instance of an outer class (or another nested class) to a nested class. If a class has access to the outer class, it may use the outer class’s private, protected, and public properties and methods. class Message { public function __construct(private string $message, private string $from, private string $to) {} public function Serialize(): string { - return new Message:>SerializedMessage($this)->message; + return new SerializedMessage($this)->message; } private class SerializedMessage { @@ -336,59 +274,87 @@ class Message { } +==== Resolution ==== + +From a resolution perspective, outer classes behave as a pseudo-namespace. Using a short name of a nested class will resolve to the nested class. To disambiguate between different classes with the same name in the namespace and the nested class, the programmer can use fully qualified names. + + +class Inner {} + +abstract class Outer { + class Inner {} + public function foo(Inner $i); // resolves to Outer\Inner + public function bar(\Inner $i); // resolves to global Inner +} + + +This applies to deeply nested classes as well: + + +class Outer { + class Middle { + class Inner {} + } + public function foo(Middle\Inner $i); // resolves to Outer\Middle\Inner +} + + +This can lead to accidentally shadowing a class in the same namespace, thus requiring the use of ''%%use%%'' statements or fully qualified names to disambiguate. + ==== Reflection ==== -Several new methods are added to ''%%ReflectionClass%%'' to help support inspection of inner classes: +Several new methods are added to ''%%ReflectionClass%%'' to help support inspection of nested classes: -$reflection = new ReflectionClass('\a\namespace\Outer:>Inner'); +$reflection = new ReflectionClass('\a\namespace\Outer\Inner'); -$reflection->isInnerClass(); // true +$reflection->isNestedClass(); // true $reflection->isPublic() || $reflection->isProtected() || $reflection->isPrivate(); // true -$reflection->getName(); // \a\namespace\Outer:>Inner -$reflection->getShortName(); // Outer:>Inner +$reflection->getName(); // \a\namespace\Outer\Inner +$reflection->getShortName(); // Inner -For non-inner classes, ''%%ReflectionClass::isInnerClass()%%'' returns false. +For non-nested classes, ''%%ReflectionClass::isNestedClass()%%'' returns ''%%false%%''. + +A future addition to ''%%ReflectionClass::getNestedClasses()%%'' may allow enumeration of nested class declarations. ==== Autoloading ==== -Inner classes are never autoloaded, only their outermost class is autoloaded. If the outermost class does not exist, then their inner classes do not exist. +Since nested classes are referenced using standard -separated names, the engine treats them like any other class name during resolution. If the outer class has not yet been loaded, PHP will attempt to autoload the nested class name, which may result in fallback behavior depending on the autoloader. Implementers are encouraged to structure autoloaders to load the outer class when a nested class is referenced. ==== Usage ==== -Inner classes may be defined in the body of any class-like structure, including but not limited to: +Nested classes may be defined in the body of any class-like structure, except traits: * in a class body * in an anonymous class body * in an enum body - * in a trait body - * in an interface body + * in an interface body (public nested classes only) -Note: While traits and interfaces may define inner classes, classes using these traits or implementing these interfaces do not inherit their inner classes. Inner classes remain strictly scoped and bound to their defining context only. +Traits are not allowed to contain nested classes and are left to a future RFC. Traits are excluded from this RFC because nested class resolution via use statements introduces ambiguities about scope and visibility. These concerns are deferred to a future RFC. ==== Outer class effects ==== -Inner classes are fully independent of their outer class’s declaration modifiers. Outer class modifiers such as abstract, final, or readonly do not implicitly cascade down or affect the inner classes defined within them. +Nested classes are fully independent of their outer class’s declaration modifiers. Outer class modifiers such as abstract, final, or readonly do not implicitly cascade down or affect the nested classes defined within them. Specifically: - * An abstract outer class can define concrete (non-abstract) inner classes. Inner classes remain instantiable, independent of the outer class’s abstractness. - * A final outer class does not force its inner classes to be final. Inner classes within a final class can be extended internally, providing flexibility within encapsulation boundaries. - * A readonly outer class can define mutable inner classes. This supports internal flexibility, allowing the outer class to maintain immutability in its external API while managing state changes internally via inner classes. + * An abstract outer class can define concrete (non-abstract) nested classes. Nested classes remain instantiable, independent of the outer class’s abstractness. + * A final outer class does not force its nested classes to be final. Nested classes within a final class can be extended internally, providing flexibility within encapsulation boundaries. + * A readonly outer class can define mutable nested classes. This supports internal flexibility, allowing the outer class to maintain immutability in its external API while managing state changes internally via nested classes. Examples: abstract class Service { - // Valid: Inner class is not abstract, despite the outer class being abstract. + // Valid: Nested class is not abstract, despite the outer class being abstract. public class Implementation { public function run(): void {} } } -// Allowed; abstract outer class does not force inner classes to be abstract. -new Service:>Implementation(); +// Allowed; abstract outer class does not force nested classes to be abstract. +new Service\Implementation(); @@ -399,8 +365,8 @@ readonly class ImmutableCollection { $this->items = $items; } - public function getMutableBuilder(): self:>Builder { - return new self:>Builder($this->items); + public function getMutableBuilder(): Builder { + return new Builder($this->items); } public class Builder { @@ -422,13 +388,17 @@ readonly class ImmutableCollection { } -In this example, even though ImmutableBuilder is a mutable inner class within a readonly outer class, the outer class maintains its immutability externally, while inner classes help internally with state management or transitional operations. +In this example, even though ''%%Builder%%'' is a mutable nested class within a readonly outer class, the outer class maintains its immutability externally, while nested classes help internally with state management or transitional operations. + +=== Interfaces === -==== Abstract inner classes ==== +Nested classes (including enums) may be defined in an interface body; however, they must be public classes. This is because an interface always defines public methods and properties, so this is to maintain consistency. -It is worth exploring what an ''%%abstract%%'' inner class means and how it works. Abstract inner classes are allowed to be the parent of any class that can see them. For example, a private abstract class may only be inherited by subclasses in the same outer class. A protected abstract class may be inherited by an inner class in a subclass of the outer class or an inner class in the same outer class. However, this is not required by subclasses of the outer class. +==== Abstract nested classes ==== -Abstract inner classes may not be instantiated, just as abstract outer classes may not be instantiated. +It is worth exploring what an ''%%abstract%%'' nested class means and how it works. Abstract nested classes are allowed to be the parent of any class that can see them. For example, a private abstract class may only be inherited by subclasses in the same outer class. A protected abstract class may be inherited by a nested class in a subclass of the outer class or a nested class in the same outer class. However, this is not required by subclasses of the outer class. + +Abstract nested classes may not be instantiated, just as abstract outer classes may not be instantiated. class OuterParent { @@ -440,16 +410,18 @@ class Middle extends OuterParent {} // Last demonstrates extending the abstract inner class explicitly. class Last extends OuterParent { - private abstract class InnerExtended extends OuterParent:>InnerBase {} + private abstract class InnerExtended extends OuterParent\InnerBase {} } ===== Backward Incompatible Changes ===== * This RFC introduces new syntax and behavior to PHP, which does not conflict with existing syntax. - * Some error messages will be updated to reflect inner classes, and tests that depend on these error messages are likely to fail. + * Some error messages will be updated to reflect nested classes, and tests that depend on these error messages are likely to fail. * Tooling using AST or tokenization may need to be updated to support the new syntax. +Note: Applying visibility modifiers to class declarations is new syntax, but only valid in nested contexts. Top-level classes will continue to behave as before, and this change introduces no ambiguity in existing code. + ===== Proposed PHP Version(s) ===== This RFC targets the next version of PHP. @@ -462,7 +434,7 @@ None. ==== To Existing Extensions ==== -Extensions accepting class names may need to be updated to support ''%%:>%%'' in class names. None were discovered during testing, but it is possible there are unbundled extensions that may be affected. +None were discovered during testing, but it is possible there are unbundled extensions that may be affected. ==== To Opcache ==== @@ -482,10 +454,10 @@ There should be no change to any existing PHP syntax. ===== Future Scope ===== - * Inner enums * Inner interfaces * Inner traits - * ''%%Outer%%'' keyword for easily accessing outer classes + +This RFC focuses on class and enum nesting. Interfaces and traits are excluded for now due to their unique visibility and resolution semantics, but may be considered in follow-up RFCs. ===== Proposed Voting Choices ===== @@ -498,8 +470,6 @@ To be completed. ===== Implementation ===== -After the project is implemented, this section should contain - the version(s) it was merged into - a link to the git commit(s) - a link to the PHP manual entry for the feature - a link to the language specification section (if any) - ===== References ===== * [[https://externals.io/message/125975#125977|email that inspired this RFC]]