Newsgroups: php.internals Path: news.php.net Xref: news.php.net php.internals:127603 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 472AC1A00BC for ; Thu, 5 Jun 2025 05:04:19 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=php.net; s=mail; t=1749099735; bh=pyNELjOGfMltWOdZLWMQK1Ct8RvMRqs196nv5Eb/lMs=; h=References:In-Reply-To:From:Date:Subject:To:Cc:From; b=nPSbM/eklhSaOPVHtVh8mXqzLzYiV+vLCz5iMR8TpjD/FRpAzVAit4pGENVShUt0C iF2u4/dogoqiK7k86ehjiq7GjKQxqPd8KDBANpvk4ySpFc2gvE9Ear78EHVaqFvF0m RpKOhPzZWx7yqyITvKjqYCOTsp4VoZFyBWq/l/UVcaQkNIAQz+x7qhMDxl4hW1iWbb U+lARpCZukC4/zYABs88B4hv+UqxWu8yWqYXCpLEhMpjSyki/GSz9/Sb04QSLAwmib N7spC7lFohBqMPvEd1RBnvMv+LIp0Wve1d5yN9fQqIcDTyAriWWDPH9vhnVjSxOdt3 jtX9DT/lO+Olw== Received: from php-smtp4.php.net (localhost [127.0.0.1]) by php-smtp4.php.net (Postfix) with ESMTP id 8584318005D for ; Thu, 5 Jun 2025 05:02:14 +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.7 required=5.0 tests=BAYES_50,DKIM_SIGNED, DKIM_VALID,DKIM_VALID_AU,DMARC_PASS,HEADER_FROM_DIFFERENT_DOMAINS, HTML_MESSAGE,RCVD_IN_DNSWL_NONE,RCVD_IN_MSPIKE_H3,RCVD_IN_MSPIKE_WL, SPF_HELO_NONE,SPF_PASS,T_REMOTE_IMAGE 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-qt1-f173.google.com (mail-qt1-f173.google.com [209.85.160.173]) (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 ; Thu, 5 Jun 2025 05:02:14 +0000 (UTC) Received: by mail-qt1-f173.google.com with SMTP id d75a77b69052e-4a440a72584so5228561cf.2 for ; Wed, 04 Jun 2025 22:04:17 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=tithe.ly; s=google; t=1749099857; x=1749704657; 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=4VELR3FgW7mqyr8621/7jgWRygtV9fMuUyAA7Ptxksk=; b=JCnm0GhCwsk1DjM2OSprppB/Mi2QalDJ+IiZGNtoOIxCp0D6Xw0VXMc9mq2Zsd33cO BFP1DlzQ4djJ5cOR1HZkNmMYLLPrtzQVV8nGsG/Cy7lpqmAOvPshZ9yzwW3czPV3MXzv /1+1xfQXBGZltbHy1Nds0FprkcxaJkjGAq6K0= X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20230601; t=1749099857; x=1749704657; 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=4VELR3FgW7mqyr8621/7jgWRygtV9fMuUyAA7Ptxksk=; b=YX3JGzAVqigIW5Hg9YRWoVeJ2ItGEEJQ4wxOPojdYho7eK2Gq8veEtGSBPETjTkycc bzSIKMgC8aDPJs5uxFNpfssDJ9E3cBU8ndyJ0AhnWt32EzYHZRZ7zalgDppIgSrhDOgy 6QZt03MTDuYr+zEFAJpClio/t2p6paeNnzwzHLUNlZwbffq/6ZeApahS5aUgejGLIhqu agrwcmBV/L6hFyXqxzDAzdSlltCS9H8wi/OIcL3wKIeKg8CWZFjWZAyKZ3UWfknqmK/k YLiKB4FpKmDvCJMNpYxFBTO1S35aIjdVs60VqGO0MKBkpaCG3K6+/TD+Pnt5sFD1AFYs CAuA== X-Gm-Message-State: AOJu0YxE6K5nEt+GzWaZIrdBLr+DEiSnntXew/U0KL37QTLL9YqPaJeV VGInNt0Wsp+v4BlkWdIIsjZsbr0mepNGf+0V45lPDIAuVXWNRlcXMTZPVZ3f1E2/M+ZN1XQuJUg aPrJgLPzNjDIUANEGQxedhLzYPu5eHf7RaXGQB46j/Q== X-Gm-Gg: ASbGncvkly/2lAe0V/mC55ItBp1RrMOXATW/UWUQXgFysYIcU+ImXEb1pH2Sd+PygfC mJoJmQgS3orRsVtRownXC7dE+pfbt33Z0hU97CtZJEdPr8pgIS17sNW/MhvHUx5fbe+GptsqT1h BmDLofB1YKRbnFsduQpVNKeTfNLdup5AI4xd8= X-Google-Smtp-Source: AGHT+IGdN/vyviE5leuxpvImlODE8kf0pSSnvfLjzeuermKvZx2AZ2zGXyXxVRqmVIoW8NyFVcuEb6OI8B/qgeiZs/8= X-Received: by 2002:a05:622a:5c96:b0:476:875e:516d with SMTP id d75a77b69052e-4a5a58019f2mr71613201cf.36.1749099856982; Wed, 04 Jun 2025 22:04:16 -0700 (PDT) Precedence: bulk list-help: list-post: List-Id: internals.lists.php.net x-ms-reactions: disallow MIME-Version: 1.0 References: In-Reply-To: Date: Thu, 5 Jun 2025 15:03:41 +1000 X-Gm-Features: AX0GCFsXwNyGF_Mpz4CkN3nCkW2ni0uKCBdK51A3PnUIcU5XcIPNlyXGcLlsMOo Message-ID: Subject: Re: [PHP-DEV] Allowing class properties to remain Uninitialized as a default value. To: Claude Pache Cc: internals@lists.php.net Content-Type: multipart/alternative; boundary="000000000000a70b030636cc0bd7" From: bradley.hayes@tithe.ly (Bradley Hayes) --000000000000a70b030636cc0bd7 Content-Type: text/plain; charset="UTF-8" Content-Transfer-Encoding: quoted-printable Here is another way to think about it, trying to make the arguments optional not have a new value type. Optional arguments currently requires a default value. Assigning values to properties is truly optional but we cant reflect this in the constructor parameters. class DTO { public function __construct( public string $id, optional public string $name, optional public null|int $age, ) {} } new DTO('some-id'); new DTO(...['id' =3D> 'some-id']); // no error for missing keys for named arguments On Thu, Jun 5, 2025 at 2:33=E2=80=AFPM Bradley Hayes wrote: > Hey Claude, i did think of the same thing you proposed and I cover that i= n > the gihub issue I linked to. This doesnt replicate the uninitialized stat= e. > > Doing ti with custom classes or enums means now not only are forced to > manually check each property everywhere it might be used you also have to > write thousands of checks and throw errors across all the constructors of > these objects. > > When properties have an uninitialized state you remove the need to handle > any of this when loops and serializers simply exclude them. > > Uninitialized states is already an incredibly useful feature that exists > right now. > > Im only proposing that we have a way to tell the constructors to ignore > the parameter instead of being forced to have array as the only parameter= . > > I get what you mean about null, but null is serving a different purpose. > Its a value that represents nothing, so that you can assign something as > nothing. > > Uninitialized is not like assigning null to a variable. > > // This implementation: > class DTO { > public function __construct( > public string $id =3D uninitialized, > public string $name =3D uninitialized, > public null|int $age =3D uninitialized, > ) {} > } > new DTO('some-id'); > // Would produce the same result as this one... > class DTO { > public function __construct(array $parameters) { > foreach ($parameters as $key =3D> $value) { > $this->{$key} =3D $value; > } > } > } > new DTO(['id' =3D> 'some-id']); > > > On Wed, Jun 4, 2025 at 7:11=E2=80=AFPM Claude Pache > wrote: > >> >> >> Le 3 juin 2025 =C3=A0 06:22, Bradley Hayes a = =C3=A9crit : >> >> Uninitialized properties are really useful. >> Being skipped in foreach loops and JSON encoded results and other >> behaviours around uninitialized properties save a lot of time wasted on >> basic checks and uncaught logical mistakes around null values. >> >> With the introduction of named arguments and promoted constructor >> properties and read-only classes, it would be great to have the true >> ability to not specify a value. >> >> class DTO { >> public function __construct( >> public string $id =3D uninitialized, >> public string $name =3D uninitialized, >> public null|int $age =3D uninitialized, >> ) {} >> } >> >> $dto =3D new DTO(id: 'someid', age: null); >> if ($dto->age =3D=3D=3D null) echo "no age was given\n"; >> echo $dto->name, PHP_EOL; // triggers the standard access before initia= lisation error >> >> >> EXAMPLE: A graphQL like API that only returns data that was asked for, i= s >> serviced by a PHP class that only fetched the data that was asked for an= d >> thus the DTO only has assigned values if they were fetched. >> (These situations usually way more complex involving multiple SQL >> joins/filters etc and nested objects/arrays in the return DTO). >> >> The DTO object has all the possible values defined on the class for type >> safety and IDE indexing, but allows the uninitialized error to happen if >> you try to use data that was never requested. >> Uninitialized Errors when directly accessing a property that was not >> assigned is also desirable as it indicates a logical error instead of >> thinking the value is null. Null is considered a real value in the datab= ase >> in countless situations and API can assign null to delete a value from a= n >> object. >> >> Additionally, since array unpacking now directly maps to named arguments= this would also save a ton of mapping code. >> >> *//array unpacking direct from the source >> *$dto =3D new DTO( ...$sqlData); >> >> (FYI: SQL is way faster at mapping thousands of values to the naming >> convention of the class than doing it in php so we do it in SQL. So yes = we >> would directly array unpack an sql result here.) >> >> I have is a discussion on this in github here: >> https://github.com/php/php-src/issues/17771 >> >> The current workaround is to make the constructor take an array as its >> only parameter and looping over it assigning matching array key values t= o >> class properties and ignoring the rest. >> >> This works but breaks indexing and prevents the use of class inheritance >> because not all the properties can be seen from the same scope forcing >> every extender of the class to copy paste the constructor code from the >> parent class. >> >> >> >> >> Hi Bradley, >> >> Originally, `null` was intended to mean =E2=80=9Cno value=E2=80=9D. Toda= y, `null` is a >> value in itself, and there has been a necessity to have something else t= o >> encode an uninitialised state, meaning =E2=80=9Creally, no value=E2=80= =9D. Although I >> understand your specific use case, I don=E2=80=99t think that it is good= long term >> design decision to rely on various built-in variations of general =E2=80= =9Cno >> value=E2=80=9D states: maybe tomorrow there will be a request for some = =E2=80=9Creally and >> truly, no value=E2=80=9D state? Instead, I think one should use >> application-specific states. With enums and union types, it is possible: >> >> ```php >> enum DTO_status { >> case uninitialized; >> case deleted; >> } >> >> >> class DTO { >> >> function __construct( >> public int|DTO_status $id =3D DTO_status::uninitialized >> , public string|DTO_status $name =3D DTO_status::uninitialized >> , public int|null|DTO_status $age =3D DTO_status::uninitialized >> ) { } >> >> } >> ``` >> >> Or, if you want to rely on the handy error =E2=80=9Cmust not be accessed= before >> initialization=E2=80=9D for free, you could also write: >> >> ```php >> class DTO { >> >> public int $id; >> public string $name; >> public int|null $age; >> >> function __construct( >> int|DTO_status $id =3D DTO_status::uninitialized >> , string|DTO_status $name =3D DTO_status::uninitialized >> , int|null|DTO_status $age =3D DTO_status::uninitialized >> ) { >> foreach ([ 'id', 'name', 'age' ] as $var) { >> if (! ${$var} instanceof DTO_status) { >> $this->$var =3D ${$var}; >> } >> } >> >> } >> >> } >> ``` >> >> With property hooks, you can support more elaborate things such as >> `$foo->id =3D DTO_status::deleted`, although you cannot (and should not)= rely >> on the built-in =E2=80=9Cmust not be accessed before initialization=E2= =80=9D error anymore, >> because you cannot (and are not supposed to) return to the uninitialised >> state: you have to manually throw the appropriate error in the getter. >> >> =E2=80=94Claude >> >> >> >> >> >> >> > > -- > > Bradley Hayes / Engineer / TITHE.LY > > > > --=20 Bradley Hayes / Engineer / TITHE.LY --000000000000a70b030636cc0bd7 Content-Type: text/html; charset="UTF-8" Content-Transfer-Encoding: quoted-printable
Here is another way to think about it, trying=C2=A0to make= the arguments optional not have a new value type.
Optional arguments currently requ=
ires a default value.
Assigni= ng values to properties is truly optional but we cant reflect this in the c= onstructor parameters.
class DTO {
public = function __construct(
= public string $id= ,
optional public string $name,
optional public null|int $age,
) {}
}<= br>new DTO('some-id'= ;);
new DTO(...['id' =3D> 'some-id']); // no= error for missing keys for named arguments


O= n Thu, Jun 5, 2025 at 2:33=E2=80=AFPM Bradley Hayes <bradley.hayes@tithe.ly> wrote:
Hey Claude, i= did think of the same thing you proposed and I cover that in the gihub iss= ue I linked to. This doesnt replicate the uninitialized state.

Doing ti with custom classes or enums means now not only are forced = to manually check each property everywhere it might be used you also have= =C2=A0to write=C2=A0thousands=C2=A0of checks and throw errors across all th= e=C2=A0constructors of these objects.

When properties have an uninit= ialized state you=C2=A0remove the need to handle any of this when loops and= serializers simply exclude=C2=A0them.

Uninitializ= ed states is already=C2=A0an incredibly useful feature that exists right no= w.

Im only proposing that we have a way to tell th= e constructors to ignore the parameter instead of being forced to have arra= y as the only parameter.

I get what you mean about n= ull, but null is serving a different purpose. Its a value that represents n= othing, so that you can assign something as nothing.

Uninitialized is not like assigning null to a variable.
<= /div>

<=
span style=3D"color:rgb(151,151,151)">// This implementation:
class DTO {
public func= tion __construct(
= public string $id =3D uninitialized,
public string $name =3D uninitialized,
public null|in= t $age =3D uninitialized,
) {}
}
new <= span style=3D"color:rgb(0,116,195)">DTO
('some-id');// Would produce the same re= sult as this one...
class DTO {
public function __construct(array <= span style=3D"color:rgb(126,0,186)">$parameters
) {
foreach ($parameters as $key =3D> $value) {
$this->{$key} = =3D $value;
<= /span>}
}
}
new DTO
(['id' =3D> '= ;some-id']);
<= /div>

On Wed, Jun 4, 2025 at 7:11=E2=80=AFPM Claude Pache <claude.pache@gmail.com= > wrote:
=


Le 3 juin 2025 =C3=A0= 06:22, Bradley Hayes <bradley.hayes@tithe.ly> a =C3=A9crit :

Uninitialized properties are really useful.
Being skipped in foreach loops and JSON encoded results and other behavi= ours around uninitialized properties save a lot of time wasted on basic che= cks and uncaught logical mistakes around null values.

<= div>With the introduction of named arguments and promoted constructor prope= rties and read-only classes, it would be great to have=C2=A0the true abilit= y to not specify a value.
class DTO {
public function __construct(
public string $id =3D= uninitialized,
public string $name
=3D uninitialized,
public null|int $age =3D uninitialized,
= ) {}
}

$dto =3D new DTO(id: 's= omeid', age: null);
if ($dto->age =3D=3D=3D null) echo "no age was given\n&qu= ot;;
echo $dto->name, PHP= _EOL; // tr= iggers the standard access before initialisation error

EXAMPLE: A graphQL like API that only returns= data that was asked for, is serviced by a PHP class that only fetched the = data that was asked for and thus the DTO only has assigned values if they w= ere fetched.
(These situations usually way more complex involving= multiple=C2=A0SQL joins/filters etc and nested objects/arrays in the retur= n DTO).

The DTO object has all the possible values= defined on the class for type safety and IDE indexing, but allows the unin= itialized error to happen if you try to use data that was never requested.<= /div>
Uninitialized Errors when directly accessing a property that= was not assigned is also desirable as it indicates a logical error instead= of thinking the value is null. Null is considered a real value in the data= base in countless situations and API can assign null to delete a value from= an object.
Additionally, since array unpacking now directly maps to named argum=
ents this would also save a ton of mapping code.
<= div>
//array unpacking direct from=
 the source
$dto =3D new DTO( ..=
.$sqlData<=
span style=3D"color:rgb(8,8,8);font-size:9.8pt">);
=
(FYI: SQL is way faster at mapping thousands of values to = the naming convention of the class than doing it in php so we do it in SQL.= So yes we would directly array unpack an sql result here.)

<= /div>
I have is a discussion on this in github here:

The curren= t workaround is to make the constructor take an array as its only parameter= and looping over it assigning matching array key values to class propertie= s and ignoring the rest.

This works but breaks ind= exing and prevents the use of class inheritance because not all the propert= ies can be seen from the same scope forcing every extender of the class to = copy paste the constructor code from the parent class.




Hi Bradley,

= Originally, `null` was intended to mean =E2=80=9Cno value=E2=80=9D. Today, = `null` is a value in itself, and there has been a necessity to have somethi= ng else to encode an uninitialised state, meaning=C2=A0=E2=80=9Creally, no = value=E2=80=9D. Although I understand your specific use case, I don=E2=80= =99t think that it is good long term design decision to rely on various bui= lt-in variations of general =E2=80=9Cno value=E2=80=9D states: maybe tomorr= ow there will be a request for some =E2=80=9Creally and truly, no value=E2= =80=9D state? Instead, I think one should use application-specific states. = With enums and union types, it is possible:

```php=
enum DTO_status {
=C2=A0 =C2=A0 case uninitialize= d;
=C2=A0 =C2=A0 case deleted;
}


class DTO {
=C2=A0 =C2=A0=C2=A0
=C2= =A0 =C2=A0 function __construct(
=C2=A0 =C2=A0 =C2=A0 =C2=A0 publ= ic int|DTO_status $id =3D DTO_status::uninitialized
=C2=A0 =C2=A0= =C2=A0 , public string|DTO_status $name =3D DTO_status::uninitialized
=C2=A0 =C2=A0 =C2=A0 , public int|null|DTO_status $age =3D DTO_status= ::uninitialized
=C2=A0 =C2=A0 ) { }
=C2=A0 =C2=A0=C2=A0=
}
```

Or, if you want to rely= on the handy error =E2=80=9Cmust not be accessed before initialization=E2= =80=9D for free, you could also write:

```php
class DTO {

=C2=A0 =C2=A0 public int $i= d;
=C2=A0 =C2=A0 public string $name;
=C2=A0 =C2=A0 pub= lic int|null $age;
=C2=A0 =C2=A0=C2=A0
=C2=A0 =C2=A0 fu= nction __construct(
=C2=A0 =C2=A0 =C2=A0 =C2=A0 int|DTO_status $i= d =3D DTO_status::uninitialized
=C2=A0 =C2=A0 =C2=A0 , string|DTO= _status $name =3D DTO_status::uninitialized
=C2=A0 =C2=A0 =C2=A0 = , int|null|DTO_status $age =3D DTO_status::uninitialized
=C2=A0 = =C2=A0 ) {=C2=A0
=C2=A0 =C2=A0 =C2=A0 =C2=A0 foreach ([ 'id&#= 39;, 'name', 'age' ] as $var) {
=C2=A0 =C2=A0 =C2= =A0 =C2=A0 =C2=A0 =C2=A0 if (! ${$var} instanceof DTO_status) {
= =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 $this->$var =3D = ${$var};
=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 }
=C2=A0 =C2=A0=C2=A0
}
```

With property hooks, you can support m= ore elaborate things such as `$foo->id =3D DTO_status::deleted`, althoug= h you cannot (and should not) rely on the built-in =E2=80=9Cmust not be acc= essed before initialization=E2=80=9D error anymore, because you cannot (and= are not supposed to) return to the uninitialised state: you have to manual= ly throw the appropriate error in the getter.

=E2= =80=94Claude



<= br>




--
=

Bradley Hayes / E= ngineer / TITHE.LY






--

Bradley Hayes= / Engineer / TITHE.= LY


<= /span>


--000000000000a70b030636cc0bd7--