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>

Reply via email to