Le lun. 14 juil. 2025 à 20:26, Rob Landers <[email protected]> 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