rfc:records

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:records [2024/08/02 09:07] – update examples and add more details withinboredomrfc:records [2024/11/17 21:38] (current) withinboredom
Line 3: Line 3:
   * Version: 0.9   * Version: 0.9
   * Date: 2024-07-19   * Date: 2024-07-19
-  * Author: Robert Landers, <landers.robert@gmail.com> +  * Author: Robert Landers, <landers.robert@gmail.com>, <rob@bottled.codes
-  * Status: Draft (or Under Discussion or Accepted or Declined)+  * Status: Under Discussion (or Accepted or Declined)
   * First Published at: http://wiki.php.net/rfc/records   * First Published at: http://wiki.php.net/rfc/records
  
Line 13: Line 13:
 ==== Value objects ==== ==== Value objects ====
  
-Value objects are immutable objects that represent a value. They’re used for storing values with a different meaning than their technical value, adding additional semantic context to the value. For example, a ''%%Point%%'' object with ''%%x%%'' and ''%%y%%'' properties can represent a point in a 2D space, and an ''%%ExpirationDate%%'' can represent a date when something expires. This prevents developers from accidentally using the wrong value in the wrong context.+Value objects are immutable objects that represent a value. They’re used to store values with a different semantic by wrapping their technical value, adding additional context. For example, a ''%%Point%%'' object with ''%%x%%'' and ''%%y%%'' properties can represent a point in a 2D space, and an ''%%ExpirationDate%%'' can represent a date when something expires. This prevents developers from accidentally using the wrong value in the wrong context.
  
-Consider this example where a function accepts an integer as a user ID, and the id is accidentally set to a non-sensical value:+Consider this example where a function accepts an integer as a user ID, and the ID is accidentally set to a nonsensical value:
  
 <code php> <code php>
Line 25: Line 25:
 $uid = $user->id; $uid = $user->id;
 // ... // ...
-$uid = 5; // somehow accidentally sets uid to an unrelated integer+$uid = 5; // accidentally sets uid to an unrelated integer
 // ... // ...
-updateUserRole($uid, 'admin'); // accidental passing of non-sensical value for uid+updateUserRole($uid, 'admin'); // accidental passes nonsensical value for uid
 </code> </code>
  
-Currently, the only solution to this is to use a class, but this requires a lot of boilerplate code. Further, ''%%readonly%%'' classes have a lot of edge cases and are rather unwieldy.+Currently, the only solution to this is to use a **class**, but this requires significant boilerplate code. Further, **readonly classes** have many edge cases and are rather unwieldy.
  
 === The solution === === The solution ===
  
