rfc:short-and-inner-classes
Differences
This shows you the differences between two versions of the page.
Both sides previous revisionPrevious revision | |||
rfc:short-and-inner-classes [2025/03/14 18:42] – added reference to PR 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.2 | + | * Version: 0.4 |
* Date: 2025-02-08 | * Date: 2025-02-08 | ||
* Author: Rob Landers, < | * Author: Rob Landers, < | ||
Line 11: | Line 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. | 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 " | + | **Inner Classes** allows for the ability |
+ | |||
+ | PHP developers currently rely heavily on annotations (e.g., @internal) or naming conventions | ||
+ | |||
+ | This RFC introduces Inner Classes to clearly communicate and enforce boundaries around encapsulated | ||
===== Proposal ===== | ===== Proposal ===== | ||
Line 35: | Line 39: | ||
</ | </ | ||
- | ==== 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/ |
- | Inner classes support modifiers such as '' | + | ==== Declaration Syntax ==== |
- | * '' | + | Declaring an inner class is just like declaring any other class. You can define |
- | * '' | + | |
- | * '' | + | |
- | If an inner class does not have any modifiers | + | 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. |
+ | |||
+ | <code php> | ||
+ | class Outer { | ||
+ | private class Inner {} | ||
+ | protected readonly class ReadOnlyInner {} | ||
+ | public abstract class AbstractInner {} | ||
+ | public final class FinalInner {} | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | If an inner class is not defined | ||
+ | |||
+ | ==== Syntax rationale ==== | ||
+ | |||
+ | The '' | ||
+ | |||
+ | * Using '' | ||
+ | * Using '' | ||
+ | |||
+ | '' | ||
==== Binding ==== | ==== Binding ==== | ||
- | Inner classes are **strongly** | + | Inner classes are explicitly |
<code php> | <code php> | ||
class Outer { | class Outer { | ||
- | | + | class OuterInner |
} | } | ||
- | class OuterV2 extends Outer { | + | interface Foo { |
- | | + | class FooInner |
} | } | ||
- | </ | ||
- | In the above listing, | + | trait Bar { |
+ | class BarInner {} | ||
+ | } | ||
+ | |||
+ | class All extends Outer implements Foo { | ||
+ | use Bar; | ||
+ | |||
+ | public function does_not_work() { | ||
+ | new self:> | ||
+ | new self:>FooInner(); // Fatal error: Class 'All:> | ||
+ | new self:> | ||
+ | |||
+ | new parent:> | ||
+ | } | ||
+ | } | ||
+ | </ | ||
=== static resolution === | === static resolution === | ||
Line 123: | Line 159: | ||
</ | </ | ||
- | '' | + | '' |
- | + | ||
- | <code php> | + | |
- | // this is currently an error | + | |
- | class Outer extends self { | + | |
- | // this extends Outer | + | |
- | class Inner extends self {} | + | |
- | } | + | |
- | </ | + | |
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. | 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. | ||
Line 162: | Line 190: | ||
<code php> | <code php> | ||
- | new $outer:> | + | // Using variables to dynamically instantiate inner classes: |
+ | $outer = " | ||
+ | $inner = " | ||
+ | $instance = new $outer:> | ||
- | $dynamic | + | // Instantiating inner class dynamically via a fully qualified class string: |
- | new $dynamic(); | + | $dynamicClassName |
+ | $instance = new $dynamicClassName(); | ||
</ | </ | ||
This provides flexibility and backwards compatibility for dynamic code that may not expect an inner class. | This provides flexibility and backwards compatibility for dynamic code that may not expect an inner class. | ||
- | ==== Visibility | + | ==== Visibility |
- | Inner classes | + | Inner classes |
- | === Instantiation | + | <code php> |
+ | class User { | ||
+ | public private(set) string $name; | ||
+ | public private(set) string $email; | ||
+ | |||
+ | private function __construct(self:> | ||
+ | $this-> | ||
+ | $this-> | ||
+ | } | ||
+ | |||
+ | 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-> | ||
+ | } | ||
+ | |||
+ | public function withName(string $name): self { | ||
+ | return new self($name, $this-> | ||
+ | } | ||
+ | |||
+ | public function build(): User { | ||
+ | return new User($this); | ||
+ | } | ||
+ | } | ||
+ | } | ||
+ | |||
+ | $user = new User:> | ||
+ | </ | ||
+ | |||
+ | This enables usages such as builder patterns and other helper classes to succinctly encapsulate behavior. | ||
+ | |||
+ | ==== Visibility from outside the outer class ==== | ||
- | Private and protected inner classes are only instantiatable within | + | Inner classes are not visible outside |
<code php> | <code php> | ||
Line 183: | Line 247: | ||
protected class Inner { | protected class Inner { | ||
public function Foo() { | public function Foo() { | ||
- | $bar = new self(); // allowed | ||
$bar = new Outer:> | $bar = new Outer:> | ||
$bar = new Outer:> | $bar = new Outer:> | ||
Line 192: | Line 255: | ||
class SubOuter extends Outer { | class SubOuter extends Outer { | ||
public function Foo() { | public function Foo() { | ||
- | $bar = new self:> | + | $bar = new parent:> |
- | $bar = new self:> | + | $bar = new parent:> |
} | } | ||
} | } | ||
Line 201: | Line 264: | ||
<code php> | <code php> | ||
- | new Outer:> | + | new Outer:> |
</ | </ | ||
=== Method return type and argument declarations === | === 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 '' | + | Inner classes may only be used as a return type or argument declarations for methods |
^Inner Class Visibility^Method Visibility^Allowed^ | ^Inner Class Visibility^Method Visibility^Allowed^ | ||
Line 219: | Line 282: | ||
|'' | |'' | ||
- | Methods and functions outside the outer class are considered | + | Methods and functions outside the outer class are considered public |
<code php> | <code php> | ||
Line 230: | Line 293: | ||
} | } | ||
- | // Fatal error: Uncaught TypeError: | + | // Fatal error: Uncaught TypeError: |
new Outer()-> | new Outer()-> | ||
</ | </ | ||
Line 236: | Line 299: | ||
=== Properties === | === 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 |
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/ | 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/ | ||
Line 250: | Line 313: | ||
</ | </ | ||
- | === Accessing outer classes === | + | ==== Accessing outer classes |
- | There is no direct | + | 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 |
+ | |||
+ | Passing an outer instance explicitly remains an available option for accessing protected/ | ||
<code php> | <code php> | ||
- | class Parser | + | class Message |
- | public | + | public function __construct(private string $message, private string $from, private string |
- | private | + | |
- | // we can access $parser-> | + | |
- | | + | return new Message:> |
- | } | + | |
} | } | ||
| | ||
- | private class ParserState | + | private class SerializedMessage |
- | | + | |
- | | + | |
- | + | | |
- | | + | |
- | 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. | ||
==== Reflection ==== | ==== Reflection ==== | ||
Line 288: | Line 350: | ||
</ | </ | ||
- | When these methods are called on outer classes, '' | + | For non-inner |
==== Autoloading ==== | ==== 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 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 ==== | ==== 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 a class body | ||
* in an anonymous 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 | + | Note: While traits and interfaces may define |
- | 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 | + | 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 | + | * An abstract outer class can define concrete (non-abstract) |
+ | * A final outer class does not force its inner classes to be final. Inner classes within | ||
+ | * A readonly | ||
- | 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 | + | <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> | ||
+ | readonly class ImmutableCollection { | ||
+ | private array $items; | ||
+ | |||
+ | public function __construct(array $items) { | ||
+ | $this-> | ||
+ | } | ||
+ | |||
+ | public function getMutableBuilder(): | ||
+ | return new self:> | ||
+ | } | ||
+ | |||
+ | public class Builder { | ||
+ | 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-> | ||
+ | } | ||
+ | } | ||
+ | } | ||
+ | </ | ||
- | This is very similar to other languages that support | + | In this example, even though ImmutableBuilder |
==== Abstract inner classes ==== | ==== Abstract inner classes ==== | ||
Line 340: | Line 433: | ||
<code php> | <code php> | ||
class OuterParent { | class OuterParent { | ||
- | protected abstract class Inner {} | + | protected abstract class InnerBase |
} | } | ||
- | // Middle is not required | + | // Middle is allowed, does not have to implement |
class Middle extends OuterParent {} | class Middle extends OuterParent {} | ||
+ | // Last demonstrates extending the abstract inner class explicitly. | ||
class Last extends OuterParent { | class Last extends OuterParent { | ||
- | | + | private abstract class InnerExtended |
- | | + | |
} | } | ||
</ | </ | ||
Line 375: | Line 468: | ||
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 398: | Line 495: | ||
===== Patches and Tests ===== | ===== Patches and Tests ===== | ||
- | https:// | + | To be completed. |
===== Implementation ===== | ===== Implementation ===== |
rfc/short-and-inner-classes.1741977749.txt.gz · Last modified: 2025/03/14 18:42 by withinboredom