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' => 'some-id']); // no error for missing keys for named arguments On Thu, Jun 5, 2025 at 2:33 PM Bradley Hayes <bradley.ha...@tithe.ly> wrote: > Hey Claude, i did think of the same thing you proposed and I cover that in > the gihub issue 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 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 = uninitialized, > public string $name = uninitialized, > public null|int $age = uninitialized, > ) {} > } > new DTO('some-id'); > // Would produce the same result as this one... > class DTO { > public function __construct(array $parameters) { > foreach ($parameters as $key => $value) { > $this->{$key} = $value; > } > } > } > new DTO(['id' => 'some-id']); > > > On Wed, Jun 4, 2025 at 7:11 PM Claude Pache <claude.pa...@gmail.com> > wrote: > >> >> >> Le 3 juin 2025 à 06:22, Bradley Hayes <bradley.ha...@tithe.ly> a écrit : >> >> 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 = uninitialized, >> public string $name = uninitialized, >> public null|int $age = uninitialized, >> ) {} >> } >> >> $dto = new DTO(id: 'someid', age: null); >> if ($dto->age === null) echo "no age was given\n"; >> echo $dto->name, PHP_EOL; // triggers 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 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 database >> in countless situations and API can assign null to delete a value from an >> 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 = 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 to >> 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 “no value”. Today, `null` is a >> value in itself, and there has been a necessity to have something else to >> encode an uninitialised state, meaning “really, no value”. Although I >> understand your specific use case, I don’t think that it is good long term >> design decision to rely on various built-in variations of general “no >> value” states: maybe tomorrow there will be a request for some “really and >> truly, no value” 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 = DTO_status::uninitialized >> , public string|DTO_status $name = DTO_status::uninitialized >> , public int|null|DTO_status $age = DTO_status::uninitialized >> ) { } >> >> } >> ``` >> >> Or, if you want to rely on the handy error “must not be accessed before >> initialization” 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 = DTO_status::uninitialized >> , string|DTO_status $name = DTO_status::uninitialized >> , int|null|DTO_status $age = DTO_status::uninitialized >> ) { >> foreach ([ 'id', 'name', 'age' ] as $var) { >> if (! ${$var} instanceof DTO_status) { >> $this->$var = ${$var}; >> } >> } >> >> } >> >> } >> ``` >> >> With property hooks, you can support more elaborate things such as >> `$foo->id = DTO_status::deleted`, although you cannot (and should not) rely >> on the built-in “must not be accessed before initialization” 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. >> >> —Claude >> >> >> >> >> >> >> > > -- > > Bradley Hayes / Engineer / TITHE.LY <http://tithe.ly/> > > <http://tithe.ly> > > -- Bradley Hayes / Engineer / TITHE.LY <http://tithe.ly/> <http://tithe.ly>