rfc:short-and-inner-classes
Differences
This shows you the differences between two versions of the page.
Both sides previous revisionPrevious revisionNext revision | Previous revision | ||
rfc:short-and-inner-classes [2025/03/05 22:12] – updated copy 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 ===== | ||
- | This RFC proposes | + | This RFC proposes |
- | Short Class Syntax | + | **Inner Classes** |
- | By adopting these enhancements, | + | PHP developers |
+ | |||
+ | This RFC introduces Inner Classes to clearly communicate | ||
===== Proposal ===== | ===== Proposal ===== | ||
- | ==== Short Class Syntax ==== | + | Inner classes allow defining |
- | + | ||
- | 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, | + | |
<code php> | <code php> | ||
+ | class Outer { | ||
+ | public class Inner { | ||
+ | public function __construct(public string $message) {} | ||
+ | } | ||
+ | | ||
+ | private class PrivateInner { | ||
+ | public function __construct(public string $message) {} | ||
+ | } | ||
+ | } | ||
- | // a simple class with two public properties | + | $foo = new Outer:> |
- | class Point(int | + | echo $foo-> |
+ | // outputs: Hello, world! | ||
+ | $baz = new Outer:> | ||
+ | // Fatal error: Class ' | ||
+ | </ | ||
- | // A readonly | + | Inner classes are just regular |
- | readonly | + | |
+ | ==== Declaration Syntax ==== | ||
+ | |||
+ | Declaring an inner class is just like declaring any other class. You can define '' | ||
+ | |||
+ | If an inner class does not explicitly declare visibility | ||
+ | |||
+ | <code php> | ||
+ | class Outer { | ||
+ | private class Inner {} | ||
+ | protected readonly class ReadOnlyInner {} | ||
+ | public abstract class AbstractInner {} | ||
+ | public final class FinalInner {} | ||
+ | } | ||
</ | </ | ||
- | This is equivalent | + | If an inner class is not defined as '' |
+ | |||
+ | ==== Syntax rationale ==== | ||
+ | |||
+ | The '' | ||
+ | |||
+ | * Using '' | ||
+ | * Using '' | ||
+ | |||
+ | '' | ||
+ | |||
+ | ==== Binding ==== | ||
+ | |||
+ | Inner classes are explicitly bound to their outer classes and are intentionally not inherited through subclassing, | ||
<code php> | <code php> | ||
- | class Point { | + | class Outer { |
- | | + | |
} | } | ||
- | readonly public | + | interface Foo { |
- | use PointTrait, Evolvable; | + | class FooInner {} |
+ | } | ||
+ | |||
+ | trait Bar { | ||
+ | class BarInner {} | ||
+ | } | ||
+ | |||
+ | class All extends | ||
+ | use Bar; | ||
| | ||
- | public function | + | public function |
+ | new self:> | ||
+ | new self:> | ||
+ | new self:> | ||
+ | |||
+ | new parent:> | ||
+ | | ||
} | } | ||
</ | </ | ||
- | Properties inside parentheses are automatically declared as class properties and default to public unless explicitly specified. | + | === static resolution === |
- | === Default Values === | + | The '' |
- | Properties with type hints may have default values: | + | <code php> |
+ | class Outer { | ||
+ | class Inner {} | ||
+ | |||
+ | // Fatal error: Cannot use the static modifier on a parameter | ||
+ | public function foo(static:> | ||
+ | |||
+ | // Parse error: syntax error, unexpected token ":>", | ||
+ | public function bar(): static:> | ||
+ | |||
+ | // Fatal error: Cannot use " | ||
+ | class Baz extends static:> | ||
+ | |||
+ | public function foobar() { | ||
+ | return new static:> | ||
+ | } | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | This is to prevent casual LSP violations of inheritance and to maintain the strong binding of inner classes. | ||
+ | |||
+ | === 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 | + | '' |
- | Short 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:> | ||
+ | } | ||
+ | } | ||
+ | } | ||
</ | </ | ||
- | === Empty Classes === | + | '' |
- | Short classes may be empty, in which case, the constructor | + | 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. |
<code php> | <code php> | ||
- | class HttpError() extends Exception; | + | class OuterParent { |
+ | class Inner {} | ||
+ | class Other {} | ||
+ | } | ||
- | throw new HttpError('Not Found', | + | 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:> | ||
+ | } | ||
+ | } | ||
</ | </ | ||
- | === Attributes | + | === Dynamic resolution |
- | Attributes can be used with short classes: | + | Just as with '' |
<code php> | <code php> | ||
- | # | + | // Using variables to dynamically instantiate inner classes: |
- | class Password(# | + | $outer = " |
+ | $inner = " | ||
+ | $instance = new $outer:> | ||
+ | |||
+ | // Instantiating inner class dynamically via a fully qualified class string: | ||
+ | $dynamicClassName = " | ||
+ | $instance = new $dynamicClassName(); | ||
</ | </ | ||
- | === Modifiers === | + | This provides flexibility and backwards compatibility for dynamic code that may not expect an inner class. |
- | Short classes | + | ==== Visibility from inner classes |
- | <code php> | + | 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. |
- | readonly | + | |
- | final class Config(string $key, mixed $value); | + | <code php> |
+ | class User { | ||
+ | public private(set) string $name; | ||
+ | public private(set) string $email; | ||
+ | |||
+ | private function __construct(self:> | ||
+ | $this-> | ||
+ | $this-> | ||
+ | } | ||
+ | |||
+ | public readonly | ||
+ | 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); | ||
+ | } | ||
+ | } | ||
+ | } | ||
- | abstract class Shape(float | + | $user = new User:> |
</ | </ | ||
- | === How it works === | + | This enables usages such as builder patterns and other helper classes to succinctly encapsulate behavior. |
- | Short classes are purely syntactic sugar and compiled into standard | + | ==== Visibility from outside the outer class ==== |
- | ==== Inner Classes ==== | + | Inner classes |
- | + | ||
- | Inner classes | + | |
<code php> | <code php> | ||
class Outer { | class Outer { | ||
- | class Inner(public | + | |
- | + | protected | |
- | | + | |
- | private class PrivateInner { | + | |
- | | + | $bar = new Outer:> |
+ | } | ||
} | } | ||
} | } | ||
- | $foo = new Outer::Inner(' | + | class SubOuter extends Outer { |
- | echo $foo-> | + | public function Foo() { |
- | // outputs: Hello, world! | + | |
- | $baz = new Outer:: | + | $bar = new parent:>Other(); // Cannot access private inner class 'Outer:> |
- | // Fatal error: Uncaught Error: | + | } |
+ | } | ||
</ | </ | ||
- | === 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 '' | + | <code php> |
+ | new Outer:>Inner(); // Cannot access | ||
+ | </ | ||
- | * '' | + | === Method return type and argument declarations === |
- | * '' | + | |
- | * '' | + | |
- | * '' | + | |
- | === Visibility Rules === | + | 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 '' |
- | Private and protected | + | ^Inner Class Visibility^Method Visibility^Allowed^ |
+ | |'' | ||
+ | |'' | ||
+ | |'' | ||
+ | |'' | ||
+ | |'' | ||
+ | |'' | ||
+ | |'' | ||
+ | |'' | ||
+ | |'' | ||
- | For example, you may return a private inner class from any method inside that same inner class or the outer class, but you may not use it as a type hint in a function outside the outer class: | + | Methods and functions outside the outer class are considered public |
<code php> | <code php> | ||
- | class Box { | + | class Outer { |
- | private class Point(int $x, int $y); | + | private class Inner {} |
- | + | ||
- | private self::Point $center; | + | public function |
- | + | return | |
- | public function __construct() | + | } |
- | | + | |
- | } | + | |
- | + | ||
- | | + | |
- | return | + | |
- | } | + | |
- | + | ||
- | public function setCenter(self::Point $center) { | + | |
- | | + | |
- | | + | |
} | } | ||
- | $box = new Box(); | + | // Fatal error: Uncaught TypeError: Public method getInner cannot return private class Outer:> |
- | $center = $box->getCenter(); | + | new Outer()->getInner(); |
- | $center-> | + | |
- | $box-> | + | |
- | var_dump($box); | + | |
</ | </ | ||
- | Outputs: | + | === 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. |
- | object(Box)# | + | |
- | | + | 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/ |
- | object(Box:: | + | |
- | | + | < |
- | | + | class Outer { |
- | | + | private |
- | int(0) | + | |
- | } | + | |
+ | | ||
+ | } | ||
} | } | ||
</ | </ | ||
- | However, if we try to use it outside the outer class as a type hint: | + | ==== Accessing outer classes ==== |
+ | |||
+ | Inner classes do not implicitly have access | ||
+ | |||
+ | Passing an outer instance explicitly remains an available option for accessing protected/ | ||
<code php> | <code php> | ||
- | function | + | class Message { |
- | $point->x = 10; | + | public |
+ | |||
+ | public function Serialize(): string | ||
+ | return new Message:> | ||
+ | | ||
+ | |||
+ | private class SerializedMessage { | ||
+ | public string | ||
+ | |||
+ | public function __construct(Message $message) { | ||
+ | $this->message | ||
+ | } | ||
+ | } | ||
} | } | ||
- | |||
- | $center = mutateBox($center); | ||
</ | </ | ||
- | We receive the following error: | + | ==== Reflection ==== |
+ | |||
+ | Several new methods are added to '' | ||
+ | |||
+ | <code php> | ||
+ | $reflection = new ReflectionClass(' | ||
- | <code> | + | $reflection->isInnerClass(); |
- | PHP Fatal error: | + | $reflection-> |
+ | $reflection-> | ||
+ | $reflection-> | ||
</ | </ | ||
- | This gives a great deal of control to developers, preventing accidental misuse of inner classes. Developers may have the inner class implement an interface so that the programmer must code to the interface, allowing large projects to enforce a consistent API. | + | For non-inner classes, |
- | === Inheritance | + | ==== Autoloading ==== |
- | Inner classes | + | Inner classes |
+ | |||
+ | ==== Usage ==== | ||
+ | |||
+ | Inner classes may be defined in the body of any class-like structure, including but not limited | ||
+ | |||
+ | * in a class body | ||
+ | * in an anonymous | ||
+ | * in an enum body | ||
+ | * in a trait body | ||
+ | * in an interface body | ||
+ | |||
+ | 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. | ||
+ | |||
+ | ==== 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. | ||
+ | |||
+ | Specifically: | ||
+ | |||
+ | * An abstract outer class can define concrete (non-abstract) inner classes. Inner classes remain instantiable, | ||
+ | * 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 | ||
+ | |||
+ | Examples: | ||
+ | |||
+ | <code 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:> | ||
+ | </ | ||
<code php> | <code php> | ||
- | readonly class Point(int | + | readonly class ImmutableCollection { |
+ | private array $items; | ||
- | class Geometry { | + | |
- | | + | $this->items = $items; |
- | protected | + | |
- | $this->points | + | |
} | } | ||
- | | + | |
- | | + | public function |
- | | + | |
- | | + | |
- | } | + | |
} | } | ||
- | | + | |
- | public class FromCoordinates extends Geometry | + | public class Builder |
- | public function __construct(int ...$coordinates) { | + | private array $items; |
- | $points | + | |
- | for ($i = 0; $i < count($coordinates); | + | public function __construct(array $items) { |
- | $points[] = new Point($coordinates[$i], | + | $this-> |
- | } | + | |
- | parent:: | + | |
} | } | ||
- | } | ||
- | } | ||
- | class Triangle extends Geometry { | + | public |
- | protected | + | $this-> |
- | | + | |
- | } | + | |
- | + | ||
- | public class FromPoints extends Triangle { | + | |
- | public function __construct(Point $p1, Point $p2, Point $p3) { | + | |
- | | + | |
} | } | ||
- | } | + | |
- | + | public function | |
- | public class FromCoordinates extends Triangle { | + | |
- | public function | + | |
- | | + | |
} | } | ||
} | } | ||
} | } | ||
- | |||
- | $t = new Triangle:: | ||
- | |||
- | var_dump($t instanceof Triangle); // true | ||
- | var_dump($t instanceof Geometry); // true | ||
- | var_dump($t instanceof Triangle:: | ||
</ | </ | ||
- | However, no classes may not inherit from inner classes, but inner classes may inherit from other classes, including | + | In this example, even though ImmutableBuilder is a mutable |
- | === Names === | + | ==== Abstract inner classes ==== |
- | Inner classes | + | It is worth exploring what an '' |
+ | |||
+ | 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 286: | 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 ==== | ||
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. | 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, | ||
===== Open Issues ===== | ===== Open Issues ===== | ||
Line 302: | Line 483: | ||
===== Future Scope ===== | ===== Future Scope ===== | ||
- | * inner enums | + | * Inner enums |
+ | * Inner interfaces | ||
+ | * Inner traits | ||
+ | * '' | ||
===== Proposed Voting Choices ===== | ===== Proposed Voting Choices ===== | ||
Line 311: | Line 495: | ||
===== Patches and Tests ===== | ===== Patches and Tests ===== | ||
- | A complete implementation is available [[https:// | + | To be completed. |
===== Implementation ===== | ===== Implementation ===== | ||
Line 324: | Line 508: | ||
===== Rejected Features ===== | ===== Rejected Features ===== | ||
- | * " | + | TBD |
rfc/short-and-inner-classes.1741212772.txt.gz · Last modified: 2025/03/05 22:12 by withinboredom