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
4587Static properties and methods are forbidden in a ` record ` (this includes
4688` const ` , a regular property may be used instead). Attempting to define
4789static 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
97127record 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
118146echo $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
157219Comparison 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.
163225However, 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
188247try {
@@ -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
295347After 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