-
-
Notifications
You must be signed in to change notification settings - Fork 0
[RFC]: Records #1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 4 commits
5e7319c
3be33e9
bd4cf96
c69bff3
49be882
090855f
c91e18c
7b1e32b
be7ae31
e9a1cc1
97057b4
fc83031
62c8edd
a490ba0
eb4f020
9089f44
6b32127
f7c3a05
a2718a6
714a445
12d702d
57109f5
1b91037
91d287e
ce74737
30d19d9
c1d0a8a
a320634
908d482
5134c01
72f3856
1b50121
1020c04
5b3a6c5
df2e506
eb437b9
a6ff51d
16e6967
c6cbaf0
6cd68e6
fdb8dd6
f14b093
c7155c3
02ed26f
be0024a
c2e5eef
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,360 @@ | ||
| # PHP RFC: Records | ||
|
|
||
| - Version: 0.9 | ||
| - Date: 2024-07-19 | ||
| - Author: Robert Landers, landers.robert@gmail.com | ||
| - Status: Draft (or Under Discussion or Accepted or Declined) | ||
| - First Published at: <http://wiki.php.net/rfc/records> | ||
|
|
||
| ## Introduction | ||
|
|
||
| This RFC proposed the introduction of `record` objects, which are immutable classes | ||
|
withinboredom marked this conversation as resolved.
Outdated
|
||
| with [value semantics](https://en.wikipedia.org/wiki/Value_semantics). | ||
|
|
||
| ### Value objects | ||
|
|
||
| Value objects are immutable objects that represent a value. They are used to store values with a different meaning than | ||
|
withinboredom marked this conversation as resolved.
Outdated
|
||
| their technical 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. | ||
|
|
||
| Consider this example: | ||
|
withinboredom marked this conversation as resolved.
Outdated
|
||
|
|
||
| ```php | ||
| function updateUserRole(int $userId, Role $role): void { | ||
| // ... | ||
| } | ||
|
|
||
| $user = getUser(/*...*/) | ||
| $uid = $user->id; | ||
| // ... | ||
| $uid = 5; // somehow accidentally sets uid to an unrelated integer | ||
| // ... | ||
| updateUserRole($uid, Role::ADMIN()); // accidental passing of | ||
|
withinboredom marked this conversation as resolved.
Outdated
|
||
| ``` | ||
|
|
||
| In this example, the uid is accidentally set to a plain integer, and updateUserRole is called with the wrong value. | ||
|
withinboredom marked this conversation as resolved.
Outdated
|
||
|
|
||
| Currently, the only solution to this is to use a class, but this requires a lot of boilerplate code. | ||
|
|
||
| #### The solution | ||
|
|
||
| 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, using the previous example: | ||
|
withinboredom marked this conversation as resolved.
Outdated
|
||
|
|
||
| ```php | ||
| record UserId(int $id); | ||
|
|
||
| function updateUserRole(UserId $userId, Role $role): void { | ||
| // ... | ||
| } | ||
|
|
||
| $user = getUser(/*...*/) | ||
| $uid = $user->id; // $uid is a UserId object | ||
| // ... | ||
| $uid = 5; | ||
| // ... | ||
| updateUserRole($uid, Role::ADMIN()); // This will throw an error | ||
| ``` | ||
|
|
||
| ## Proposal | ||
|
|
||
| This RFC proposes the introduction of a new record keyword in PHP to define immutable data objects. These objects will | ||
|
withinboredom marked this conversation as resolved.
Outdated
|
||
| 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 cannot extend other records or classes; | ||
| composition is allowed, however. | ||
|
|
||
| #### Syntax and semantics | ||
|
|
||
| ##### Definition | ||
|
|
||
| A `record` is defined by the word "record", followed by the name of its type, an open parenthesis containing one or more | ||
| typed parameters that become public, immutable, properties. | ||
|
withinboredom marked this conversation as resolved.
Outdated
|
||
| They may optionally implement an interface using the `implements` keyword. | ||
| A `record` body is optional. | ||
|
withinboredom marked this conversation as resolved.
Outdated
|
||
|
|
||
| A `record` may contain a constructor with zero arguments to perform further initialization, if required. | ||
|
withinboredom marked this conversation as resolved.
Outdated
|
||
| If it does not have a constructor, an implicit, empty contstructor is provided. | ||
|
|
||
| A `record` body may contain property hooks, methods, and use traits (so long as they do not conflict with `record` | ||
| rules). | ||
| Regular properties may also be defined, but they are immutable by default and are no different from `const`. | ||
|
withinboredom marked this conversation as resolved.
Outdated
|
||
|
|
||
| Static properties and methods are forbidden in a `record` (this includes | ||
| `const`, a regular property may be used instead). Attempting to define | ||
| static properties, methods, constants results in a compilation error. | ||
|
withinboredom marked this conversation as resolved.
Outdated
|
||
|
|
||
| ``` php | ||
|
withinboredom marked this conversation as resolved.
|
||
| namespace Paint; | ||
|
|
||
| record Pigment(int $red, int $yellow, int $blue) { | ||
| public function mix(Pigment $other, float $amount): Pigment { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you use a more semantically meaningful name than
withinboredom marked this conversation as resolved.
|
||
| return $this->with( | ||
| red: $this->red * (1 - $amount) + $other->red * $amount, | ||
| yellow: $this->yellow * (1 - $amount) + $other->yellow * $amount, | ||
| blue: $this->blue * (1 - $amount) + $other->blue * $amount | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| record StockPaint(Pigment $color, float $volume); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Bikeshedding, but I don't get what
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Like when you go to the store and buy some paint. You choose a color and then they mix it for you. The colors they put into the mixer would be the "stock".
withinboredom marked this conversation as resolved.
|
||
|
|
||
| record PaintBucket(StockPaint ...$constituents) { | ||
| public function mixIn(StockPaint $paint): PaintBucket { | ||
| return $this->with(...$this->constituents, $paint); | ||
| } | ||
|
|
||
| public function color(): Pigment { | ||
| return array_reduce($this->constituents, fn($color, $paint) => $color->mix($paint->color, $paint->volume), Pigment(0, 0, 0)); | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| ##### Usage | ||
|
|
||
| A `record` may be used as a `readonly class`, | ||
|
withinboredom marked this conversation as resolved.
Outdated
withinboredom marked this conversation as resolved.
Outdated
|
||
| as the behavior of it is very similar with no key differences to assist in migration from `readonly class`. | ||
|
withinboredom marked this conversation as resolved.
Outdated
|
||
|
|
||
| ##### Optional parameters and default values | ||
|
|
||
| A `record` can also be defined with optional parameters that are set if left out during instantiation. | ||
|
withinboredom marked this conversation as resolved.
Outdated
|
||
|
|
||
| ``` php | ||
| record Rectangle(int $x, int $y = 10); | ||
| var_dump(Rectangle(10)); // output a record with x: 10 and y: 10 | ||
| ``` | ||
|
|
||
| ##### 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. | ||
|
|
||
| The auto-generated `with` method accepts only named arguments defined in the constructor. | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can a $point2 = $point1->with([ "x" => 5 ]);or: $point2 = $point1->with(...[ "x" => 5 ]);If not, why not? The issue is that PHP has not a type I would call a "parameter set" or more succinctly, "args" so the only way to pass them around from function to function is via arrays. I could also envision records defined to be args for other records, functions and/or classes e.g.: record PointArgs(int $x, int $y)
record Point(int $x, int $y) {
// Point stuff here
}
function DoSomething(Point $p, PointArgs $pa) {
$p2 = $p->with($pa)
}
$pa = PointArgs(5,10);
$p = Point($pa);
DoSomething($p, PointArgs(3,9));Along those lines, is certainly would be nice if I could use a shorthand like this: DoSomething($p, {3,9});And/or: DoSomething($p, {x:3,y:9});Something to ponder?
withinboredom marked this conversation as resolved.
Outdated
|
||
| No other property names can be used, and it returns a new record object with the given values. | ||
|
withinboredom marked this conversation as resolved.
Outdated
|
||
|
|
||
| ``` php | ||
| $point1 = Point(3, 4); | ||
| $point2 = $point1->with(x: 5); | ||
| $point3 = $point1->with(null, 10); // must use named arguments | ||
|
withinboredom marked this conversation as resolved.
Outdated
|
||
|
|
||
| echo $point1->x; // Outputs: 3 | ||
| echo $point2->x; // Outputs: 5 | ||
| ``` | ||
|
|
||
| A developer may define their own `with` method if they so choose, | ||
|
withinboredom marked this conversation as resolved.
Outdated
|
||
| and reference the generated `with` method using `parent::with()`. | ||
|
withinboredom marked this conversation as resolved.
|
||
| This allows a developer to define policies or constraints on how data is updated. | ||
|
|
||
| ``` php | ||
| record Planet(string $name, int $population) { | ||
| public function with(int $population) { | ||
| return parent::with(population: $population); | ||
| } | ||
|
withinboredom marked this conversation as resolved.
Outdated
|
||
| } | ||
| $pluto = Planet("Pluto", 0); | ||
| // we made it! | ||
| $pluto = $pluto->with(population: 1); | ||
| // and then we changed the name | ||
| $mickey = $pluto->with(name: "Mickey"); // no named argument for population error | ||
|
withinboredom marked this conversation as resolved.
Outdated
|
||
| ``` | ||
|
|
||
| ##### Constructors | ||
|
|
||
| Optionally, they may also define a constructor to provide validation or other initialization logic: | ||
|
withinboredom marked this conversation as resolved.
Outdated
|
||
|
|
||
| ```php | ||
| record User(string $name, string $email) { | ||
| public string $id; | ||
|
|
||
| public function __construct() { | ||
| if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { | ||
| throw new InvalidArgumentException("Invalid email address"); | ||
| } | ||
|
|
||
| $this->id = hash('sha256', $email); | ||
| $this->name = ucwords($name); | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| During construction, a `record` is fully mutable. | ||
| This allows the developer freedom to mutate properties as needed to ensure a canonical representation of an object. | ||
|
|
||
| #### 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 are no | ||
| longer needed. | ||
|
|
||
| ``` php | ||
| $point1 = Point(3, 4); | ||
| $point2 = $point1; // No data duplication, $point2 references the same data as $point1 | ||
| $point3 = Point(3, 4); // No data duplication here either, it is pointing the the same memory as $point1 | ||
|
|
||
| $point4 = $point1->with(x: 5); // Data duplication occurs here, creating a new instance with modified data | ||
|
withinboredom marked this conversation as resolved.
Outdated
|
||
| ``` | ||
|
|
||
| ##### Cloning and with() | ||
|
|
||
| Calling `clone` on a `record` results in the exact same record object being returned. As it is a "value" object, it | ||
|
withinboredom marked this conversation as resolved.
Outdated
|
||
| 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 a 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)`. | ||
|
|
||
| #### Equality | ||
|
|
||
| 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. | ||
|
|
||
| ### Reflection | ||
|
|
||
| Records in PHP will be fully supported by the reflection API, | ||
| providing access to their properties and methods just like regular classes. | ||
| However, immutability and special instantiation rules will be enforced. | ||
|
withinboredom marked this conversation as resolved.
Outdated
|
||
|
|
||
| #### ReflectionClass support | ||
|
|
||
| `ReflectionClass` can be used to inspect records, their properties, and methods. Any attempt to modify record properties | ||
|
withinboredom marked this conversation as resolved.
Outdated
|
||
| via reflection will throw an exception, maintaining immutability. Attempting to create a new instance via | ||
| `ReflectionClass` will cause a `ReflectionException` to be thrown. | ||
|
|
||
| ``` php | ||
| $point = Point(3, 4); | ||
| $reflection = new \ReflectionClass($point); | ||
|
|
||
| foreach ($reflection->getProperties() as $property) { | ||
| echo $property->getName() . ': ' . $property->getValue($point) . PHP_EOL; | ||
| } | ||
| ``` | ||
|
|
||
| #### Immutability enforcement | ||
|
|
||
| Attempts to modify record properties via reflection will throw an exception. | ||
|
withinboredom marked this conversation as resolved.
Outdated
|
||
|
|
||
| ``` php | ||
| 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" | ||
| } | ||
| ``` | ||
|
|
||
| #### ReflectionFunction for implicit constructor | ||
|
withinboredom marked this conversation as resolved.
Outdated
|
||
|
|
||
| Using `ReflectionFunction` on a record will reflect the implicit constructor. | ||
|
|
||
| ``` php | ||
| $constructor = new \ReflectionFunction('Geometry\Point'); | ||
| echo 'Constructor Parameters: '; | ||
| foreach ($constructor->getParameters() as $param) { | ||
| echo $param->getName() . ' '; | ||
| } | ||
| ``` | ||
|
|
||
| #### New functions and methods | ||
|
withinboredom marked this conversation as resolved.
Outdated
|
||
|
|
||
| - Calling `is_object($record)` will return `true`. | ||
| - A new function, `is_record($record)`, will return `true` for records, and `false` otherwise | ||
| - Calling `get_class($record)` will return the record name | ||
|
withinboredom marked this conversation as resolved.
Outdated
|
||
|
|
||
| #### var_dump | ||
|
|
||
| Calling `var_dump` will look much like it does for objects, but instead of `object` it will say `record`. | ||
|
withinboredom marked this conversation as resolved.
Outdated
|
||
|
|
||
| record(Point)#1 (2) { | ||
| ["x"]=> | ||
| int(1) | ||
| ["y"]=> | ||
| int(2) | ||
| } | ||
|
|
||
| ### 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 a `function` with the same name. | ||
|
|
||
|
withinboredom marked this conversation as resolved.
Outdated
withinboredom marked this conversation as resolved.
Outdated
|
||
| ### Auto loading | ||
|
withinboredom marked this conversation as resolved.
Outdated
|
||
|
|
||
| As invoking a record value by its name looks remarkably similar to calling a function, | ||
| and PHP has no function autoloader, auto loading will not be supported in this implementation. | ||
| If function auto loading were to be implemented in the future, an autoloader could locate the `record` and autoload it. | ||
| The author of this RFC strongly encourages someone to put forward a function auto loading RFC if auto loading is desired for records. | ||
|
|
||
| ## Backward Incompatible Changes | ||
|
|
||
| No backward incompatible changes. | ||
|
withinboredom marked this conversation as resolved.
Outdated
|
||
|
|
||
| ## Proposed PHP Version(s) | ||
|
|
||
| PHP 8.5 | ||
|
|
||
| ## RFC Impact | ||
|
|
||
| ### To SAPIs | ||
|
|
||
| N/A | ||
|
|
||
| ### To Existing Extensions | ||
|
|
||
| N/A | ||
|
|
||
| ### To Opcache | ||
|
|
||
| Unknown. | ||
|
|
||
| ### New Constants | ||
|
|
||
| None | ||
|
|
||
| ### php.ini Defaults | ||
|
|
||
| None | ||
|
|
||
| ## Open Issues | ||
|
|
||
|
withinboredom marked this conversation as resolved.
|
||
| Todo | ||
|
|
||
| ## Unaffected PHP Functionality | ||
|
|
||
| None. | ||
|
|
||
| ## Future Scope | ||
|
withinboredom marked this conversation as resolved.
|
||
|
|
||
| ## Proposed Voting Choices | ||
|
|
||
| Include these so readers know where you are heading and can discuss the | ||
| proposed voting options. | ||
|
|
||
| ## Patches and Tests | ||
|
|
||
| TBD | ||
|
withinboredom marked this conversation as resolved.
|
||
|
|
||
| ## Implementation | ||
|
|
||
| After the project is implemented, this section should contain | ||
|
|
||
| 1. the version(s) it was merged into | ||
| 2. a link to the git commit(s) | ||
| 3. a link to the PHP manual entry for the feature | ||
| 4. a link to the language specification section (if any) | ||
|
|
||
|
withinboredom marked this conversation as resolved.
Outdated
|
||
| ## References | ||
|
|
||
| Links to external references, discussions or RFCs | ||
|
withinboredom marked this conversation as resolved.
Outdated
|
||
|
|
||
| ## Rejected Features | ||
|
|
||
| Keep this updated with features that were discussed on the mail lists. | ||
|
withinboredom marked this conversation as resolved.
Outdated
|
||
Uh oh!
There was an error while loading. Please reload this page.