Re: Allowing class properties to remain Uninitialized as a default value.

From: Date: Wed, 04 Jun 2025 09:11:01 +0000
Subject: Re: Allowing class properties to remain Uninitialized as a default value.
References: 1  Groups: php.internals 
Request: Send a blank email to internals+get-127576@lists.php.net to get a copy of this message

> Le 3 juin 2025 à 06:22, Bradley Hayes <bradley.hayes@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








Thread (7 messages)

« previous php.internals (#127576) next »