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

Reply via email to