Le lun. 14 juil. 2025 à 20:26, Rob Landers <rob@bottled.codes> a écrit :
> 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. > Not sure it's really a contribution to the discussion but in case you missed it, you can make public properties "public(set)": final readonly class Response { public function __construct( public public(set) int $statusCode, public public(set) string $reasonPhrase, // ... ) { if($this->statusCode >= 600) { throw new LogicException(); } } } Then your example works. That's ugly, but that doesn't have to stay that way. And I'm now wondering why is that not the default? Nicolas