Re: RFC: Records
| From: | Rob Landers | Date: | Mon, 14 Jul 2025 18:22:34 +0000 |
| Subject: | Re: RFC: Records | ||
| References: | 1 2 | Groups: | php.internals |
| Request: | Send a blank email to internals+get-128042@lists.php.net to get a copy of this message | ||
Hey Dmitry,
Please remember to bottom post!
On Mon, Jul 14, 2025, at 11:16, Dmitry Derepko wrote:
> Hi, Rob!
>
> I'm just wondering, why the implementation differs from a regular class?
>
> Record makes a "class" immutable and unique, using field value comparison instead of
> object refs. It has a custom
with method which is similar to the new
> clone operator accepted recently. I'd suggest not introducing a
> new keyword "record" and new syntax to create records, but use regular
> class and new Class. Class should
> have a modifier "data" like readonly, so it should be:
>
> data class Record {
> public function __construct(public $a){} // $a is mutable, because why not?
> }
>
> Data class will compare with others using field value comparison. Moreover, if you want to make
> it readonly, use the existing keyword to achieve this:
>
> readonly data class Record {
> public function __construct(public $a){} // $a is immutable, because of
> "readonly" class modifier
> }
Data classes are basically structs (as we collectively discovered last December when I tried
implementing exactly that). Records are implemented completely differently than structs, even though
they seem very similar.. This is largely why they’re completely different keywords. Further,
records were meant to be nested inside other classes, and I did try to actually pull that off with
regular classes — and it was declined. This negates the short-syntax and nested aspect of records.
>
> 1. So record Record {} becomes readonly data class Record {}
A record is not a class just like an enum is not a class. :) Though, it is class-like semantics.
> 2. We introduce data classes, which are similar to records and are mutable by default
You’re referring to structs, not records.
> 3. No new constructions to create the new type of classes, no adjustments for autoloading and
> other things internally
Records aren’t created (aka, "new'd"); that’s an intentional design choice.
Whether or not it is a good one is still up for debate.
> 4. Eliminate with method, replace with method with the new
> clone
The new clone is not compatible with records or any other type defined via a constructor (including
regular classes). This was a deliberate choice of that implementation. Here's some examples run
on that branch:
<?php
final readonly class Response {
public function __construct(
public int $statusCode,
public string $reasonPhrase,
// ...
) {
if($this->statusCode >= 600) {
throw new LogicException();
}
}
}
$test = new Response(404, "Not Found");
var_dump($test);
$test = clone($test, ['statusCode' => 900]);
var_dump($test);
--- output ---
PHP Fatal error: Uncaught Error: Cannot modify protected(set) readonly property
Response::$statusCode from global scope in /home/withinboredom/code/php-src/test.php:28
Stack trace:
#0 /home/withinboredom/code/php-src/test.php(28): clone()
#1 {main}
thrown in /home/withinboredom/code/php-src/test.php on line 28
Fatal error: Uncaught Error: Cannot modify protected(set) readonly property Response::$statusCode
from global scope in /home/withinboredom/code/php-src/test.php:28
Stack trace:
#0 /home/withinboredom/code/php-src/test.php(28): clone()
#1 {main}
thrown in /home/withinboredom/code/php-src/test.php on line 28
Then removing the readonly distinction:
object(Response)#1 (2) {
["statusCode"]=>
int(404)
["reasonPhrase"]=>
string(9) "Not Found"
}
object(Response)#2 (2) {
["statusCode"]=>
int(900)
["reasonPhrase"]=>
string(9) "Not Found"
}
---
As you can see, it doesn't work for readonly classes and allows invalid mutable objects to be
created, requiring devs to rethink how validation will be structured as this will make any
constructor validations entirely bypassable in PHP 8.5+. Previously, you needed to use reflection to
do this, but now it is going to be incredibly easy and a footgun. I think this will be another weird
quirk of PHP from now on and it is by design.
> 5. Inline constructor isn’t necessary and could be proposed separately. I’ve thought
> recently about this feature
I will probably remove this, to be honest. It was for nesting records inside classes or other
records, but this feature was declined, so there isn't really a need for it.
>
> WDYT?
>
>
> (sorry for the duplicate in private mailbox)
>
> ----------
>
> Best regards,
> Dmitrii Derepko.
> @xepozz
Data classes are much more similar to structs than to records -- and are even implemented completely
differently. Records are effectively just well-defined arrays with behaviour attached. They're
not classes, but offer similar semantics.
— Rob