Newsgroups: php.internals Path: news.php.net Xref: news.php.net php.internals:128054 X-Original-To: internals@lists.php.net Delivered-To: internals@lists.php.net Received: from php-smtp4.php.net (php-smtp4.php.net [45.112.84.5]) by lists.php.net (Postfix) with ESMTPS id 2E6681A00BC for ; Tue, 15 Jul 2025 17:05:29 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=php.net; s=mail; t=1752599021; bh=6WMcMZFIpiH9FXMzmpG/4ywVzOrSmd88whnnz3cV4sg=; h=References:In-Reply-To:From:Date:Subject:To:Cc:From; b=hLJmPZ/a80UE0yYwEGtGCHyEEMmF7EGemPu2aI7iM0mCdfG7qjj1NbwxixWspBlLR IYNYYrmq/jpSFxiPw1lvGT+3RK4EzXxNdOyPiwNS9fh62vEyjEKQE1VuO4KTABVJaa SUJ7Wv1iO0RvGR+wjsZIi6UPy3fyRjrzf8wqYFCefYb6AUvFgBcpUK0CDoHu8BXOiE XaAu6UBebNLwphYU47uO/JVHzJexS+DqmUeLIIbbd30inM5kEl5khOSsS4Dz0zV/gN slCZFMdFOd8HsrK+jyfF6NALwLT0j4JD9KWrik/l8Se/LwqH0qtgmTHBiUaOSY1SBM H6O6Vp9IxKGIw== Received: from php-smtp4.php.net (localhost [127.0.0.1]) by php-smtp4.php.net (Postfix) with ESMTP id 276C61801E4 for ; Tue, 15 Jul 2025 17:03:40 +0000 (UTC) X-Spam-Checker-Version: SpamAssassin 4.0.1 (2024-03-25) on php-smtp4.php.net X-Spam-Level: X-Spam-Status: No, score=-1.2 required=5.0 tests=BAYES_20,DKIM_SIGNED, DKIM_VALID,DKIM_VALID_AU,DKIM_VALID_EF,DMARC_PASS,FREEMAIL_FROM, HTML_MESSAGE,RCVD_IN_DNSWL_NONE,RCVD_IN_MSPIKE_H2,SPF_HELO_NONE, SPF_PASS autolearn=no autolearn_force=no version=4.0.1 X-Spam-Virus: Error (Cannot connect to unix socket '/var/run/clamav/clamd.ctl': connect: Connection refused) X-Envelope-From: Received: from mail-lf1-f47.google.com (mail-lf1-f47.google.com [209.85.167.47]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits) server-digest SHA256) (No client certificate requested) by php-smtp4.php.net (Postfix) with ESMTPS for ; Tue, 15 Jul 2025 17:03:39 +0000 (UTC) Received: by mail-lf1-f47.google.com with SMTP id 2adb3069b0e04-553ba7f11cbso5738704e87.1 for ; Tue, 15 Jul 2025 10:05:27 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20230601; t=1752599126; x=1753203926; darn=lists.php.net; h=cc:to:subject:message-id:date:from:in-reply-to:references :mime-version:from:to:cc:subject:date:message-id:reply-to; bh=OTQtCm+uumnr6+RpYdT99Xaryfl+/pg+ss1AVYR0mh8=; b=RCdZZeCT0UeLgE0irElMV+adqzKxeN18Ym+5engu5M39MoGPjoWb9ueLQjPBr0RsK2 KQJVbMR/ZTnNH8l6O6ZYrEwbGF/non1dOXR7IQaejmHdvxFiPUdaxBG4AqPhBAzrlY0l J8ADzEauCVVxCtln1PA99ZOfjP7Uzm07x/GRImfcZWOkA96c4ZP0LbxUs9TJ4Y0myxUW IXFzulyIvjKDQ3Oh3st8KIl9PwQvN3JbhNEbVsJ5oMgAxTVlvjjWdETfiWzfFCtFtN+T MjnmbPIt8oIcpHEqQLo3vWGWSCs/ctv7O1RbU6t+QcrBZkLBDQ8BQww6LwR6q81Z14pq sDrw== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20230601; t=1752599126; x=1753203926; h=cc:to:subject:message-id:date:from:in-reply-to:references :mime-version:x-gm-message-state:from:to:cc:subject:date:message-id :reply-to; bh=OTQtCm+uumnr6+RpYdT99Xaryfl+/pg+ss1AVYR0mh8=; b=OiKaLh0ZhHP2RvWHpTHbBVCqed6Wqi+V+l4xTiu1zG3C+tSHo22/BorcjIqzWM8vQF 7967ZCinrBNxXdj26e0aFGc4eBApRHVA93DoYf8lw1o0yT/a/TwxUhs+xsehCTG8WCOk /sBpt/V2bTT1d2R/NXgtjnPKPJNL+WC5XXhQGoa/eGsSIpbDFs6ET08Pg7RViYSlhmol A2E9ZdhvTSV0Bk5XIhGI59I0+7hC3f6b9HNhqqUfGK+54du1Q2+iJzF/IuD7oypcIZ6Y xhp6KDzFMHt/ziWRoTt0UB2dQ6UcEh/yFq6MjDC6h7jT14k4jLs36gHUfEaJ85MpaVPa GFxg== X-Forwarded-Encrypted: i=1; AJvYcCUS+nmqJKmJJmRkUiuXT1Vhyvffwl/ma2QVPuEk6c787sTHb7S1xf7fuN0rw36hxIr4TE/1msP7fBE=@lists.php.net X-Gm-Message-State: AOJu0YxJCqsvr+CBwC2Yg1cDzEty3HI+Av/h/LfHTESAUm+ZkaBfGukY aOXA+tgYl9ndXcIHx+ktIAEOrbvjChKXCZHdWQv2Nw+EyEJDd9dptp9vlJFC/xA+kJCVjjltMm4 C6cRKpp01t4k2N+xCXBvEdJwmDMEqkrrcJQQM X-Gm-Gg: ASbGnct/Gx7wkxSnVCLfWWzmtpLw2hF3DqAElKMNk658KAYm6Sf74P5GuvUfUKB9P+U BEGj/8hDfDuaSABYxbwews42VZ6CIYUTCIeBqLgpRCrDsTOXcknJQ9SJs82+4DWSGiZjOEOyvMN 8Dam98ckQQ1WyEe7chUXIUfKvPvn749Gs4rt0SCIpoLT6hZKvs0Rb5ydCOg2fZWm99CFTnGEqvS R9d X-Google-Smtp-Source: AGHT+IEK3KpWsP45853eG9kJegoYtLqVzzp+sOB+uWPJGnWOTProX7tcPa2LzfeCCVInCIjtQWNf3jntxpYkGuxPWGs= X-Received: by 2002:a05:6512:398f:b0:553:aa32:4105 with SMTP id 2adb3069b0e04-55a2332f218mr110079e87.24.1752599125484; Tue, 15 Jul 2025 10:05:25 -0700 (PDT) Precedence: bulk list-help: list-post: List-Id: internals.lists.php.net x-ms-reactions: disallow MIME-Version: 1.0 References: <0D6532F3-6E95-48B9-B394-E9CC1EC00B56@gmail.com> In-Reply-To: Date: Tue, 15 Jul 2025 19:05:13 +0200 X-Gm-Features: Ac12FXxRTIc_PQEwrCRlgwFoKpb24VMKuEJGMhXA_FbbWt7arzS3kXDeph1JaVk Message-ID: Subject: Re: [PHP-DEV] RFC: Records To: Rob Landers Cc: Dmitry Derepko , internals@lists.php.net Content-Type: multipart/alternative; boundary="0000000000004efe7a0639fac851" From: nicolas.grekas+php@gmail.com (Nicolas Grekas) --0000000000004efe7a0639fac851 Content-Type: text/plain; charset="UTF-8" Content-Transfer-Encoding: quoted-printable Le lun. 14 juil. 2025 =C3=A0 20:26, Rob Landers a =C3= =A9crit : > 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 readonl= y, > so it should be: > > data class Record { > public function __construct(public $a){} // $a is mutable, because wh= y > 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=E2=80=99re completely different keywords. Furthe= r, records > were meant to be nested inside other classes, and I did try to actually > pull that off with regular classes =E2=80=94 and it was declined. This ne= gates 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 i= s > class-like semantics. > > 2. We introduce data classes, which are similar to records and are mutabl= e > by default > > > You=E2=80=99re 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=E2=80=99t created (aka, "new'd"); that=E2=80=99s an intentio= nal 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 vi= a > a constructor (including regular classes). This was a deliberate choice o= f > that implementation. Here's some examples run on that branch: > > > final readonly class Response { > public function __construct( > public int $statusCode, > public string $reasonPhrase, > // ... > ) { > if($this->statusCode >=3D 600) { > throw new LogicException(); > } > } > } > > $test =3D new Response(404, "Not Found"); > var_dump($test); > $test =3D clone($test, ['statusCode' =3D> 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"]=3D> > int(404) > ["reasonPhrase"]=3D> > string(9) "Not Found" > } > object(Response)#2 (2) { > ["statusCode"]=3D> > int(900) > ["reasonPhrase"]=3D> > 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 th= is > 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 >=3D 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 --0000000000004efe7a0639fac851 Content-Type: text/html; charset="UTF-8" Content-Transfer-Encoding: quoted-printable


Le=C2=A0lun. 14= juil. 2025 =C3=A0=C2=A020:26, Rob Landers <rob@bottled.codes> a =C3= =A9crit=C2=A0:
<= u>
Hey Dmitry,

Please remember to bot= tom post!

On Mon, Jul 14, 2025, at 11:16, Dmit= ry Derepko wrote:
Hi, Rob!

I'm just wondering, why th= e implementation differs from a regular class?=C2=A0

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 recor= ds, but use regular `class` and `new Class`. Class should have a modifier &= quot;data" like readonly, so it should be:

da= ta class Record {
=C2=A0 =C2=A0 public function __construct(publi= c $a){} // $a is mutable, because why not?
}

=
Data class will compare with others using field value comparison. More= over, if you want to make it readonly, use the existing keyword to achieve = this:

readonly data class Record {
=C2= =A0 =C2=A0 public function __construct(public $a){} // $a is immutable, bec= ause of "readonly" class modifier
}
<= div>
Data classes are basically structs (as we collectively d= iscovered last December when I tried implementing exactly that). Records ar= e implemented completely differently than structs, even though they seem ve= ry similar. This is largely why they=E2=80=99re completely different keywor= ds. Further, records were meant to be nested inside other classes, and I di= d try to actually pull that off with regular classes =E2=80=94 and it was d= eclined. This negates the short-syntax and nested aspect of records.
<= div>

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. W= e introduce data classes, which are similar to records and are mutable by d= efault

You=E2=80=99re referring to st= ructs, not records.

3. No new constructions to create the new type = of classes, no adjustments for autoloading and other things internally

Records aren=E2=80=99t created (aka, &quo= t;new'd"); that=E2=80=99s an intentional design choice. Whether or= not it is a good one is still up for debate.

4. Eliminate with met= hod, replace `with` method with the new `clone`

=
The new clone is not compatible with records or any other type d= efined 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 {
=C2=A0=C2=A0=C2=A0 public function __construct(=
=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 public int $statusCod= e,
=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 public string $reas= onPhrase,
=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 // ...
=
=C2=A0=C2=A0=C2=A0 ) {
=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0= =C2=A0 if($this->statusCode >=3D 600) {
=C2=A0=C2=A0=C2=A0= =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 throw new LogicException()= ;
=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 }
=C2=A0= =C2=A0=C2=A0 }
}

$test =3D new Respo= nse(404, "Not Found");
var_dump($test);
$test= =3D clone($test, ['statusCode' =3D> 900]);
var_dump($= test);

--- output ---

PHP= Fatal error:=C2=A0 Uncaught Error: Cannot modify protected(set) readonly p= roperty Response::$statusCode from global scope in /home/withinboredom/code= /php-src/test.php:28
Stack trace:
#0 /home/withinboredo= m/code/php-src/test.php(28): clone()
#1 {main}
=C2=A0 t= hrown in /home/withinboredom/code/php-src/test.php on line 28
Fat= al error: Uncaught Error: Cannot modify protected(set) readonly property Re= sponse::$statusCode from global scope in /home/withinboredom/code/php-src/t= est.php:28
Stack trace:
#0 /home/withinboredom/code/php= -src/test.php(28): clone()
#1 {main}
=C2=A0 thrown in /= home/withinboredom/code/php-src/test.php on line 28

Then removing the readonly distinction:

object(R= esponse)#1 (2) {
=C2=A0 ["statusCode"]=3D>
=C2=A0 int(404)
=C2=A0 ["reasonPhrase"]=3D>
=C2=A0 string(9) "Not Found"
}
object(Resp= onse)#2 (2) {
=C2=A0 ["statusCode"]=3D>
= =C2=A0 int(900)
=C2=A0 ["reasonPhrase"]=3D>
=C2=A0 string(9) "Not Found"
}

