> 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