rfc:records
Differences
This shows you the differences between two versions of the page.
Both sides previous revisionPrevious revisionNext revision | Previous revision | ||
rfc:records [2024/08/02 09:07] – update examples and add more details withinboredom | rfc: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, < | + | * Author: Robert Landers, < |
- | * Status: | + | * Status: Under Discussion |
* First Published at: http:// | * First Published at: http:// | ||
Line 13: | Line 13: | ||
==== Value objects ==== | ==== Value objects ==== | ||
- | Value objects are immutable objects that represent a value. They’re used for storing | + | Value objects are immutable objects that represent a value. They’re used to store values with a different |
- | Consider this example where a function accepts an integer as a user ID, and the id is accidentally set to a non-sensical | + | Consider this example where a function accepts an integer as a user ID, and the ID is accidentally set to a nonsensical |
<code php> | <code php> | ||
Line 25: | Line 25: | ||
$uid = $user-> | $uid = $user-> | ||
// ... | // ... | ||
- | $uid = 5; // somehow | + | $uid = 5; // accidentally sets uid to an unrelated integer |
// ... | // ... | ||
- | updateUserRole($uid, | + | updateUserRole($uid, |
</ | </ | ||
- | Currently, the only solution to this is to use a class, but this requires | + | Currently, the only solution to this is to use a **class**, but this requires |
=== The solution === | === The solution === | ||
- | Like arrays, strings, and other values, '' | + | Like arrays, strings, and other values, |
- | Let’s take a look at the updated example, using a '' | + | Let’s take a look at an updated example using a '' |
<code php> | <code php> | ||
Line 50: | Line 50: | ||
$uid = 5; | $uid = 5; | ||
// ... | // ... | ||
- | updateUserRole($uid, | + | updateUserRole($uid, |
</ | </ | ||
Line 57: | Line 57: | ||
===== Proposal ===== | ===== Proposal ===== | ||
- | This RFC proposes the introduction of a new record keyword in PHP to define immutable | + | This RFC proposes the introduction of a '' |
==== Syntax and semantics ==== | ==== Syntax and semantics ==== | ||
Line 63: | Line 63: | ||
=== Definition === | === Definition === | ||
- | A **record** is defined by the keyword '' | + | A **record** is defined by the keyword '' |
A **record** may optionally implement an interface using the '' | A **record** may optionally implement an interface using the '' | ||
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 time, the 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 '' | + | A **record** body may also contain static methods and properties, which behave identically to static methods and properties |
+ | |||
+ | As an example, the following code defines a **record** named '' | ||
<code php> | <code php> | ||
Line 126: | Line 128: | ||
=== Usage === | === Usage === | ||
- | A record may be used as a readonly | + | A record may be used much like a class, as the behavior of the two is very similar, |
+ | |||
+ | <code php> | ||
+ | $gray = $bucket-> | ||
+ | </ | ||
+ | |||
+ | Records are instantiated in a function format, with '' | ||
+ | |||
+ | <code php> | ||
+ | $black = & | ||
+ | $white = & | ||
+ | $blackPaint = & | ||
+ | $whitePaint = & | ||
+ | $bucket = & | ||
+ | |||
+ | $gray = $bucket-> | ||
+ | $grey = $bucket-> | ||
+ | |||
+ | assert($gray === $grey); // true | ||
+ | </ | ||
=== Optional parameters and default values === | === Optional parameters and default values === | ||
Line 132: | Line 153: | ||
A '' | A '' | ||
- | 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 | + | 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/ |
<code php> | <code php> | ||
record Rectangle(int $x, int $y = 10); | record Rectangle(int $x, int $y = 10); | ||
- | var_dump(Rectangle(10)); | + | var_dump(&Rectangle(10)); |
</ | </ | ||
=== Auto-generated with method === | === Auto-generated with method === | ||
- | To enhance the usability of records, the RFC proposes | + | To make records |
- | The auto-generated '' | + | == How the with method |
- | Example showing how this works with properties: | + | **Named arguments** |
+ | |||
+ | The '' | ||
+ | |||
+ | **Variadic arguments** | ||
+ | |||
+ | Variadic arguments from the inline constructor don’t require named arguments in the '' | ||
+ | |||
+ | 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-> | $this-> | ||
Line 156: | Line 185: | ||
} | } | ||
- | $userId = UserId(1); | + | $userId = &UserId(1); |
- | $otherId = $userId-> | + | $otherId = $userId-> |
- | $otherId = $userId-> | + | $otherId = $userId-> |
- | $otherId = $userId-> | + | $otherId = $userId-> |
</ | </ | ||
- | 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, | record Vector(int $dimensions, | ||
- | $vector = Vector(3, 1, 2, 3); | + | $vector = &Vector(3, 1, 2, 3); |
- | $vector = $vector-> | + | $vector = $vector-> |
- | $vector = $vector-> | + | $vector = $vector-> |
- | $vector = $vector-> | + | $vector = $vector-> |
</ | </ | ||
- | |||
- | 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 '' | + | A developer may define their own '' |
<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:: | return parent:: | ||
} | } | ||
Line 190: | Line 217: | ||
$pluto = $pluto-> | $pluto = $pluto-> | ||
// and then we changed the name | // and then we changed the name | ||
- | $mickey = $pluto-> | + | $mickey = $pluto-> |
</ | </ | ||
=== Constructors === | === Constructors === | ||
- | A **record** has two concepts | + | 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 | + | 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, |
<code php> | <code php> | ||
- | // Inline constructor | + | // Inline constructor |
- | 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-> |
throw new InvalidArgumentException(" | throw new InvalidArgumentException(" | ||
} | } | ||
| | ||
- | $this-> | + | $this-> |
- | $this-> | + | $this-> |
+ | // all properties are now immutable | ||
} | } | ||
} | } | ||
</ | </ | ||
- | When a traditional constructor exists and is called, the properties are already initialized to the value of the inline constructor and are mutable until the end of the method, at 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 " | ||
+ | } | ||
+ | } | ||
+ | |||
+ | record SpaceCar(string $model) implements Car, SpaceShip { | ||
+ | public function drive(): void { | ||
+ | echo " | ||
+ | } | ||
+ | |||
+ | public function launch(): void { | ||
+ | echo " | ||
+ | } | ||
+ | } | ||
+ | |||
+ | record Submarine(string $model) implements Vehicle { | ||
+ | use Submersible; | ||
+ | } | ||
+ | |||
+ | record TowTruct(string $model, private Car $towing) implements Car { | ||
+ | use Towable; | ||
+ | } | ||
+ | </ | ||
==== Mental models and how it works ==== | ==== Mental models and how it works ==== | ||
- | From the perspective of a developer, declaring a record declares an object | + | 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 |
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-> | ||
+ | } | ||
+ | |||
+ | public function add(Point $point): Point { | ||
+ | return & | ||
+ | } | ||
+ | |||
+ | public function dot(Point $point): int { | ||
+ | return $this->x * $point-> | ||
+ | } | ||
+ | } | ||
// 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-> | ||
+ | } | ||
+ | |||
+ | public function with(...$parameters) { | ||
+ | // validity checks omitted for brevity | ||
+ | $parameters = array_merge([$this-> | ||
+ | return Point(...$parameters); | ||
+ | } | ||
| | ||
- | public function __construct() {} | + | |
+ | return Point($this-> | ||
+ | } | ||
+ | |||
+ | public function dot(Point $point): int { | ||
+ | return $this->x * $point-> | ||
+ | } | ||
+ | } | ||
+ | |||
+ | // used to enforce immutability but has nearly the same implementation | ||
+ | readonly class Point { | ||
+ | public float $magnitude; | ||
+ | |||
+ | | ||
+ | |||
+ | public function with(...$parameters): | ||
+ | // validity checks omitted for brevity | ||
+ | $parameters = array_merge([$this-> | ||
+ | return Point(...$parameters); | ||
+ | } | ||
+ | |||
+ | public function add(Point $point): Point { | ||
+ | return Point($this-> | ||
+ | } | ||
+ | |||
+ | public function dot(Point $point): int { | ||
+ | return $this->x * $point-> | ||
+ | | ||
} | } | ||
function Point(int $x, int $y): Point { | function Point(int $x, int $y): Point { | ||
static $points = []; | static $points = []; | ||
- | $key = "$x,$y"; | + | |
+ | | ||
if ($points[$key] ?? null) { | if ($points[$key] ?? null) { | ||
+ | // return an existing point | ||
return $points[$key]; | return $points[$key]; | ||
} | } | ||
| | ||
- | $reflector = new \ReflectionClass(Point::class); | + | |
- | $point = $reflector-> | + | |
- | $point->x = $x; | + | $mutablePoint |
- | $point->y = $y; | + | $mutablePoint->x = $x; |
- | $point-> | + | $mutablePoint->y = $y; |
- | $reflector->finalizeRecord($point); | + | $mutablePoint-> |
+ | |||
+ | // copy properties to an immutable Point and return it | ||
+ | $point = new Point($mutablePoint->x, $mutablePoint-> | ||
+ | $point-> | ||
return $points[$key] = $point; | return $points[$key] = $point; | ||
} | } | ||
</ | </ | ||
- | In reality, it isn’t too much different than what actually happens | + | In reality, |
==== Performance considerations ==== | ==== Performance considerations ==== | ||
- | To ensure that records are both performant and memory-efficient, | + | To ensure that records are both performant and memory-efficient, |
<code php> | <code php> | ||
- | $point1 = Point(3, 4); | + | $point1 = &Point(3, 4); |
$point2 = $point1; // No data duplication, | $point2 = $point1; // No data duplication, | ||
- | $point3 = Point(3, 4); // No data duplication, | + | $point3 = Point(3, 4); // No data duplication, |
$point4 = $point1-> | $point4 = $point1-> | ||
+ | $point5 = & | ||
</ | </ | ||
Line 271: | Line 399: | ||
Calling '' | Calling '' | ||
- | '' | + | If '' |
==== 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(' | + | echo $single = serialize(&Single(' |
- | echo $multiple = serialize(Multiple(' | + | echo $multiple = serialize(&Multiple(' |
- | echo unserialize($single) === Single(' | + | echo unserialize($single) === &Single(' |
- | echo unserialize($multiple) === Multiple(' | + | echo unserialize($multiple) === &Multiple(' |
</ | </ | ||
+ | |||
+ | If a record contains objects or values that are unserializable, | ||
==== Equality ==== | ==== Equality ==== | ||
Line 292: | Line 422: | ||
A '' | A '' | ||
- | Comparison operations will behave exactly like they do for classes, | + | Comparison operations will behave exactly like they do for classes, |
+ | |||
+ | === Non-trivial values === | ||
+ | |||
+ | For non-trivial values (e.g., objects, closures, resources, etc.), the '' | ||
+ | |||
+ | 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 | + | $date1 = DateTime(' |
- | | + | $date2 = DateTime(' |
- | | + | |
- | | + | record |
+ | |||
+ | $dateRecord1 | ||
+ | $dateRecord2 = Date($date2); | ||
+ | |||
+ | echo $dateRecord1 === $dateRecord2; | ||
+ | </ | ||
+ | |||
+ | However, this 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 | + | public |
- | | + | |
} | } | ||
- | /* ... */ | ||
} | } | ||
- | $time1 = Time(1000); | + | $date1 = &Date(' |
- | $time2 = Time(5000); | + | $date2 = &Date(' |
- | echo $time1 < $time2; // Outputs: true | + | echo $date1-> |
</ | </ | ||
+ | |||
+ | ==== Type hinting ==== | ||
+ | |||
+ | A '' | ||
+ | |||
+ | <code php> | ||
+ | function doSomething(\Record $record): void { | ||
+ | // ... | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | The only method on the interface is '' | ||
==== Reflection ==== | ==== Reflection ==== | ||
- | Records can be interacted with via ReflectionClass, | + | A new reflection class will be added to support |
- | Developers may create new instances | + | * '' |
+ | * '' | ||
+ | * '' | ||
+ | * '' | ||
+ | * '' | ||
+ | * '' | ||
- | === ReflectionClass support === | + | Using '' |
- | It can be used to inspect records, their properties, and methods. Any attempt to modify | + | Attempting |
- | <code php> | + | === finalizeRecord() === |
- | $point | + | |
- | $reflection | + | |
- | foreach ($reflection-> | + | The '' |
- | echo $property-> | + | |
- | } | + | |
- | </ | + | |
- | === Immutability enforcement === | + | Calling '' |
- | Attempts to modify record properties via reflection will throw a '' | + | === isRecord() === |
- | <code php> | + | The '' |
- | try { | + | |
- | $property = $reflection-> | + | |
- | $property-> | + | |
- | } catch (\ReflectionException $e) { | + | |
- | echo 'Exception: | + | |
- | } | + | |
- | </ | + | |
- | === ReflectionFunction for inline constructor | + | === getInlineConstructor() |
- | Using '' | + | The '' |
- | <code php> | + | Invoking the '' |
- | $constructor = new \ReflectionFunction('Geometry\Point'); | + | |
- | echo 'Constructor Parameters: | + | |
- | foreach ($constructor-> | + | |
- | echo $param-> | + | |
- | } | + | |
- | </ | + | |
- | === Bypassing constructors | + | === getTraditionalConstructor() |
- | During custom deserialization, | + | The '' |
- | Note that until '' | + | Invoking the '' |
- | <code php> | + | === makeMutable() === |
- | record Point(int $x, int $y); | + | |
- | $example = Point(1, 2); | + | The '' |
- | $pointReflector = new \ReflectionClass(Point:: | + | A mutable record can be finalized again using '' |
- | // create | + | |
- | $point = $pointReflector-> | + | |
- | $point-> | + | |
- | $point-> | + | |
- | assert($example | + | |
- | $pointReflector-> | + | |
- | assert($example === $point); // true | + | |
- | </ | + | |
- | Alternatively, | + | === isMutable() === |
- | <code php> | + | The '' |
- | record | + | |
- | $example = Point(1, 2); | + | === Custom deserialization |
- | $pointReflector = new \ReflectionFunction(Point:: | + | In cases where custom deserialization is required, a developer can use '' |
- | $point = $pointReflector-> | + | |
- | assert($example === $point); // true | + | <code php> |
- | </ | + | record Seconds(int $seconds); |
- | === New and/or modified functions and methods === | + | $example |
- | * Calling '' | + | $reflector = new ReflectionRecord(Seconds:: |
- | * A new function, '' | + | $expiration = $reflector-> |
- | * Calling '' | + | $expiration-> |
- | * A new method, '' | + | assert($example !== $expiration); // true |
+ | $expiration = $reflector-> | ||
+ | assert($example === $expiration); // true | ||
+ | </ | ||
==== var_dump ==== | ==== var_dump ==== | ||
- | When passed an instance of a record the '' | + | When passed an instance of a record the '' |
<code txt> | <code txt> | ||
Line 412: | Line 550: | ||
==== Considerations for implementations ==== | ==== Considerations for implementations ==== | ||
- | A '' | + | A '' |
==== 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. | + | * '' |
===== Backward Incompatible Changes ===== | ===== Backward Incompatible Changes ===== | ||
To avoid conflicts with existing code, the '' | To avoid conflicts with existing code, the '' | ||
+ | |||
+ | Since '' | ||
===== 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 '' | ||
===== Unaffected PHP Functionality ===== | ===== Unaffected PHP Functionality ===== | ||
Line 461: | Line 602: | ||
===== Future Scope ===== | ===== Future Scope ===== | ||
+ | |||
+ | * Records for " | ||
+ | * 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, | + | To be completed during |
- | + | ||
- | - the version(s) it was merged into | + | |
- | - a 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:// |
===== 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