Skip to content

Commit bd4cf96

Browse files
committed
apply some off-line feedback
1 parent 3be33e9 commit bd4cf96

2 files changed

Lines changed: 262 additions & 142 deletions

File tree

drafts/records.md

Lines changed: 160 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -8,90 +8,120 @@
88

99
## Introduction
1010

11-
In modern PHP development, the need for concise and immutable data
12-
structures is increasingly recognized. Inspired by the concept of
13-
"records", "data objects", or "structs" in other languages, this RFC
14-
proposes the addition of `record` objects in PHP. These records will
15-
provide a concise and immutable data structure, distinct from `readonly`
16-
classes, enabling developers to define immutable objects with less
17-
boilerplate code.
11+
This RFC proposed the introduction of `record` objects, which are immutable classes
12+
with [value semantics](https://en.wikipedia.org/wiki/Value_semantics).
13+
14+
### Value objects
15+
16+
Value objects are immutable objects that represent a value. They are used to store values with a different meaning than
17+
their technical value.
18+
For example, a `Point` object with `x` and `y` properties can represent a point in a 2D space,
19+
and an `ExpirationDate` can represent a date when something expires.
20+
This prevents developers from accidentally using the wrong value in the wrong context.
21+
22+
Consider this example:
23+
24+
```php
25+
function updateUserRole(int $userId, Role $role): void {
26+
// ...
27+
}
28+
29+
$user = getUser(/*...*/)
30+
$uid = $user->id;
31+
// ...
32+
$uid = 5; // somehow accidentally sets uid to an unrelated integer
33+
// ...
34+
updateUserRole($uid, Role::ADMIN()); // accidental passing of
35+
```
36+
37+
In this example, the uid is accidentally set to a plain integer, and updateUserRole is called with the wrong value.
38+
39+
Currently, the only solution to this is to use a class, but this requires a lot of boilerplate code.
40+
41+
#### The solution
42+
43+
Like arrays, strings, and other values, `record` objects are strongly equal to each other if they contain the same
44+
values.
45+
46+
Let's take a look, using the previous example:
47+
48+
```php
49+
record UserId(int $id);
50+
51+
function updateUserRole(UserId $userId, Role $role): void {
52+
// ...
53+
}
54+
55+
$user = getUser(/*...*/)
56+
$uid = $user->id; // $uid is a UserId object
57+
// ...
58+
$uid = 5;
59+
// ...
60+
updateUserRole($uid, Role::ADMIN()); // This will throw an error
61+
```
1862

1963
## Proposal
2064

21-
This RFC proposes the introduction of a new record keyword in PHP to
22-
define immutable data objects. These objects will allow properties to be
23-
initialized concisely and will provide built-in methods for common
24-
operations such as modifying properties and equality checks using a
25-
function-like instantiation syntax. Records can implement interfaces and
26-
use traits but cannot extend other records or classes.
65+
This RFC proposes the introduction of a new record keyword in PHP to define immutable data objects. These objects will
66+
allow properties to be initialized concisely and will provide built-in methods for common operations such as modifying
67+
properties and equality checks using a function-like instantiation syntax.
68+
Records can implement interfaces and use traits but cannot extend other records or classes;
69+
composition is allowed, however.
2770

2871
#### Syntax and semantics
2972

3073
##### Definition
3174

32-
A `record` is defined by the word "record", followed by the name of its
33-
type, an open parenthesis containing zero or more typed parameters that
34-
become public, immutable, properties. They may optionally implement an
35-
interface using the `implements` keyword. A `record` body is optional.
75+
A `record` is defined by the word "record", followed by the name of its type, an open parenthesis containing one or more
76+
typed parameters that become public, immutable, properties.
77+
They may optionally implement an interface using the `implements` keyword.
78+
A `record` body is optional.
3679

37-
A `record` may NOT contain a constructor; instead of defining initial
38-
values, property hooks should be used to produce computable values
39-
on-demand. Defining a constructor emits a compilation error.
80+
A `record` may contain a constructor with zero arguments to perform further initialization, if required.
81+
If it does not have a constructor, an implicit, empty contstructor is provided.
4082

41-
A `record` body may contain property hooks, methods, and use traits.
42-
Regular properties may also be defined, but they are immutable by
43-
default and are no different than `const`.
83+
A `record` body may contain property hooks, methods, and use traits (so long as they do not conflict with `record`
84+
rules).
85+
Regular properties may also be defined, but they are immutable by default and are no different from `const`.
4486

4587
Static properties and methods are forbidden in a `record` (this includes
4688
`const`, a regular property may be used instead). Attempting to define
4789
static properties, methods, constants results in a compilation error.
4890

4991
``` php
50-
namespace Geometry;
51-
52-
interface Shape {
53-
public function area(): float;
54-
}
55-
56-
trait Dimension {
57-
public function dimensions(): array {
58-
return [$this->width, $this->height];
92+
namespace Paint;
93+
94+
record Pigment(int $red, int $yellow, int $blue) {
95+
public function mix(Pigment $other, float $amount): Pigment {
96+
return $this->with(
97+
red: $this->red * (1 - $amount) + $other->red * $amount,
98+
yellow: $this->yellow * (1 - $amount) + $other->yellow * $amount,
99+
blue: $this->blue * (1 - $amount) + $other->blue * $amount
100+
);
59101
}
60102
}
61103

62-
record Vector2(int $x, int $y);
63-
64-
record Rectangle(Vector2 $leftTop, Vector2 $rightBottom) implements Shape {
65-
use Dimension;
104+
record StockPaint(Pigment $color, float $volume);
66105

67-
public int $height = 1; // this will always and forever be "1", it is immutable.
68-
69-
public int $width { get => $this->rightBottom->x - $this->topLeft->x; }
70-
public int $height { get => $this->rightBottom->y - $this->topLeft->y; }
71-
72-
public function area(): float {
73-
return $this->width * $this->height;
106+
record PaintBucket(StockPaint ...$constituents) {
107+
public function mixIn(StockPaint $paint): PaintBucket {
108+
return $this->with(...$this->constituents, $paint);
109+
}
110+
111+
public function color(): Pigment {
112+
return array_reduce($this->constituents, fn($color, $paint) => $color->mix($paint->color, $paint->volume), Pigment(0, 0, 0));
74113
}
75114
}
76115
```
77116

78117
##### Usage
79118

80-
A `record` may be used as a `readonly class`, as the behavior of it is
81-
very similar with no key differences to assist in migration from
82-
`readonly class`.
83-
84-
``` php
85-
$rect1 = Rectangle(Point(0, 0), Point(1, -1));
86-
$rect2 = $rect1->with(topLeft: Point(0, 1));
87-
88-
var_dump($rect2->dimensions());
89-
```
119+
A `record` may be used as a `readonly class`,
120+
as the behavior of it is very similar with no key differences to assist in migration from `readonly class`.
90121

91122
##### Optional parameters and default values
92123

93-
A `record` can also be defined with optional parameters that are set if
94-
left out during instantiation.
124+
A `record` can also be defined with optional parameters that are set if left out during instantiation.
95125

96126
``` php
97127
record Rectangle(int $x, int $y = 10);
@@ -100,14 +130,12 @@ var_dump(Rectangle(10)); // output a record with x: 10 and y: 10
100130

101131
##### Auto-generated `with` method
102132

103-
To enhance the usability of records, the RFC proposes automatically
104-
generating a `with` method for each record. This method allows for
105-
partial updates of properties, creating a new instance of the record
106-
with the specified properties updated.
133+
To enhance the usability of records, the RFC proposes automatically generating a `with` method for each record.
134+
This method allows for partial updates of properties, creating a new instance of the record with the specified
135+
properties updated.
107136

108-
The auto-generated `with` method accepts only named arguments defined in
109-
the constructor. No other property names can be used, and it returns a
110-
new record object with the given values.
137+
The auto-generated `with` method accepts only named arguments defined in the constructor.
138+
No other property names can be used, and it returns a new record object with the given values.
111139

112140
``` php
113141
$point1 = Point(3, 4);
@@ -118,15 +146,51 @@ echo $point1->x; // Outputs: 3
118146
echo $point2->x; // Outputs: 5
119147
```
120148

121-
A developer may define their own `with` method if they so choose.
149+
A developer may define their own `with` method if they so choose,
150+
and reference the generated `with` method using `parent::with()`.
151+
This allows a developer to define policies or constraints on how data is updated.
152+
153+
``` php
154+
record Planet(string $name, int $population) {
155+
public function with(int $population) {
156+
return parent::with(population: $population);
157+
}
158+
}
159+
$pluto = Planet("Pluto", 0);
160+
// we made it!
161+
$pluto = $pluto->with(population: 1);
162+
// and then we changed the name
163+
$mickey = $pluto->with(name: "Mickey"); // no named argument for population error
164+
```
165+
166+
##### Constructors
167+
168+
Optionally, they may also define a constructor to provide validation or other initialization logic:
169+
170+
```php
171+
record User(string $name, string $email) {
172+
public string $id;
173+
174+
public function __construct() {
175+
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
176+
throw new InvalidArgumentException("Invalid email address");
177+
}
178+
179+
$this->id = hash('sha256', $email);
180+
$this->name = ucwords($name);
181+
}
182+
}
183+
```
184+
185+
During construction, a `record` is fully mutable.
186+
This allows the developer freedom to mutate properties as needed to ensure a canonical representation of an object.
122187

123188
#### Performance considerations
124189

125-
To ensure that records are both performant and memory-efficient, the RFC
126-
proposes leveraging PHP's copy-on-write (COW) semantics (similar to
127-
arrays) and interning values. Unlike interned strings, the garbage
128-
collector will be allowed to clean up these interned records when they
129-
are no longer needed.
190+
To ensure that records are both performant and memory-efficient,
191+
the RFC proposes leveraging PHP's copy-on-write (COW) semantics (similar to arrays) and interning values.
192+
Unlike interned strings, the garbage collector will be allowed to clean up these interned records when they are no
193+
longer needed.
130194

131195
``` php
132196
$point1 = Point(3, 4);
@@ -138,37 +202,33 @@ $point4 = $point1->with(x: 5); // Data duplication occurs here, creating a new i
138202

