rfc:short-and-inner-classes
Differences
This shows you the differences between two versions of the page.
Next revision | Previous revision | ||
rfc:short-and-inner-classes [2025/02/22 20:09] – created withinboredom | rfc:short-and-inner-classes [2025/03/15 10:28] (current) – reword and clarify withinboredom | ||
---|---|---|---|
Line 1: | Line 1: | ||
- | ====== PHP RFC: Inner Classes | + | ====== PHP RFC: Inner Classes ====== |
- | * Version: 0.1 | + | * Version: 0.4 |
* Date: 2025-02-08 | * Date: 2025-02-08 | ||
* Author: Rob Landers, < | * Author: Rob Landers, < | ||
- | * Status: | + | * Status: Under Discussion |
* First Published at: http:// | * First Published at: http:// | ||
===== Introduction ===== | ===== Introduction ===== | ||
- | PHP has steadily evolved | + | This RFC proposes a significant enhancement |
- | This RFC proposes two related enhancements | + | **Inner Classes** allows for the ability |
- | **Short class syntax**, which allows for defining simple | + | PHP developers currently rely heavily on annotations (e.g., @internal) |
+ | |||
+ | 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, | ||
+ | |||
+ | ===== Proposal ===== | ||
+ | |||
+ | Inner classes | ||
<code php> | <code php> | ||
- | class Point(int $x, int $y); | + | class Outer { |
+ | public class Inner { | ||
+ | public function __construct(public string | ||
+ | } | ||
+ | |||
+ | private class PrivateInner { | ||
+ | public function __construct(public string $message) {} | ||
+ | } | ||
+ | } | ||
+ | |||
+ | $foo = new Outer:> | ||
+ | echo $foo-> | ||
+ | // outputs: Hello, world! | ||
+ | $baz = new Outer:> | ||
+ | // Fatal error: Class ' | ||
</ | </ | ||
- | This syntax serves as a shorthand for defining | + | Inner classes |
- | **Inner | + | ==== Declaration Syntax ==== |
+ | |||
+ | Declaring an inner class is just like declaring any other class. You can define '' | ||
+ | |||
+ | If an inner class does not explicitly declare | ||
<code php> | <code php> | ||
- | class Foo { | + | class Outer { |
- | public class Bar(public | + | private class Inner {} |
+ | protected readonly class ReadOnlyInner {} | ||
+ | public | ||
+ | | ||
} | } | ||
</ | </ | ||
- | ===== Proposal ===== | + | If an inner class is not defined as '' |
- | ==== Short Class Syntax ==== | + | ==== Syntax |
- | The proposed syntax for a short class definition is as follows: a keyword | + | The '' |
- | <code php> | + | * Using '' |
+ | * Using '' | ||
- | // 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. | + | ==== Binding ==== |
- | readonly class Vector(int $x, int $y) extends BaseVector implements JsonSerializable use PointTrait, Evolvable; | + | |
- | </ | + | |
- | This is equivalent | + | Inner classes are explicitly bound to their outer classes and are intentionally not inherited through subclassing, |
<code php> | <code php> | ||
- | class Point { | + | class Outer { |
- | | + | |
+ | } | ||
+ | |||
+ | interface Foo { | ||
+ | class FooInner {} | ||
+ | } | ||
+ | |||
+ | trait Bar { | ||
+ | class BarInner | ||
} | } | ||
- | readonly public | + | class All extends |
- | use PointTrait, Evolvable; | + | use Bar; |
| | ||
- | public function | + | public function |
+ | new self:> | ||
+ | new self:> | ||
+ | new self:> | ||
+ | |||
+ | new parent:> | ||
+ | | ||
} | } | ||
</ | </ | ||
- | Any properties defined within the parenthesis are defined as a property on the class and are automatically | + | === static resolution === |
+ | |||
+ | The '' | ||
<code php> | <code php> | ||
- | // declare $shapes as a private property | + | class Outer { |
- | class Geometry(private | + | class Inner {} |
+ | |||
+ | | ||
+ | | ||
+ | |||
+ | // Parse error: syntax error, unexpected token ":>", | ||
+ | public function bar(): static:> | ||
+ | |||
+ | // Fatal error: Cannot | ||
+ | class Baz extends static:> | ||
+ | |||
+ | public function foobar() { | ||
+ | return new static:> | ||
+ | } | ||
+ | } | ||
</ | </ | ||
- | === Default Values === | + | This is to prevent casual LSP violations of inheritance and to maintain the strong binding of inner classes. |
- | Default values may be provided for properties but only for properties with type hints: | + | === parent resolution === |
+ | |||
+ | The '' | ||
<code php> | <code php> | ||
- | class Point(int $x = 0, int $y = 0); | + | class Foo { |
+ | class Bar {} | ||
+ | } | ||
+ | |||
+ | class Baz extends Foo { | ||
+ | // parent:> | ||
+ | class Bar extends parent:> | ||
+ | // inside the class body, parent refers to Foo:> | ||
+ | public function doSomething(): parent {} | ||
+ | } | ||
+ | } | ||
</ | </ | ||
- | === Inheritance | + | '' |
- | With class short syntax, no behavior may be defined, yet it can still utilize traits, interfaces, and other classes. | + | === self resolution === |
+ | |||
+ | The '' | ||
<code php> | <code php> | ||
- | class Point(int $x, int $y) extends BasePoint implements JsonSerializable use PointTrait, Evolvable; | + | class Outer { |
+ | class Middle { | ||
+ | class Other {} | ||
+ | |||
+ | // extends Outer:> | ||
+ | class Inner extends self:> | ||
+ | public function foo(): self {} // returns Outer:> | ||
+ | } | ||
+ | } | ||
+ | } | ||
</ | </ | ||
- | Note that the original constructor from any parent | + | '' |
- | === Empty 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. |
- | Short classes | + | <code php> |
+ | class OuterParent { | ||
+ | class Inner {} | ||
+ | class Other {} | ||
+ | } | ||
+ | |||
+ | class MiddleChild extends OuterParent { | ||
+ | // Fatal Error: cannot find class MiddleChild:> | ||
+ | class Inner extends self:> | ||
+ | } | ||
+ | |||
+ | class OuterChild extends OuterParent { | ||
+ | class Inner {} | ||
+ | public function foo() { | ||
+ | $inner = new self:> | ||
+ | $inner = new parent:> | ||
+ | $inner = new self:> | ||
+ | $inner = new parent:> | ||
+ | } | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | === Dynamic resolution === | ||
+ | |||
+ | Just as with '' | ||
<code php> | <code php> | ||
- | class Point() extends BasePoint use PointTrait; | + | // Using variables to dynamically instantiate inner classes: |
+ | $outer = " | ||
+ | $inner = " | ||
+ | $instance = new $outer:> | ||
+ | |||
+ | // Instantiating inner class dynamically via a fully qualified | ||
+ | $dynamicClassName = " | ||
+ | $instance = new $dynamicClassName(); | ||
</ | </ | ||
- | === Attributes === | + | This provides flexibility and backwards compatibility for dynamic code that may not expect an inner class. |
- | Attributes may also be used with short classes: | + | ==== 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. | ||
<code php> | <code php> | ||
- | # | + | class User { |
- | class Password(# | + | |
+ | public private(set) string $email; | ||
+ | |||
+ | private function __construct(self:> | ||
+ | $this-> | ||
+ | $this-> | ||
+ | } | ||
+ | |||
+ | public readonly final class Builder { | ||
+ | public function __construct(public private(set) | ||
+ | |||
+ | public function withEmail(string $email): self { | ||
+ | return new self($this-> | ||
+ | } | ||
+ | |||
+ | public function withName(string $name): self { | ||
+ | return new self($name, $this-> | ||
+ | } | ||
+ | |||
+ | public function build(): User { | ||
+ | return new User($this); | ||
+ | } | ||
+ | } | ||
+ | } | ||
+ | |||
+ | $user = new User:> | ||
</ | </ | ||
- | === Modifiers === | + | This enables usages such as builder patterns and other helper classes to succinctly encapsulate behavior. |
- | Short classes | + | ==== Visibility from outside the outer class ==== |
+ | |||
+ | Inner classes | ||
<code php> | <code php> | ||
- | readonly | + | class Outer { |
+ | private class Other {} | ||
+ | protected class Inner { | ||
+ | public function Foo() { | ||
+ | | ||
+ | | ||
+ | } | ||
+ | } | ||
+ | } | ||
- | final class Config(string | + | class SubOuter extends Outer { |
+ | public function Foo() { | ||
+ | | ||
+ | | ||
+ | } | ||
+ | } | ||
+ | </ | ||
- | abstract | + | Attempting to instantiate a private or protected inner class outside its outer class will result in a fatal error: |
+ | |||
+ | <code php> | ||
+ | new Outer:> | ||
</ | </ | ||
- | === How it works === | + | === Method return type and argument declarations |
- | Short classes | + | Inner classes |
- | ==== Inner Classes ==== | + | ^Inner Class Visibility^Method Visibility^Allowed^ |
+ | |'' | ||
+ | |'' | ||
+ | |'' | ||
+ | |'' | ||
+ | |'' | ||
+ | |'' | ||
+ | |'' | ||
+ | |'' | ||
+ | |'' | ||
- | Inner classes are classes that are defined within another | + | Methods and functions outside the outer class are considered public from the perspective of an inner class. |
<code php> | <code php> | ||
class Outer { | class Outer { | ||
- | class Inner(public string $message); | + | |
| | ||
- | | + | public function |
- | | + | return new self:> |
} | } | ||
} | } | ||
- | $foo = new Outer:: | + | // Fatal error: Uncaught |
- | echo $foo-> | + | new Outer()-> |
- | // outputs: Hello, world! | + | |
- | $baz = new Outer:: | + | |
- | // Fatal error: Uncaught | + | |
</ | </ | ||
- | Inner classes have inheritance similar to static properties, allowing you to define rich class hierarchies: | + | === Properties === |
+ | |||
+ | 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 | ||
<code php> | <code php> | ||
- | readonly | + | class Outer { |
- | + | | |
- | class Geometry | + | |
- | | + | |
- | protected function __construct(Point ...$points) | + | |
- | $this-> | + | |
- | | + | |
| | ||
- | | + | public function |
- | | + | |
- | parent:: | + | |
- | } | + | |
- | } | + | |
- | + | ||
- | public class FromCoordinates extends Geometry | + | |
- | | + | |
- | $points = []; | + | |
- | for ($i = 0; $i < count($coordinates); | + | |
- | $points[] = new Point($coordinates[$i], | + | |
- | } | + | |
- | parent:: | + | |
- | } | + | |
} | } | ||
} | } | ||
+ | </ | ||
- | class Triangle extends Geometry | + | ==== Accessing outer classes ==== |
- | | + | |
- | parent:: | + | 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. |
- | | + | |
+ | Passing an outer instance explicitly remains an available option for accessing protected/ | ||
+ | |||
+ | <code php> | ||
+ | class Message | ||
+ | | ||
| | ||
- | | + | public function |
- | | + | |
- | | + | |
- | } | + | |
} | } | ||
| | ||
- | | + | |
- | public function __construct(int $x1, int $y1, int $x2, int $y2, int $x3, int $y3) { | + | public string $message; |
- | | + | |
+ | public function __construct(Message | ||
+ | | ||
} | } | ||
} | } | ||
} | } | ||
+ | </ | ||
- | $t = new Triangle:: | + | ==== Reflection ==== |
+ | |||
+ | Several | ||
+ | |||
+ | <code php> | ||
+ | $reflection = new ReflectionClass(' | ||
- | var_dump($t instanceof Triangle); // true | + | $reflection-> |
+ | $reflection-> | ||
+ | $reflection-> | ||
+ | $reflection-> | ||
</ | </ | ||
- | === Modifiers === | + | For non-inner classes, '' |
- | Inner classes support modifiers such as '' | + | ==== Autoloading ==== |
- | * '' | + | Inner classes are never autoloaded, only their outermost |
- | * '' | + | |
- | Thus, an inner class with the modifier '' | + | ==== Usage ==== |
- | '' | + | Inner classes may be defined in the body of any class-like structure, including but not limited to: |
- | === Visibility === | + | * in a class body |
+ | * in an anonymous class body | ||
+ | * in an enum body | ||
+ | * in a trait body | ||
+ | * in an interface body | ||
- | A '' | + | Note: While traits and interfaces may define inner classes, classes using these traits |
+ | |||
+ | ==== Outer class effects ==== | ||
+ | |||
+ | Inner classes are fully independent of their outer class’s declaration modifiers. Outer class modifiers such as abstract, final, | ||
+ | |||
+ | 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 readonly outer class can define mutable inner classes. This supports internal flexibility, | ||
+ | |||
+ | Examples: | ||
<code php> | <code php> | ||
- | class Outer { | + | abstract |
- | | + | |
- | + | | |
- | public function | + | public function |
- | return new self:: | + | |
} | } | ||
} | } | ||
- | function doSomething(Outer:: | + | // Allowed; abstract outer class does not force inner classes to be abstract. |
- | echo $inner-> | + | new Service:>Implementation(); |
- | } | + | |
- | + | ||
- | doSomething(new Outer()->getInner()); | + | |
- | // Fatal error: Private inner class Outer:: | + | |
</ | </ | ||
- | |||
- | You may also not instantiate a private/ | ||
<code php> | <code php> | ||
- | $x = new Outer::Inner(); | + | readonly class ImmutableCollection { |
- | // Fatal error: Uncaught Error: Cannot access private inner class Outer::Inner | + | private array $items; |
+ | |||
+ | public function __construct(array $items) { | ||
+ | | ||
+ | } | ||
+ | |||
+ | public function getMutableBuilder(): self:>Builder { | ||
+ | return new self:> | ||
+ | } | ||
+ | |||
+ | public | ||
+ | private array $items; | ||
+ | |||
+ | public function __construct(array $items) { | ||
+ | $this-> | ||
+ | } | ||
+ | |||
+ | public function add(mixed $item): self { | ||
+ | $this-> | ||
+ | return $this; | ||
+ | } | ||
+ | |||
+ | public function build(): ImmutableCollection { | ||
+ | return new ImmutableCollection($this-> | ||
+ | } | ||
+ | } | ||
+ | } | ||
</ | </ | ||
- | === Inheritance === | + | 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. |
- | Classes may not inherit from inner classes. Inner classes may inherit from other classes, including the outer class. | + | ==== Abstract |
- | === Names === | + | It is worth exploring what an '' |
- | Inner classes may not have any name that conflicts with a constant or static property of the same name. | + | Abstract inner classes may not be instantiated, |
<code php> | <code php> | ||
- | class Foo { | + | class OuterParent |
- | | + | |
- | | + | |
- | + | ||
- | // Fatal error: Uncaught Error: Cannot redeclare Foo::Bar | + | |
} | } | ||
- | class Foo { | + | // Middle is allowed, does not have to implement InnerBase |
- | | + | class Middle extends OuterParent |
- | class Bar(public string $message); | + | |
- | + | // Last demonstrates extending the abstract inner class explicitly. | |
- | | + | class Last extends OuterParent { |
+ | | ||
} | } | ||
</ | </ | ||
Line 252: | Line 447: | ||
===== Backward Incompatible Changes ===== | ===== Backward Incompatible Changes ===== | ||
- | This RFC introduces new syntax and behavior to PHP, which does not conflict with existing 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) ===== | ===== Proposed PHP Version(s) ===== | ||
Line 266: | Line 463: | ||
==== To Existing Extensions ==== | ==== To Existing Extensions ==== | ||
- | Extensions accepting class names may need to be updated to support '' | + | Extensions accepting class names may need to be updated to support '' |
==== To Opcache ==== | ==== To Opcache ==== | ||
- | Most of the changes are in compilation and AST, so the impact | + | This change introduces a new opcode, |
+ | |||
+ | ==== To Tooling ==== | ||
+ | |||
+ | Tooling that uses AST or tokenization may need to be updated to support the new syntax, such as syntax highlighting, | ||
===== Open Issues ===== | ===== Open Issues ===== | ||
Line 278: | Line 479: | ||
===== Unaffected PHP Functionality ===== | ===== Unaffected PHP Functionality ===== | ||
- | There should be no change to existing PHP functionality. | + | There should be no change to any existing PHP syntax. |
===== Future Scope ===== | ===== Future Scope ===== | ||
- | * inner enums | + | * Inner enums |
+ | * Inner interfaces | ||
+ | * Inner traits | ||
+ | * '' | ||
===== Proposed Voting Choices ===== | ===== Proposed Voting Choices ===== | ||
+ | |||
+ | As this is a significant change to the language, a 2/3 majority is required. | ||
===== Patches and Tests ===== | ===== Patches and Tests ===== | ||
- | A complete implementation is available [[https:// | + | To be completed. |
===== Implementation ===== | ===== Implementation ===== | ||
Line 297: | Line 503: | ||
===== References ===== | ===== References ===== | ||
- | Links to external references, discussions or RFCs | + | * [[https:// |
+ | * [[https:// | ||
===== Rejected Features ===== | ===== Rejected Features ===== | ||
- | Keep this updated with features that were discussed on the mail lists. | + | TBD |
rfc/short-and-inner-classes.1740254954.txt.gz · Last modified: 2025/02/22 20:09 by withinboredom