|
| 1 | +# PHP RFC: Records |
| 2 | + |
| 3 | +- Version: 0.9 |
| 4 | +- Date: 2024-07-19 |
| 5 | +- Author: Robert Landers, [email protected] |
| 6 | +- Status: Draft (or Under Discussion or Accepted or Declined) |
| 7 | +- First Published at: <http://wiki.php.net/rfc/records> |
| 8 | + |
| 9 | +## Introduction |
| 10 | + |
| 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. |
| 18 | + |
| 19 | +## Proposal |
| 20 | + |
| 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. |
| 27 | + |
| 28 | +#### Syntax and semantics |
| 29 | + |
| 30 | +##### Definition |
| 31 | + |
| 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. |
| 36 | + |
| 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. |
| 40 | + |
| 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`. |
| 44 | + |
| 45 | +Static properties and methods are forbidden in a `record` (this includes |
| 46 | +`const`, a regular property may be used instead). Attempting to define |
| 47 | +static properties, methods, constants results in a compilation error. |
| 48 | + |
| 49 | +``` 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]; |
| 59 | + } |
| 60 | +} |
| 61 | + |
| 62 | +record Vector2(int $x, int $y); |
| 63 | + |
| 64 | +record Rectangle(Vector2 $leftTop, Vector2 $rightBottom) implements Shape { |
| 65 | + use Dimension; |
| 66 | + |
| 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; |
| 74 | + } |
| 75 | +} |
| 76 | +``` |
| 77 | + |
| 78 | +##### Usage |
| 79 | + |
| 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 | +``` |
| 90 | + |
| 91 | +##### Optional parameters and default values |
| 92 | + |
| 93 | +A `record` can also be defined with optional parameters that are set if |
| 94 | +left out during instantiation. |
| 95 | + |
| 96 | +``` php |
| 97 | +record Rectangle(int $x, int $y = 10); |
| 98 | +var_dump(Rectangle(10)); // output a record with x: 10 and y: 10 |
| 99 | +``` |
| 100 | + |
| 101 | +##### Auto-generated `with` method |
| 102 | + |
| 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. |
| 107 | + |
| 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. |
| 111 | + |
| 112 | +``` php |
| 113 | +$point1 = Point(3, 4); |
| 114 | +$point2 = $point1->with(x: 5); |
| 115 | +$point3 = $point1->with(null, 10); // must use named arguments |
| 116 | + |
| 117 | +echo $point1->x; // Outputs: 3 |
| 118 | +echo $point2->x; // Outputs: 5 |
| 119 | +``` |
| 120 | + |
| 121 | +A developer may define their own `with` method if they so choose. |
| 122 | + |
| 123 | +#### Performance considerations |
| 124 | + |
| 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. |
| 130 | + |
| 131 | +``` php |
| 132 | +$point1 = Point(3, 4); |
| 133 | +$point2 = $point1; // No data duplication, $point2 references the same data as $point1 |
| 134 | +$point3 = Point(3, 4); // No data duplication here either, it is pointing the the same memory as $point1 |
| 135 | + |
| 136 | +$point4 = $point1->with(x: 5); // Data duplication occurs here, creating a new instance with modified data |
| 137 | +``` |
| 138 | + |
| 139 | +##### Cloning and with() |
| 140 | + |
| 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`. |
| 144 | + |
| 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)`. |
| 149 | + |
| 150 | +#### Equality |
| 151 | + |
| 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`. |
| 156 | + |
| 157 | +Comparison operations will behave exactly like they do for classes. |
| 158 | + |
| 159 | +### Reflection |
| 160 | + |
| 161 | +Records in PHP will be fully supported by the reflection API, providing |
| 162 | +access to their properties and methods just like regular classes. |
| 163 | +However, immutability and special instantiation rules will be enforced. |
| 164 | + |
| 165 | +#### ReflectionClass support |
| 166 | + |
| 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. |
| 172 | + |
| 173 | +``` php |
| 174 | +$point = Point(3, 4); |
| 175 | +$reflection = new \ReflectionClass($point); |
| 176 | + |
| 177 | +foreach ($reflection->getProperties() as $property) { |
| 178 | + echo $property->getName() . ': ' . $property->getValue($point) . PHP_EOL; |
| 179 | +} |
| 180 | +``` |
| 181 | + |
| 182 | +#### Immutability enforcement |
| 183 | + |
| 184 | +Attempts to modify record properties via reflection will throw an |
| 185 | +exception. |
| 186 | + |
| 187 | +``` php |
| 188 | +try { |
| 189 | + $property = $reflection->getProperty('x'); |
| 190 | + $property->setValue($point, 10); // This will throw an exception |
| 191 | +} catch (\ReflectionException $e) { |
| 192 | + echo 'Exception: ' . $e->getMessage() . PHP_EOL; // "Cannot modify a record property" |
| 193 | +} |
| 194 | +``` |
| 195 | + |
| 196 | +#### ReflectionFunction for implicit constructor |
| 197 | + |
| 198 | +Using `ReflectionFunction` on a record will reflect the implicit |
| 199 | +constructor. |
| 200 | + |
| 201 | +``` php |
| 202 | +$constructor = new \ReflectionFunction('Geometry\Point'); |
| 203 | +echo 'Constructor Parameters: '; |
| 204 | +foreach ($constructor->getParameters() as $param) { |
| 205 | + echo $param->getName() . ' '; |
| 206 | +} |
| 207 | +``` |
| 208 | + |
| 209 | +#### New functions and methods |
| 210 | + |
| 211 | +- Calling `is_object($record)` will return `true`. |
| 212 | +- A new function, `is_record($record)`, will return `true` for records, |
| 213 | + and `false` otherwise |
| 214 | +- Calling `get_class($record)` will return the record name |
| 215 | + |
| 216 | +#### var_dump |
| 217 | + |
| 218 | +Calling `var_dump` will look much like it does for objects, but instead |
| 219 | +of `object` it will say `record`. |
| 220 | + |
| 221 | + record(Point)#1 (2) { |
| 222 | + ["x"]=> |
| 223 | + int(1) |
| 224 | + ["y"]=> |
| 225 | + int(2) |
| 226 | + } |
| 227 | + |
| 228 | +### Considerations for implementations |
| 229 | + |
| 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. |
| 233 | + |
| 234 | +### Auto loading |
| 235 | + |
| 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. |
| 243 | + |
| 244 | +## Backward Incompatible Changes |
| 245 | + |
| 246 | +No backward incompatible changes. |
| 247 | + |
| 248 | +## Proposed PHP Version(s) |
| 249 | + |
| 250 | +PHP 8.5 |
| 251 | + |
| 252 | +## RFC Impact |
| 253 | + |
| 254 | +### To SAPIs |
| 255 | + |
| 256 | +N/A |
| 257 | + |
| 258 | +### To Existing Extensions |
| 259 | + |
| 260 | +N/A |
| 261 | + |
| 262 | +### To Opcache |
| 263 | + |
| 264 | +Unknown. |
| 265 | + |
| 266 | +### New Constants |
| 267 | + |
| 268 | +None |
| 269 | + |
| 270 | +### php.ini Defaults |
| 271 | + |
| 272 | +None |
| 273 | + |
| 274 | +## Open Issues |
| 275 | + |
| 276 | +Todo |
| 277 | + |
| 278 | +## Unaffected PHP Functionality |
| 279 | + |
| 280 | +None. |
| 281 | + |
| 282 | +## Future Scope |
| 283 | + |
| 284 | +## Proposed Voting Choices |
| 285 | + |
| 286 | +Include these so readers know where you are heading and can discuss the |
| 287 | +proposed voting options. |
| 288 | + |
| 289 | +## Patches and Tests |
| 290 | + |
| 291 | +TBD |
| 292 | + |
| 293 | +## Implementation |
| 294 | + |
| 295 | +After the project is implemented, this section should contain |
| 296 | + |
| 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) |
| 301 | + |
| 302 | +## References |
| 303 | + |
| 304 | +Links to external references, discussions or RFCs |
| 305 | + |
| 306 | +## Rejected Features |
| 307 | + |
| 308 | +Keep this updated with features that were discussed on the mail lists. |
0 commit comments