139203
##### Cloning and with()
140204

141-
Calling `clone` on a `record` results in the exact same record object
142-
being returned. As it is a "value" object, it represents a value and is
143-
the same thing as saying `clone 3`—you expect to get back a `3`.
205+
Calling `clone` on a `record` results in the exact same record object being returned. As it is a "value" object, it
206+
represents a value and is the same thing as saying `clone 3`—you expect to get back a `3`.
144207

145-
`with` may be called with no arguments, and it is the same behavior as
146-
`clone`. This is an important consideration because a developer may call
147-
`$new = $record->with(...$array)` and we don't want to crash. If a
148-
developer wants to crash, they can do by `assert($new !== $record)`.
208+
`with` may be called with no arguments, and it is the same behavior as `clone`.
209+
This is an important consideration because a developer may call `$new = $record->with(...$array)` and we don’t want to
210+
crash.
211+
If a developer wants to crash, they can do by `assert($new !== $record)`.
149212

150213
#### Equality
151214

152-
A `record` is always strongly equal (`===`) to another record with the
153-
same value in the properties, much like an `array` is strongly equal to
154-
another array containing the same elements. For all intents,
155-
`$recordA === $recordB` is the same as `$recordA == $recordB`.
215+
A `record` is always strongly equal (`===`) to another record with the same value in the properties,
216+
much like an `array` is strongly equal to another array containing the same elements.
217+
For all intents, `$recordA === $recordB` is the same as `$recordA == $recordB`.
156218

