You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
@@ -12,7 +12,7 @@ This RFC proposes the introduction of ''%%record%%'' objects, which are immutabl
12
12
13
13
==== Value objects ====
14
14
15
-
Value objects are immutable objects that represent a value. They’re used to store values with a different semantic meaning than 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.
15
+
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.
16
16
17
17
Consider this example where a function accepts an integer as a user ID, and the ID is accidentally set to a nonsensical value:
18
18
@@ -76,6 +76,8 @@ A **record** body may also declare properties whose values are only mutable duri
76
76
77
77
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.
78
78
79
+
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.
80
+
79
81
<code php>
80
82
namespace Paint;
81
83
@@ -125,7 +127,26 @@ record PaintBucket(StockPaint ...$constituents) {
125
127
126
128
=== Usage ===
127
129
128
-
A record may be used as a readonly class, as the behavior of the two is very similar, assisting in migrating from one implementation to another.
130
+
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:
Records are instantiated in a function format, with ''%%&%%'' prepended. This provides visual feedback that a record is being created instead of a function call.
@@ -135,7 +156,7 @@ One or more properties defined in the inline constructor may have a default valu
135
156
136
157
<code php>
137
158
record Rectangle(int $x, int $y = 10);
138
-
var_dump(Rectangle(10)); // output a record with x: 10 and y: 10
159
+
var_dump(&Rectangle(10)); // output a record with x: 10 and y: 10
139
160
</code>
140
161
141
162
=== Auto-generated with method ===
@@ -163,7 +184,7 @@ record UserId(int $id) {
163
184
}
164
185
}
165
186
166
-
$userId = UserId(1);
187
+
$userId = &UserId(1);
167
188
$otherId = $userId->with(2); // Fails: Named arguments must be used
168
189
$otherId = $userId->with(serialNumber: "U2"); // Error: serialNumber is not defined in the inline constructor
169
190
$otherId = $userId->with(id: 2); // Success: id is updated
@@ -174,20 +195,15 @@ Using variadic arguments:
174
195
<code php>
175
196
record Vector(int $dimensions, int ...$values);
176
197
177
-
$vector = Vector(3, 1, 2, 3);
198
+
$vector = &Vector(3, 1, 2, 3);
178
199
$vector = $vector->with(dimensions: 4); // Success: values are updated
179
200
$vector = $vector->with(dimensions: 4, 1, 2, 3, 4); // Error: mixing named arguments with variadic arguments is not allowed by PHP syntax
180
201
$vector = $vector->with(dimensions: 4)->with(1, 2, 3, 4); // Success: First update dimensions, then values
181
202
</code>
182
203
183
204
== Custom with method ==
184
205
185
-
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 is updated.
186
-
187
-
Contravariance and covariance are enforced in the developer’s code via the ''%%Record%%'' interface:
188
-
189
-
* Contravariance: the parameter type of the custom ''%%with%%'' method must be a supertype of the generated ''%%with%%'' method.
190
-
* Covariance: the return type of the custom ''%%with%%'' method must be ''%%self%%'' of the generated ''%%with%%'' method.
206
+
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.
191
207
192
208
<code php>
193
209
record Planet(string $name, int $population) {
@@ -212,43 +228,57 @@ The inline constructor is always required and must define at least one parameter
212
228
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.
213
229
214
230
<code php>
215
-
// Inline constructor
216
-
record User(string $name, string $email) {
231
+
// Inline constructor defining two properties
232
+
record User(string $name, string $emailAddress) {
217
233
public string $id;
218
234
219
235
// Traditional constructor
220
236
public function __construct() {
221
-
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
237
+
if (!is_valid_email($this->emailAddress)) {
222
238
throw new InvalidArgumentException("Invalid email address");
223
239
}
224
240
225
-
$this->id = hash('sha256', $email);
226
-
$this->name = ucwords($name);
241
+
$this->id = hash('sha256', $this->emailAddress);
242
+
$this->name = ucwords($this->name);
243
+
// all properties are now immutable
227
244
}
228
245
}
229
246
</code>
230
247
231
248
==== Mental models and how it works ====
232
249
233
-
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 retrieves an existing object from an array.
250
+
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.
234
251
235
252
For example, this would be a valid mental model for a Point record:
// copy properties to an immutable Point and return it
336
+
$point = new Point($mutablePoint->x, $mutablePoint->y);
337
+
$point->magnitude = $mutablePoint->magnitude;
300
338
return $points[$key] = $point;
301
339
}
302
340
</code>
303
341
304
-
In reality, this is quite different from how it works in the engine, but this provides a mental model of how behavior should be expected to work. In other words, if it can work in the above model, then it be possible.
342
+
In reality, this is quite different from how it works in the engine, but this provides a mental model of how behavior should be expected to work.
305
343
306
344
==== Performance considerations ====
307
345
308
-
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.
346
+
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.
309
347
310
348
<code php>
311
-
$point1 = Point(3, 4);
349
+
$point1 = &Point(3, 4);
312
350
$point2 = $point1; // No data duplication, $point2 references the same data as $point1
313
351
$point3 = Point(3, 4); // No data duplication, it is pointing to the same memory as $point1
314
352
315
353
$point4 = $point1->with(x: 5); // Data duplication occurs here, creating a new instance
354
+
$point5 = &Point(5, 4); // No data duplication, it is pointing to the same memory as $point4
316
355
</code>
317
356
318
357
=== Cloning and with() ===
319
358
320
359
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%%''.
321
360
322
-
''%%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)%%''.
361
+
If ''%%->with()%%'' is called with no arguments, a warning will be emitted, as this is most likely a mistake.
323
362
324
363
==== Serialization and deserialization ====
325
364
326
-
Records are fully serializable and deserializable.
365
+
Records are fully serializable and deserializable, even when nested.
If a record contains objects or values that are unserializable, the record will not be serializable.
379
+
339
380
==== Equality ====
340
381
341
382
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%%''.
@@ -344,7 +385,7 @@ Comparison operations will behave exactly like they do for classes, which is cur
344
385
345
386
=== Non-trivial values ===
346
387
347
-
For non-trivial values (e.g., objects, closures, resources, etc.), the ''%%===%%'' operator will return ''%%true%%'' if the two operands reference the same object.
388
+
For non-trivial values (e.g., objects, closures, resources, etc.), the ''%%===%%'' operator will return ''%%true%%'' if the two operands reference the same instances.
348
389
349
390
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:
@@ -412,21 +453,25 @@ Calling ''%%finalizeRecord()%%'' on a record that has already been finalized wil
412
453
413
454
=== isRecord() ===
414
455
415
-
The ''%%isRecord()%%'' method is used to determine if an object is a record. It returns ''%%true%%'' if the object is a record,
456
+
The ''%%isRecord()%%'' method is used to determine if an object is a record. It returns ''%%true%%'' if the object is a record.
416
457
417
458
=== getInlineConstructor() ===
418
459
419
460
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.
420
461
462
+
Invoking the ''%%invoke()%%'' method on the ''%%ReflectionFunction%%'' will create a finalized record.
463
+
421
464
=== getTraditionalConstructor() ===
422
465
423
466
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.
424
467
468
+
Invoking the ''%%invoke()%%'' method on the ''%%ReflectionMethod%%'' on a finalized record will throw an exception.
469
+
425
470
=== makeMutable() ===
426
471
427
472
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.
428
473
429
-
A mutable record can be finalized again using ''%%finalizeRecord()%%'' and to the engine, these are regular classes. For example, ''%%var_dump()%%'' will output ''%%object%%'' instead of ''%%record%%''.
474
+
A mutable record can be finalized again using ''%%finalizeRecord()%%''. A mutable record will not be considered a record by ''%%isRecord()%%'' or implement the ''%%\Record%%'' interface. It is a regular object with the same properties and methods as the record. For example, ''%%var_dump()%%'' will output ''%%object%%'' instead of ''%%record%%''.
430
475
431
476
=== isMutable() ===
432
477
@@ -439,10 +484,10 @@ In cases where custom deserialization is required, a developer can use ''%%Refle
439
484
<code php>
440
485
record Seconds(int $seconds);
441
486
442
-
$example = Seconds(5);
487
+
$example = &Seconds(5);
443
488
444
-
$reflector = new ReflectionRecord(ExpirationDate::class);
A ''%%record%%'' cannot share its name with an existing ''%%record%%'', ''%%class%%'', or ''%%function%%'' because defining a ''%%record%%'' creates both a ''%%class%%'' and a ''%%function%%'' with the same name.
512
+
A ''%%record%%'' cannot share its name with an existing ''%%record%%'', ''%%class%%'', ''%%interface%%'', ''%%trait%%'', or ''%%function%%'', just like a class.
468
513
469
514
==== Autoloading ====
470
515
471
-
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.
472
-
473
-
Once function autoloading is implemented in PHP at some hopeful point in the future, said autoloader could locate the record and then autoload it.
474
-
475
-
The author of this RFC strongly encourages someone to put forward a function autoloading RFC if autoloading is desired for records.
516
+
Records will be autoloaded in the same way as classes.
476
517
477
518
===== Backward Incompatible Changes =====
478
519
479
520
To avoid conflicts with existing code, the ''%%record%%'' keyword will be handled similarly to ''%%enum%%'' to prevent backward compatibility issues.
480
521
522
+
Since ''%%&%%'' is currently a syntax error when prefixed on a function call, it will be used to denote a record instantiation.
523
+
481
524
===== Proposed PHP Version(s) =====
482
525
483
526
PHP 8.5
@@ -518,25 +561,20 @@ None.
518
561
519
562
===== Proposed Voting Choices =====
520
563
521
-
Include these so readers know where you’re heading and can discuss the proposed voting options.
564
+
2/3 majority.
522
565
523
566
===== Patches and Tests =====
524
567
525
568
TBD
526
569
527
570
===== Implementation =====
528
571
529
-
After the project is implemented, this section should contain
530
-
531
-
- the version(s) it was merged into
532
-
- a link to the git commit(s)
533
-
- a link to the PHP manual entry for the feature
534
-
- a link to the language specification section (if any)
572
+
To be completed during a later phase of discussion.
0 commit comments