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.
--
Bradley Hayes / Engineer / TITHE.LY http://tithe.ly/
Could be handy to allow this in normal functions too (but this it not
the main use case and could be disallowed):
public static function get(string $name, mixed $default = uninitialized): string
{
$value = getenv($name);
if ($value === false) {
return $default; // throws uninitialized error.
}
return $value;
}
Not specifying a default value can now be done without creating a
class or 'some highly specific string to avoid conflicts' to check for
it just throws an error when it cant return a value.
However, the uninitialized error is too generic for this situation and
youd want a more specific error message.
This would naturally make you think to check for uninitialized instead
of using a try->catch block.
To avoid treating it as a type of value with === a helper function
could be provided changing the function like so:
public static function get(string $name, mixed $default = uninitialized): string
{
$value = getenv($name);
if ($value === false) {
if (isInitialized($default)) return $default; throw new
\Exception("Environment variable '$name' not found.");
}
return $value;
}
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 errorEXAMPLE: 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/17771The 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.--
Bradley Hayes / Engineer / TITHE.LY http://tithe.ly/
--
Bradley Hayes / Engineer / TITHE.LY http://tithe.ly/
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 errorEXAMPLE: 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/17771The 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:
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:
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
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']);
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 errorEXAMPLE: 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/17771The 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: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: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/
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
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.pache@gmail.com
wrote: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 errorEXAMPLE: 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/17771The 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: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: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/
--
Bradley Hayes / Engineer / TITHE.LY http://tithe.ly/
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
Hi Bradley,
You can already achieve this, just don’t add those properties to the
constructor.
Yes, static analysis tools might complain, but that’s fine if your
intention is to leave some properties uninitialized after construction.
The constructor is, by definition, for properties that must be initialized
during object creation.
Kind regards,
Faizan
On Thu, Jun 5, 2025 at 2:33 PM Bradley Hayes bradley.hayes@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.pache@gmail.com
wrote: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 errorEXAMPLE: 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/17771The 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: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: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/
--
Bradley Hayes / Engineer / TITHE.LY http://tithe.ly/