---

As you can see, it doesn't work for rea= donly classes and allows invalid mutable objects to be created, requiring d= evs to rethink how validation will be structured as this will make any cons= tructor 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 an= d a footgun. I think this will be another weird quirk of PHP from now on an= d it is by design.

Not sure it&= #39;s really a contribution to the discussion but in case you missed it, yo= u can make public properties "public(set)":

<= div>final readonly class Response {
=C2=A0 =C2=A0 public function __cons= truct(
=C2=A0 =C2=A0 =C2=A0 =C2=A0 public public(set) int $statusCode,=C2=A0 =C2=A0 =C2=A0 =C2=A0 public public(set) string $reasonPhrase,
= =C2=A0 =C2=A0 =C2=A0 =C2=A0 // ...
=C2=A0 =C2=A0 ) {
=C2=A0 =C2=A0 = =C2=A0 =C2=A0 if($this->statusCode >=3D 600) {
=C2=A0 =C2=A0 =C2= =A0 =C2=A0 =C2=A0 =C2=A0 throw new LogicException();
=C2=A0 =C2=A0 =C2= =A0 =C2=A0 }
=C2=A0 =C2=A0 }
}

Then your= example works.
That's ugly, but that doesn't have to sta= y that way.
And I'm now wondering why is that not the default= ?

Nicolas --0000000000004efe7a0639fac851--