-Like arrays, strings, and other values, ''%%record%%'' objects are strongly equal to each other if they contain the same values.+Like arrays, strings, and other values, **record** objects are strongly equal (''%%===%%''to each other if they contain the same values.
  
-Let’s take a look at the updated exampleusing a ''%%record%%'' type for ''%%UserId%%''. Thus, if someone were to pass an ''%%int%%'' to ''%%updateUserRole%%'', it would throw an error:+Let’s take a look at an updated example using a ''%%record%%'' type for ''%%UserId%%''. Thus, if someone were to pass an ''%%int%%'' to ''%%updateUserRole%%'', it would throw an error:
  
 <code php> <code php>
Line 50: Line 50:
 $uid = 5; $uid = 5;
 // ... // ...
-updateUserRole($uid, 'admin'); // This will throw an error+updateUserRole($uid, 'admin'); // This will throw a TypeError
 </code> </code>
  
Line 57: Line 57:
 ===== Proposal ===== ===== Proposal =====
  
-This RFC proposes the introduction of a new record keyword in PHP to define immutable data objects. These objects will allow properties to be initialized concisely and will provide built-in methods for common operations such as modifying properties and equality checks using a function-like instantiation syntax. Records can implement interfaces and use traits but can’t extend other records or classes; composition is allowed, however.+This RFC proposes the introduction of a ''%%record%%'' keyword in PHP to define immutable value objects. These objects will allow properties to be initialized concisely and will provide built-in methods for common operations such as modifying properties, performing equality checks, and using a function-like instantiation syntax. Records can implement interfaces and use traits but can’t extend other records or classes; composition is allowed, however.
  
 ==== Syntax and semantics ==== ==== Syntax and semantics ====
Line 63: Line 63:
 === Definition === === Definition ===
  
-A **record** is defined by the keyword ''%%record%%'', followed by the name of its type (e.g., ''%%UserId%%''), and then must list one or more typed parameters (e.g., ''%%int $id%%'') that become properties of the record. A parameter may provide ''%%private%%'' or ''%%public%%'' modifiers, but are ''%%public%%'' by default. This is referred to as the "inline constructor."+A **record** is defined by the keyword ''%%record%%'', followed by the name of its type (e.g., ''%%UserId%%''), and then must list one or more typed parameters (e.g., ''%%int $id%%'') that become properties of the record. A parameter may provide ''%%private%%'' or ''%%public%%'' modifiers, but are ''%%public%%'' when not specified. This is referred to as the "inline constructor."
  
 A **record** may optionally implement an interface using the ''%%implements%%'' keyword, which may optionally be followed by a record body enclosed in curly braces ''%%{}%%''. A **record** may optionally implement an interface using the ''%%implements%%'' keyword, which may optionally be followed by a record body enclosed in curly braces ''%%{}%%''.
Line 69: Line 69:
 A **record** may not extend another record or class. A **record** may not extend another record or class.
  
-A **record** may contain a traditional constructor with zero arguments to perform further initialization, but if it does, it must take zero arguments.+A **record** may contain a traditional constructor with zero arguments to perform further initialization.
  
 A **record** body may contain property hooks, methods, and use traits. A **record** body may contain property hooks, methods, and use traits.
  
-A **record** body may also declare properties whose values are only mutable during a constructor call. At any other time the property is immutable.+A **record** body may also declare properties whose values are only mutable during a constructor call. At any other timethe property is immutable.
  
-A **record** body may also contain static methods and properties, which behave identically to class static methods and properties. They may be accessed using the ''%%::%%'' operator.+A **record** body may also contain static methods and properties, which behave identically to static methods and properties in classes. They may be accessed using the ''%%::%%'' operator
 + 
 +As an example, the following code defines a **record** named ''%%Pigment%%'' to represent a color, ''%%StockPaint%%'' to represent paint colors in stock, and ''%%PaintBucket%%'' to represent a collection of stock paints mixed together. The actual behavior isn’t important, but illustrates the syntax and semantics of records.
  
 <code php> <code php>
Line 126: Line 128:
 === Usage === === Usage ===
  
-A record may be used as readonly class, as the behavior of the two is very similar, which should be able to assist in migrating from one implementation to another.+A record may be used much like a class, as the behavior of the two is very similar, assisting in migrating from one implementation to another
 + 
 +<code php> 
 +$gray = $bucket->mixIn($blackPaint)->mixIn($whitePaint); 
 +</code> 
 + 
 +Records are instantiated in a function format, with ''%%&%%'' prependedThis provides visual feedback that a record is being created instead of a function call. 
 + 
 +<code php> 
 +$black = &Pigment(0, 0, 0); 
 +$white = &Pigment(255, 255, 255); 
 +$blackPaint = &StockPaint($black, 1); 
 +$whitePaint = &StockPaint($white, 1); 
 +$bucket = &PaintBucket(); 
 + 
 +$gray = $bucket->mixIn($blackPaint)->mixIn($whitePaint); 
 +$grey = $bucket->mixIn($blackPaint)->mixIn($whitePaint); 
 + 
 +assert($gray === $grey); // true 
 +</code>
  
 === Optional parameters and default values === === Optional parameters and default values ===
Line 132: Line 153:
 A ''%%record%%'' can also be defined with optional parameters that are set if omitted during instantiation. A ''%%record%%'' can also be defined with optional parameters that are set if omitted during instantiation.
  
-One or more properties defined in the inline constructor may have a default value declared using the same syntax and rules as any other default parameter declared in methods/functions. If a property has a default value, it is optional when instantiating the record and PHP will assign the default value to the property.+One or more properties defined in the inline constructor may have a default value declared using the same syntax and rules as any other default parameter in methods/functions. If a property has a default value, it is optional when instantiating the recordand PHP will assign the default value to the property if omitted.
  
 <code php> <code php>
 record Rectangle(int $x, int $y = 10); record Rectangle(int $x, int $y = 10);
-var_dump(Rectangle(10)); // output a record with x: 10 and y: 10+var_dump(&Rectangle(10)); // output a record with x: 10 and y: 10
 </code> </code>
  
 === Auto-generated with method === === Auto-generated with method ===
  
-To enhance the usability of records, the RFC proposes automatically generating a ''%%with%%'' method for each record. This method allows for partial updates of properties, creating a new instance of the record with the specified properties updated.+To make records more useful, the RFC proposes generating a ''%%with%%'' method for each record. This method allows for partial updates to the properties, creating a new instance of the record with the specified properties updated.
  
-The auto-generated ''%%with%%'' method accepts only named arguments defined in the constructor, except variadic arguments. For variadic arguments, these don’t require named arguments. No other property names can be used, and it returns a record object with the given values.+== How the with method works ==
  
-Example showing how this works with properties:+**Named arguments** 
 + 
 +The ''%%with%%'' method accepts only named arguments defined in the inline constructor. Properties not defined in the inline constructor can’t be updated by this method. 
 + 
 +**Variadic arguments** 
 + 
 +Variadic arguments from the inline constructor don’t require named arguments in the ''%%with%%'' method. However, mixing named and variadic arguments in the same ''%%with%%'' method call is not allowed by PHP syntax. 
 + 
 +Using named arguments:
  
 <code php> <code php>
 record UserId(int $id) { record UserId(int $id) {
   public string $serialNumber;   public string $serialNumber;
-  +
   public function __construct() {   public function __construct() {
     $this->serialNumber = "U{$this->id}";     $this->serialNumber = "U{$this->id}";
Line 156: Line 185:
 } }
  
-$userId = UserId(1); +$userId = &UserId(1); 
-$otherId = $userId->with(2); // failure due to not using named arguments +$otherId = $userId->with(2); // Fails: Named arguments must be used 
-$otherId = $userId->with(serialNumber: "U2"); // serialNumber is not defined in the inline constructor +$otherId = $userId->with(serialNumber: "U2"); // Error: serialNumber is not defined in the inline constructor 
-$otherId = $userId->with(id: 2); // success+$otherId = $userId->with(id: 2); // Success: id is updated
 </code> </code>
  
-Example showing how this works with variadic arguments, take note that PHP doesn’t allow sending variadic arguments with named arguments. Thus, the developer may need to perform multiple operations to construct a new record from an existing one using variadic arguments.+Using variadic arguments:
  
 <code php> <code php>
 record Vector(int $dimensions, int ...$values); record Vector(int $dimensions, int ...$values);
  
-$vector = Vector(3, 1, 2, 3); +$vector = &Vector(3, 1, 2, 3); 
-$vector = $vector->with(4, 5, 6); // automatically sent to $values +$vector = $vector->with(dimensions: 4); // Success: values are updated 
-$vector = $vector->with(dimensions: 4, 1, 2, 3, 4); // not allowed by PHP syntax +$vector = $vector->with(dimensions: 4, 1, 2, 3, 4); // Error: mixing named arguments with variadic arguments is not allowed by PHP syntax 
-$vector = $vector->with(dimensions: 4)->with(1, 2, 3, 4); // set dimensions firstthe set values.+$vector = $vector->with(dimensions: 4)->with(1, 2, 3, 4); // Success: First update dimensions, then values
 </code> </code>
- 
-This may look a bit confusing at first glance, but PHP currently doesn’t allow mixing named arguments with variadic arguments. 
  
 == Custom with method == == Custom with method ==
  
-A developer may define their own ''%%with%%'' method if they so choose, and reference the generated ''%%with%%'' method using ''%%parent::with()%%''. This allows a developer to define policies or constraints on how data is updated.+A developer may define their own ''%%with%%'' method if they choose, and reference the generated ''%%with%%'' method using ''%%parent::with()%%''. This allows a developer to define policies or constraints on how data can change from instance to instance.
  
 <code php> <code php>
 record Planet(string $name, int $population) { record Planet(string $name, int $population) {
   // create a with method that only accepts population updates   // create a with method that only accepts population updates
-  public function with(int $population) {+  public function with(int $population): Planet {
     return parent::with(population: $population);     return parent::with(population: $population);
   }   }
Line 190: Line 217:
 $pluto = $pluto->with(population: 1); $pluto = $pluto->with(population: 1);
 // and then we changed the name // and then we changed the name
-$mickey = $pluto->with(name: "Mickey"); // ERROR: no named argument for population+$mickey = $pluto->with(name: "Mickey"); // Error: no named argument for population
 </code> </code>
  
 === Constructors === === Constructors ===
  
-A **record** has two concepts of construction: the inline constructor and the traditional constructor.+A **record** has two types of constructors: the inline constructor and the traditional constructor
 + 
 +The inline constructor is always required and must define at least one parameter. The traditional constructor is optional and can be used for further initialization logic, but must not accept any arguments.
  
-The inline constructor is always required and must define at least one parameter. The traditional constructor is optional and can be used for further initialization logic; it must take zero arguments.+When a traditional constructor exists and is called, the properties are already initialized to the values from the inline constructor and are mutable until the end of the method, at which point they become immutable.
  
 <code php> <code php>
-// Inline constructor +// Inline constructor defining two properties 
-record User(string $name, string $email) {+record User(string $name, string $emailAddress) {
   public string $id;   public string $id;
  
   // Traditional constructor   // Traditional constructor
   public function __construct() {   public function __construct() {
-    if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {+    if (!is_valid_email($this->emailAddress)) {
       throw new InvalidArgumentException("Invalid email address");       throw new InvalidArgumentException("Invalid email address");
     }     }
          
-    $this->id = hash('sha256', $email); +    $this->id = hash('sha256', $this->emailAddress); 
-    $this->name = ucwords($name);+    $this->name = ucwords($this->name); 
 +    // all properties are now immutable
   }   }
 } }
 </code> </code>
  
-When traditional constructor exists and is calledthe properties are already initialized to the value of the inline constructor and are mutable until the end of the methodat which point they become immutable.+==== Implementing Interfaces ==== 
 + 
 +A **record** can implement interfaces, but it cannot extend other records or classes, but may use traits: 
 + 
 +<code php> 
 +interface Vehicle {} 
 + 
 +interface Car extends Vehicle { 
 +  public function drive(): void; 
 +
 + 
 +interface SpaceShip extends Vehicle { 
 +  public function launch(): void; 
 +
 + 
 +record FancyCar(string $model) implements Car { 
 +  public function drive(): void { 
 +    echo "Driving Fancy Car {$this->model}"; 
 +  } 
 +
 + 
 +record SpaceCar(string $model) implements CarSpaceShip { 
 +  public function drive(): void { 
 +    echo "Driving a Space Car {$this->model}"; 
 +  } 
 +   
 +  public function launch(): void { 
 +    echo "Launching a Space Car {$this->model}"; 
 +  } 
 +
 + 
 +record Submarine(string $model) implements Vehicle { 
 +  use Submersible; 
 +
 + 
 +record TowTruct(string $modelprivate Car $towing) implements Car { 
 +  use Towable; 
 +
 +</code>
  
 ==== Mental models and how it works ==== ==== Mental models and how it works ====
  
-From the perspective of a developer, declaring a record declares an object and function with the same name. The developer can consider the record function (the inline constructor) as a factory function that creates a new object or uses an existing object from an array.+From the perspective of a developer, declaring a record declares an object with the same name. The developer can consider the record function (the inline constructor) as a factory function that creates a new object or retrieves an existing object from an array.
  
 For example, this would be a valid mental model for a Point record: For example, this would be a valid mental model for a Point record:
  
 <code php> <code php>
-record Point(int $x, int $y);+record Point(int $x, int $y) 
 +    public float $magnitude; 
 +     
 +    public function __construct() { 
 +        $this->magnitude = sqrt($this->x ** 2 + $this->y ** 2); 
 +    } 
 + 
 +    public function add(Point $point): Point { 
 +        return &Point($this->x + $point->x, $this->y + $point->y); 
 +    } 
 +     
 +    public function dot(Point $point): int { 
 +        return $this->x * $point->x + $this->y * $point->y; 
 +    } 
 +}
  
 // similar to declaring the following function and class // similar to declaring the following function and class
  
-class Point {+// used during construction to allow mutability 
 +class Point_Implementation {
     public int $x;     public int $x;
     public int $y;     public int $y;
 +    public float $magnitude;
 +
 +    public function __construct() {
 +        $this->magnitude = sqrt($this->x ** 2 + $this->y ** 2);
 +    }
 +
 +    public function with(...$parameters) {
 +        // validity checks omitted for brevity
 +        $parameters = array_merge([$this->x, $this->y], $parameters);
 +        return Point(...$parameters);
 +    }
          
-    public function __construct() {}+    public function add(Point $point): Point { 
 +        return Point($this->x + $point->x, $this->y + $point->y); 
 +    } 
 +     
 +    public function dot(Point $point): int { 
 +        return $this->x * $point->x + $this->y * $point->y; 
 +    } 
 +
 + 
 +// used to enforce immutability but has nearly the same implementation 
 +readonly class Point { 
 +    public float $magnitude; 
 + 
 +    public function __construct(public int $x, public int $y) {} 
 + 
 +    public function with(...$parameters): self { 
 +        // validity checks omitted for brevity 
 +        $parameters = array_merge([$this->x, $this->y], $parameters); 
 +        return Point(...$parameters); 
 +    } 
 +     
 +    public function add(Point $point): Point { 
 +        return Point($this->x + $point->x, $this->y + $point->y); 
 +    } 
 +     
 +    public function dot(Point $point): int { 
 +        return $this->x * $point->x + $this->y * $point->y; 
 +    }
 } }
  
 function Point(int $x, int $y): Point { function Point(int $x, int $y): Point {
     static $points = [];     static $points = [];
-    $key = "$x,$y";+     
 +    $key = hash_object($mutablePoint);
     if ($points[$key] ?? null) {     if ($points[$key] ?? null) {
 +        // return an existing point
         return $points[$key];         return $points[$key];
     }     }
          
-    $reflector = new \ReflectionClass(Point::class); +    // create a new point 
-    $point = $reflector->newInstanceWithoutConstructor(); +    $reflector = new \ReflectionClass(Point_Implementation::class); 
-    $point->x = $x; +    $mutablePoint = $reflector->newInstanceWithoutConstructor(); 
-    $point->y = $y; +    $mutablePoint->x = $x; 
-    $point->__construct(); +    $mutablePoint->y = $y; 
-    $reflector->finalizeRecord($point);+    $mutablePoint->__construct(); 
 +     
 +    // copy properties to an immutable Point and return it 
 +    $point = new Point($mutablePoint->x, $mutablePoint->y)
 +    $point->magnitude = $mutablePoint->magnitude;
     return $points[$key] = $point;     return $points[$key] = $point;
 } }
 </code> </code>
  
-In reality, it isn’t too much different than what actually happens in Cexcept that the C version is more efficient and frees up memory when the object is no longer referenced.+In reality, this is quite different from how it works in the enginebut this provides a mental model of how behavior should be expected to work.
  
 ==== Performance considerations ==== ==== Performance considerations ====
  
-To ensure that records are both performant and memory-efficient, the RFC proposes leveraging PHP’s copy-on-write (COW) semantics (similar to arrays) and interning values. Unlike interned strings, the garbage collector will be allowed to clean up these interned records when they’re no longer necessary.+To ensure that records are both performant and memory-efficient, the RFC proposes leveraging PHP’s copy-on-write (COW) semantics (similar to arrays) and interning values. Unlike interned strings, the garbage collector will be allowed to clean up these interned records when they’re no longer referenced.
  
 <code php> <code php>
-$point1 = Point(3, 4);+$point1 = &Point(3, 4);
 $point2 = $point1; // No data duplication, $point2 references the same data as $point1 $point2 = $point1; // No data duplication, $point2 references the same data as $point1
-$point3 = Point(3, 4); // No data duplication, it is pointing the the same memory as $point1+$point3 = Point(3, 4); // No data duplication, it is pointing to the same memory as $point1
  
 $point4 = $point1->with(x: 5); // Data duplication occurs here, creating a new instance $point4 = $point1->with(x: 5); // Data duplication occurs here, creating a new instance
 +$point5 = &Point(5, 4); // No data duplication, it is pointing to the same memory as $point4
 </code> </code>
  
Line 271: Line 399:
 Calling ''%%clone%%'' on a ''%%record%%'' results in the same record object being returned. As it is a "value" object, it represents a value and is the same thing as saying ''%%clone 3%%''—you expect to get back a ''%%3%%''. Calling ''%%clone%%'' on a ''%%record%%'' results in the same record object being returned. As it is a "value" object, it represents a value and is the same thing as saying ''%%clone 3%%''—you expect to get back a ''%%3%%''.
  
-''%%with%%'' may be called with no arguments, and it is the same behavior as ''%%clone%%''. This is an important consideration because developer may call ''%%$new = $record->with(...$array)%%'' and we don’t want to crash. If a developer wants to crash, they can do by ''%%assert($new !== $record)%%''.+If ''%%->with()%%'' is called with no arguments, a warning will be emitted, as this is most likely mistake.
  
 ==== Serialization and deserialization ==== ==== Serialization and deserialization ====
  
-Records are fully serializable and deserializable.+Records are fully serializable and deserializable, even when nested.
  
 <code php> <code php>
Line 281: Line 409:
 record Multiple(string $value1, string $value2); record Multiple(string $value1, string $value2);
  
-echo $single = serialize(Single('value')); // Outputs: "O:6:"Single":1:{s:5:"value";s:5:"value";}" +echo $single = serialize(&Single('value')); // Outputs: "O:6:"Single":1:{s:5:"value";s:5:"value";}" 
-echo $multiple = serialize(Multiple('value1', 'value2')); // Outputs: "O:8:"Multiple":1:{s:6:"values";a:2:{i:0;s:6:"value1";i:1;s:6:"value2";}}"+echo $multiple = serialize(&Multiple('value1', 'value2')); // Outputs: "O:8:"Multiple":1:{s:6:"values";a:2:{i:0;s:6:"value1";i:1;s:6:"value2";}}"
  
-echo unserialize($single) === Single('value'); // Outputs: true +echo unserialize($single) === &Single('value'); // Outputs: true 
-echo unserialize($multiple) === Multiple('value1', 'value2'); // Outputs: true+echo unserialize($multiple) === &Multiple('value1', 'value2'); // Outputs: true
 </code> </code>
 +
 +If a record contains objects or values that are unserializable, the record will not be serializable.
  
 ==== Equality ==== ==== Equality ====
Line 292: Line 422:
 A ''%%record%%'' is always strongly equal (''%%===%%'') to another record with the same value in the properties, much like an ''%%array%%'' is strongly equal to another array containing the same elements. For all intents, ''%%$recordA === $recordB%%'' is the same as ''%%$recordA == $recordB%%''. A ''%%record%%'' is always strongly equal (''%%===%%'') to another record with the same value in the properties, much like an ''%%array%%'' is strongly equal to another array containing the same elements. For all intents, ''%%$recordA === $recordB%%'' is the same as ''%%$recordA == $recordB%%''.
  
-Comparison operations will behave exactly like they do for classes, for example:+Comparison operations will behave exactly like they do for classes, which is currently undefined. 
 + 
 +=== Non-trivial values === 
 + 
 +For non-trivial values (e.g., objects, closures, resources, etc.), the ''%%===%%'' operator will return ''%%true%%'' if the two operands reference the same instances. 
 + 
 +For example, if two different DateTime records reference the exact same date and are stored in a record, the records will not be considered equal:
  
 <code php> <code php>
-record Time(float $milliseconds 0{ +$date1 = DateTime('2024-07-19'); 
-    public float $totalSeconds { +$date2 = DateTime('2024-07-19'); 
-        get =$this->milliseconds 1000+ 
-    }+record Date(DateTime $date); 
 + 
 +$dateRecord1 Date($date1); 
 +$dateRecord2 = Date($date2); 
 + 
 +echo $dateRecord1 === $dateRecord2; // Outputs: false 
 +</code> 
 + 
 +Howeverthis can be worked around by being a bit creative (see: mental model) as only the values passed in the constructor are compared: 
 + 
 +<code php> 
 +record Date(string $date) { 
 +    public DateTime $datetime;
          
-    public float $totalMinutes +    public function __construct() 
-        get => $this->totalSeconds / 60,+        $this->datetime = new DateTime($this->date);
     }     }
-    /* ... */ 
 } }
  
-$time1 Time(1000); +$date1 &Date('2024-07-19'); 
-$time2 Time(5000);+$date2 &Date('2024-07-19');
  
-echo $time1 < $time2; // Outputs: true+echo $date1->datetime === $date2->datetime ? 'true' : 'false'; // Outputs: true
 </code> </code>
 +
 +==== Type hinting ====
 +
 +A ''%%\Record%%'' interface will be added to the engine to allow type hinting for records. All records implement this interface.
 +
 +<code php>
 +function doSomething(\Record $record): void {
 +    // ...
 +}
 +</code>
 +
 +The only method on the interface is ''%%with%%'', which is a variadic method that accepts named arguments and returns ''%%self%%''.
  
 ==== Reflection ==== ==== Reflection ====
  
-Records can be interacted with via ReflectionClass, similar to readonly classes. For instance, a developer can inspect private properties but not change them as records are immutable.+A new reflection class will be added to support records: ''%%ReflectionRecord%%'' which will inherit from ''%%ReflectionClass%%'' and add a few additional methods:
  
-Developers may create new instances of records using ReflectionFunction or ReflectionClassMore on this below.+  * ''%%ReflectionRecord::finalizeRecord(object $instance): Record%%'': Finalizes a record under construction, making it immutable. 
 +  * ''%%ReflectionRecord::isRecord(mixed $object): bool%%'': Returns ''%%true%%'' if the object is a record, and ''%%false%%'' otherwise. 
 +  * ''%%ReflectionRecord::getInlineConstructor(): ReflectionFunction%%'': Returns the inline constructor of the record as ''%%ReflectionFunction%%''
 +  * ''%%ReflectionRecord::getTraditionalConstructor(): ReflectionMethod%%'': Returns the traditional constructor of the record as ''%%ReflectionMethod%%''
 +  * ''%%ReflectionRecord::makeMutable(Record $instance): object%%'': Returns a new record instance with the properties mutable. 
 +  * ''%%ReflectionRecord::isMutable(Record $instance): bool%%'': Returns ''%%true%%'' if the record is mutable, and ''%%false%%'' otherwise.
  
-=== ReflectionClass support ===+Using ''%%ReflectionRecord%%'' will allow developers to inspect records, their properties, and methods, as well as create new instances for testing or custom deserialization.
  
-It can be used to inspect records, their properties, and methods. Any attempt to modify record properties via reflection will throw an exception, maintaining immutability.+Attempting to use ''%%ReflectionClass%%'' or ''%%ReflectionFunction%%'' on a record will throw a ''%%ReflectionException%%'' exception.
  
-<code php> +=== finalizeRecord() ===
-$point Point(3, 4); +
-$reflection new \ReflectionClass($point);+
  
-foreach ($reflection->getProperties() as $property) { +The ''%%finalizeRecord()%%'' method is used to make a record immutable and look up its value in the internal cache, returning an instance that represents the finalized record.
-    echo $property->getName() . '' . $property->getValue($point) . PHP_EOL; +
-+
-</code>+
  
-=== Immutability enforcement ===+Calling ''%%finalizeRecord()%%'' on a record that has already been finalized will return the same instance. Attempting to finalize a regular object will throw a ''%%ReflectionException%%''.
  
-Attempts to modify record properties via reflection will throw a ''%%ReflectionException%%'' exception.+=== isRecord() ===
  
-<code php> +The ''%%isRecord()%%'' method is used to determine if an object is a recordIt returns ''%%true%%'' if the object is finalized record.
-try { +
-    $property = $reflection->getProperty('x'); +
-    $property->setValue($point, 10); // This will throw an exception +
-} catch (\ReflectionException $e) { +
-    echo 'Exception: ' . $e->getMessage() . PHP_EOL; // "Cannot modify a record property" +
-+
-</code>+
  
-=== ReflectionFunction for inline constructor ===+=== getInlineConstructor() ===
  
-Using ''%%ReflectionFunction%%'' on a record will reflect the inline constructor and can be used to construct new instances.+The ''%%getInlineConstructor()%%'' method is used to get the inline constructor of a record as a ''%%ReflectionFunction%%''. This can be used to inspect inlined properties and their types.
  
-<code php> +Invoking the ''%%invoke()%%'' method on the ''%%ReflectionFunction%%'' will create a finalized record.
-$constructor = new \ReflectionFunction('Geometry\Point')+
-echo 'Constructor Parameters: '+
-foreach ($constructor->getParameters() as $param) { +
-    echo $param->getName() . ' '+
-+
-</code>+
  
-=== Bypassing constructors ===+=== getTraditionalConstructor() ===
  
-During custom deserialization, a developer may need to bypass constructors to fill in properties. Since records are mutable until the constructor is called, this can be done by setting the properties before calling ''%%finalizeRecord()%%''.+The ''%%getTraditionalConstructor()%%'' method is used to get the traditional constructor of a record as a ''%%ReflectionMethod%%''. This can be useful to inspect the constructor for further initialization.
  
-Note that until ''%%finalizeRecord()%%'' is called, any guarantees of immutability and value semantics are **not enforced**.+Invoking the ''%%invoke()%%'' method on the ''%%ReflectionMethod%%'' on a finalized record will throw an exception.
  
-<code php> +=== makeMutable() ===
-record Point(int $x, int $y);+
  
-$example = Point(1, 2);+The ''%%makeMutable()%%'' method is used to create a new instance of a record with mutable properties. The returned instance doesn’t provide any value semantics and should only be used for testing purposes or when there is no other option.
  
-$pointReflector = new \ReflectionClass(Point::class)+A mutable record can be finalized again using ''%%finalizeRecord()%%''. A mutable record will not be considered record by ''%%isRecord()%%'' or implement the ''%%\Record%%'' interface. It is a regular object with the same properties and methods as the recordFor example, ''%%var_dump()%%'' will output ''%%object%%'' instead of ''%%record%%''.
-// create new point while keeping the properties mutable. +
-$point = $pointReflector->newInstanceWithoutConstructor(); +
-$point->x = 1; +
-$point->y = 2; +
-assert($example !== $point); // true +
-$pointReflector->finalizeRecord($point)+
-assert($example === $point); // true +
-</code>+
  
-Alternatively, a developer can use ''%%ReflectionFunction%%'' to get access to the inline constructor and call it directly:+=== isMutable() ===
  
-<code php> +The ''%%isMutable()%%'' method is used to determine if a record has been made mutable via ''%%makeMutable()%%'' or otherwise not yet finalized.
-record Point(int $x, int $y);+
  
-$example = Point(1, 2);+=== Custom deserialization example ===
  
-$pointReflector = new \ReflectionFunction(Point::class); +In cases where custom deserialization is requireda developer can use ''%%ReflectionRecord%%'' to manually construct a new instance of a record.
-$point = $pointReflector->invoke(12);+
  
-assert($example === $point); // true +<code php> 
-</code>+record Seconds(int $seconds);
  
-=== New and/or modified functions and methods ===+$example &Seconds(5);
  
-  * Calling ''%%is_object($record)%%'' will return ''%%true%%''. +$reflector = new ReflectionRecord(Seconds::class); 
-  * A new function, ''%%is_record($record)%%'', will return ''%%true%%'' for records, and ''%%false%%'' otherwise. +$expiration = $reflector->newInstanceWithoutConstructor(); // this is a mutable object 
-  * Calling ''%%get_class($record)%%'' will return the record name as a string. +$expiration->seconds = 5; 
-  * A new method, ''%%ReflectionClass::finalizeRecord($instance)%%'', will be added to finalize a record, making it immutable.+assert($example !== $expiration); // true 
 +$expiration = $reflector->finalizeRecord($expiration); 
 +assert($example === $expiration); // true 
 +</code>
  
 ==== var_dump ==== ==== var_dump ====
  
-When passed an instance of a record the ''%%var_dump()%%'' function will generate output the same as if an equivalent object was passed — e.g., both having the same properties — except the output generated will replace the prefix text "object" with the text "record."+When passed an instance of a record the ''%%var_dump()%%'' function will output the same as if an equivalent object were passed — e.g., both having the same properties — except the output generated will replace the prefix text "object" with the text "record."
  
 <code txt> <code txt>
Line 412: Line 550:
 ==== Considerations for implementations ==== ==== Considerations for implementations ====
  
-A ''%%record%%'' cannot be named after an existing ''%%record%%'', ''%%class%%'' or ''%%function%%''. This is because defining a ''%%record%%'' creates both a ''%%class%%'' and ''%%function%%'' with the same name.+A ''%%record%%'' cannot share its name with an existing ''%%record%%'', ''%%class%%''''%%interface%%''''%%trait%%'', or ''%%function%%'', just like class.
  
 ==== Autoloading ==== ==== Autoloading ====
  
-This RFC chooses to omit autoloading from the specification for a record. The reason is that instantiating a record calls the function implicitly declared when the record is explicitly declared, PHP doesn’t currently support autoloading functions, and solving function autoloading is out-of-scope for this RFC.+Records will be autoloaded in the same way as classes.
  
-Once function autoloading is implemented in PHP at some hopeful point in the future, said autoloader could locate the record and then autoload it.+==== New Functions ====
  
-The author of this RFC strongly encourages someone to put forward a function autoloading RFC if autoloading is desired for records.+  * ''%%record_exists%%'' will return ''%%true%%'' if a record exists and ''%%false%%'' otherwise. It has the same signature as ''%%class_exists%%''.
  
 ===== Backward Incompatible Changes ===== ===== Backward Incompatible Changes =====
  
 To avoid conflicts with existing code, the ''%%record%%'' keyword will be handled similarly to ''%%enum%%'' to prevent backward compatibility issues. To avoid conflicts with existing code, the ''%%record%%'' keyword will be handled similarly to ''%%enum%%'' to prevent backward compatibility issues.
 +
 +Since ''%%&%%'' is currently a syntax error when prefixed on a function call, it will be used to denote a record instantiation.
  
 ===== Proposed PHP Version(s) ===== ===== Proposed PHP Version(s) =====
Line 454: Line 594:
 ===== Open Issues ===== ===== Open Issues =====
  
-To-do+  * Distill how CoW works, exactly. 
 +  * Address conflict with ''%%&%%'' syntax: https://3v4l.org/CE5rt
  
 ===== Unaffected PHP Functionality ===== ===== Unaffected PHP Functionality =====
Line 461: Line 602:
  
 ===== Future Scope ===== ===== Future Scope =====
 +
 +  * Records for "record-like" types, such as DateTime, DateInterval, and others.
 +  * Short definition syntax for classes
  
 ===== Proposed Voting Choices ===== ===== Proposed Voting Choices =====
  
-Include these so readers know where you’re heading and can discuss the proposed voting options.+2/3 majority.
  
 ===== Patches and Tests ===== ===== Patches and Tests =====
Line 472: Line 616:
 ===== Implementation ===== ===== Implementation =====
  
-After the project is implemented, this section should contain +To be completed during later phase of discussion.
- +
-  - the version(s) it was merged into +
-  - 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 ===== ===== References =====
  
-Links to external references, discussions or RFCs+  * [[https://en.wikipedia.org/wiki/Value_semantics|Value semantics]]
  
 ===== Rejected Features ===== ===== Rejected Features =====
  
-Keep this updated with features that were discussed on the mail lists.+TBD
  
rfc/records.1722589620.txt.gz · Last modified: 2024/08/02 09:07 by withinboredom