157219
Comparison operations will behave exactly like they do for classes.
158220

159221
### Reflection
160222

161-
Records in PHP will be fully supported by the reflection API, providing
162-
access to their properties and methods just like regular classes.
223+
Records in PHP will be fully supported by the reflection API,
224+
providing access to their properties and methods just like regular classes.
163225
However, immutability and special instantiation rules will be enforced.
164226

165227
#### ReflectionClass support
166228

167-
`ReflectionClass` can be used to inspect records, their properties, and
168-
methods. Any attempt to modify record properties via reflection will
169-
throw an exception, maintaining immutability. Attempting to create a new
170-
instance via `ReflectionClass` will cause a `ReflectionException` to be
171-
thrown.
229+
`ReflectionClass` can be used to inspect records, their properties, and methods. Any attempt to modify record properties
230+
via reflection will throw an exception, maintaining immutability. Attempting to create a new instance via
231+
`ReflectionClass` will cause a `ReflectionException` to be thrown.
172232

173233
``` php
174234
$point = Point(3, 4);
@@ -181,8 +241,7 @@ foreach ($reflection->getProperties() as $property) {
181241

182242
#### Immutability enforcement
183243

184-
Attempts to modify record properties via reflection will throw an
185-
exception.
244+
Attempts to modify record properties via reflection will throw an exception.
186245

187246
``` php
188247
try {
@@ -195,8 +254,7 @@ try {
195254

196255
#### ReflectionFunction for implicit constructor
197256

198-
Using `ReflectionFunction` on a record will reflect the implicit
199-
constructor.
257+
Using `ReflectionFunction` on a record will reflect the implicit constructor.
200258

201259
``` php
202260
$constructor = new \ReflectionFunction('Geometry\Point');
@@ -209,14 +267,12 @@ foreach ($constructor->getParameters() as $param) {
209267
#### New functions and methods
210268

211269
- Calling `is_object($record)` will return `true`.
212-
- A new function, `is_record($record)`, will return `true` for records,
213-
and `false` otherwise
270+
- A new function, `is_record($record)`, will return `true` for records, and `false` otherwise
214271
- Calling `get_class($record)` will return the record name
215272

216273
#### var_dump
217274

218-
Calling `var_dump` will look much like it does for objects, but instead
219-
of `object` it will say `record`.
275+
Calling `var_dump` will look much like it does for objects, but instead of `object` it will say `record`.
220276

221277
record(Point)#1 (2) {
222278
["x"]=>
@@ -227,19 +283,15 @@ of `object` it will say `record`.
227283

228284
### Considerations for implementations
229285

230-
A `record` cannot be named after an existing `record`, `class` or
231-
`function`. This is because defining a `record` creates both a `class`
232-
and a `function` with the same name.
286+
A `record` cannot be named after an existing `record`, `class` or `function`. This is because defining a `record`
287+
creates both a `class` and a `function` with the same name.
233288

234289
### Auto loading
235290

236-
As invoking a record value by its name looks remarkably similar to
237-
calling a function, and PHP has no function autoloader, auto loading
238-
will not be supported in this implementation. If function auto loading
239-
were to be implemented in the future, an autoloader could locate the
240-
`record` and autoload it. The author of this RFC strongly encourages
241-
someone to put forward a function auto loading RFC if auto loading is
242-
desired for records.
291+
As invoking a record value by its name looks remarkably similar to calling a function,
292+
and PHP has no function autoloader, auto loading will not be supported in this implementation.
293+
If function auto loading were to be implemented in the future, an autoloader could locate the `record` and autoload it.
294+
The author of this RFC strongly encourages someone to put forward a function auto loading RFC if auto loading is desired for records.
243295

244296
## Backward Incompatible Changes
245297

@@ -294,10 +346,10 @@ TBD
294346

295347
After the project is implemented, this section should contain
296348

297-
1. the version(s) it was merged into
298-
2. a link to the git commit(s)
299-
3. a link to the PHP manual entry for the feature
300-
4. a link to the language specification section (if any)
349+
1. the version(s) it was merged into
350+
2. a link to the git commit(s)
351+
3. a link to the PHP manual entry for the feature
352+
4. a link to the language specification section (if any)
301353

302354
## References
303355

0 commit comments

Comments
 (0)