> Le 3 juin 2025 à 06:22, Bradley Hayes <[email protected]> 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