rfc:short-and-inner-classes

Differences

This shows you the differences between two versions of the page.

Link to this comparison view

Both sides previous revisionPrevious revision
Next revision
Previous revision
rfc:short-and-inner-classes [2025/03/12 09:05] – major rewrite to better showcase features and rules withinboredomrfc: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, <rob@bottled.codes>   * Author: Rob Landers, <rob@bottled.codes>
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 "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 insideThis feature is not meant to be used as a "module" systembut 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 visibilityMany 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 ===== ===== Proposal =====
Line 35: Line 39:
 </code> </code>
  
-==== 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. 
 + 
 +<code php> 
 +class Outer { 
 +    private class Inner {} 
 +    protected readonly class ReadOnlyInner {} 
 +    public abstract class AbstractInner {} 
 +    public final class FinalInner {} 
 +
 +</code> 
 + 
 +If an inner class is not defined as ''%%private%%'', ''%%protected%%'', or ''%%public%%'', it is assumed to be ''%%public%%''
 + 
 +==== Syntax rationale ====
  
-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:+The ''%%:>%%'' syntax was selected after evaluating alternatives:
  
-  * ''%%public%%'', ''%%private%%'', and ''%%protected%%'' apply to the **visibility** of the inner class+  * Using ''%%::%%'' was cumbersome and visually ambiguous due to existing static resolution syntax
-  * ''%%final%%''''%%readonly%%'', and ''%%abstract%%'' apply **to the inner class itself**. +  * Using ''%%\%%'' or ''%%\\%%'' was rejected due to potential conflicts with namespaces and escaping, causing confusion.
-  * ''%%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 does not have any modifiers defined, it is assumed to be ''%%public%%'' by default.+''%%:>%%'' communicates "inner membership," visually linking the inner class to the outer class.
  
 ==== Binding ==== ==== 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 classthe child’s inner class is distinctThe following example best shows this:+Inner classes are explicitly bound to their outer classes and are intentionally not inherited through subclassingtraits, or interfaces. This prevents ambiguity or complexity arising from inheriting tightly bound encapsulated logic.
  
 <code php> <code php>
 class Outer { class Outer {
-    protected class Inner {}+    class OuterInner {}
 } }
  
-class OuterV2 extends Outer +interface Foo 
-    protected class Inner {}+    class FooInner {}
 } }
-</code> 
  
-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:>OuterInnernot found 
 +        new self:>FooInner(); // Fatal error: Class 'All:>FooInnernot found 
 +        new self:>BarInner(); // Fatal error: Class 'All:>BarInnernot found 
 +         
 +        new parent:>OuterInner(); // This does work because it resolves to Outer:>OuterInner 
 +    } 
 +
 +</code>
  
 === static resolution === === static resolution ===
Line 123: Line 159:
 </code> </code>
  
-''%%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%%''. +''%%self%%'' explicitly resolves to the current class body it is written in, just like with ''%%parent%%''. On ''%%inner%%'' classes.
- +
-<code php> +
-// this is currently an error +
-class Outer extends self { +
-    // this extends Outer +
-    class Inner extends self {} +
-+
-</code>+
  
 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:>$inner+// Using variables to dynamically instantiate inner classes: 
 +$outer = "Outer"; 
 +$inner = "Inner"; 
 +$instance = new $outer:>$inner();
  
-$dynamic = "Outer:>Inner"; +// Instantiating inner class dynamically via a fully qualified class string: 
-new $dynamic();+$dynamicClassName = "Outer:>Inner"; 
 +$instance = new $dynamicClassName();
 </code> </code>
  
 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 Rules ====+==== Visibility from inner classes ====
  
-=== Instantiation ===+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.
  
-Private and protected inner classes are only instantiatable within their outer class (or subclasses for protected). Since inner classes are inside outer classesthey can instantiate other private, protectedor public classes of the outer class if it is visible to them.+<code php> 
 +class User { 
 +    public private(setstring $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 = nullpublic 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(); 
 +</code> 
 + 
 +This enables usages such as builder patterns and other helper classes to succinctly encapsulate behavior. 
 + 
 +==== 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.
  
 <code php> <code php>
Line 181: Line 247:
     protected class Inner {     protected class Inner {
         public function Foo() {         public function Foo() {
-            $bar = new self(); // allowed 
             $bar = new Outer:>Inner(); // allowed             $bar = new Outer:>Inner(); // allowed
             $bar = new Outer:>Other(); // allowed             $bar = new Outer:>Other(); // allowed
Line 190: Line 255:
 class SubOuter extends Outer { class SubOuter extends Outer {
     public function Foo() {     public function Foo() {
-        $bar = new self:>Inner(); // allowed to access protected inner class +        $bar = new parent:>Inner(); // allowed to access protected inner class 
-        $bar = new self:>Other(); // Fatal error: Class 'Outer:>Other' is private+        $bar = new parent:>Other(); // Cannot access private inner class 'Outer:>Other'
     }     }
 } }
Line 199: Line 264:
  
 <code php> <code php>
-new Outer:>Inner(); // Fatal error: Class 'Outer:>Inner' is private+new Outer:>Inner(); // Cannot access protected inner class 'Outer:>Inner'
 </code> </code>
  
 === 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 ''%%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^ ^Inner Class Visibility^Method Visibility^Allowed^
Line 217: Line 282:
 |''%%private%%''       |''%%private%%''  |Yes    | |''%%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%%'':
  
 <code php> <code php>
Line 228: Line 293:
 } }
  
-// Fatal error: Uncaught TypeError: Method getInner is public but returns a private classOuter:>Inner+// Fatal error: Uncaught TypeError: Public method getInner cannot return private class Outer:>Inner
 new Outer()->getInner(); new Outer()->getInner();
 </code> </code>
Line 234: 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 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: 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:
Line 248: Line 313:
 </code> </code>
  
-=== Accessing outer classes ===+==== 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.+Inner classes do not implicitly have access to an outer class instance. This design choice avoids implicit coupling between classes and keeps inner classes simplerfacilitating 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.
  
 <code php> <code php>
-class Parser +class Message 
-    public class ParseError extends Exception { +    public function __construct(private string $message, private string $from, private string $to) {} 
-        private function __construct(Parser $parser) { +     
-            // we can access $parser->state here +    public function Serialize()string { 
-            parent::__construct(/* ... */); +        return new Message:>SerializedMessage($this)->message;
-        }+
     }     }
          
-    private class ParserState {+    private class SerializedMessage 
-     +        public string $message
-    private self:>ParserState $state+         
-     +        public function __construct(Message $message) { 
-    private function throwParserError(): never +            $this->message = strlen($message->from. $message->from . strlen($message->to) . $message->to . strlen($message->message) . $message->message; 
-        throw new self:>ParseError($this);+        }
     }     }
 } }
 </code> </code>
- 
-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 286: Line 350:
 </code> </code>
  
-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 ==== ==== Autoloading ====
Line 292: Line 356:
 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 ====+==== Usage ====
  
-Inner classes support all features of regular classes, including:+Inner classes may be defined in the body of any class-like structure, including but not limited to:
  
-  * Properties: both static and instanced +  * in a class body 
-  * Methods: both static and instanced +  * in an anonymous class body 
-  * Property hooks +  * in an enum body 
-  * Magic methods +  * in a trait body 
-  * Traits +  * in an interface body 
-  * Interfaces + 
-  * Abstract classes +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. 
-  * Final classes + 
-  * Readonly classes +==== Outer class effects ==== 
-  * Class constants+ 
 +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, 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: 
 + 
 +<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:>Implementation(); 
 +</code> 
 + 
 +<code 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); 
 +        } 
 +    } 
 +
 +</code> 
 + 
 +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. 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. 
 + 
 +<code php> 
 +class OuterParent { 
 +    protected abstract class InnerBase {} 
 +
 + 
 +// 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 { 
 +    private abstract class InnerExtended extends OuterParent:>InnerBase {} 
 +
 +</code>
  
 ===== Backward Incompatible Changes ===== ===== Backward Incompatible Changes =====
Line 330: 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, static analysis, and code generation tools.
  
 ===== Open Issues ===== ===== Open Issues =====
Line 353: Line 495:
 ===== Patches and Tests ===== ===== 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 ===== ===== Implementation =====
rfc/short-and-inner-classes.1741770338.txt.gz · Last modified: 2025/03/12 09:05 by withinboredom