Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Explicit undefined/uninitialized default value for constructor property promotion #17771

Open
b-hayes opened this issue Feb 12, 2025 · 9 comments

Comments

@b-hayes
Copy link

b-hayes commented Feb 12, 2025

Description

There is currently no way to truly make constructor parameters optional forcing workarounds like this:

class DataTransferObject
{
    final public function __construct(array $data)
    {
        foreach ($data as $key => $val) {
            $this->{$key} = $val; // allows us to skip properties but throws an error in read only classes.
        }
    }
}

class OtherClass extends DataTransferObject {
    public string $id;
    public string $name;
    // ... etc
}

The use case for this is the fact that we need to allow properties to be uninitialized for a GraphQL like situation.
You cant use fn arguments because that forces you to have a default value to be optional.
Null is considered a real value in the database and many other aspects of the code and is different from a "missing value".

Uninitialized vars are easily automatically ignored with for each loops and thus avoids a great deal of code needed to simply not return the keys you never asked for in an API response. Related data for things you didnt ask for are not fetched from the database, data calls return only the relevant data AS the property name they will be assigned to in the class making all of this incredibly streamlined.

The only thing thats missing is immutability and adding readonly will prevent the constructor form working.
Getters with setters would defeat the purpose, and copying the same code to hundreds of classes is far from ideal.
Traits can work for reusable code but the base class will still have to enforce some kind of check to make sure that every child class is using the trait. (this is going to be my current work around after damianwadley pointed this out below).

It would be great if there was some way to explicitly indicate an optional parameter without assuming a default value for a nice clean readonly class that allows properties to not have a value.

Like so:

readonly class OtherClass extends DataTransferObject {
    public function __construct(
        public string $id = uninitialized,
        public string $name = uninitialized,
    ) {}
}
new OtherClass( ...$data );

Or perhaps introduce an optional keyword in front of the param, but I think uninitialized as a keyword would potentially become useful for other senarios.

@damianwadley
Copy link
Member

or even a trait to set the state of the readonly class.

A trait can do it. "Language-assisted copy-and-paste" is pretty much literally what you want here. Or maybe I'm missing something?
https://3v4l.org/9G0dm

I don't know if it's just what you typed up there, but this

$this->${$key} = $val; //causes error weather the property exists or not.

isn't the right syntax. That will look for the $key variable, dereference it to another variable, then take that as a property name to assign to. Too much indirection. It should have been just ->$key - which is what I did in the code linked to above.

@b-hayes
Copy link
Author

b-hayes commented Feb 12, 2025

Omg your right! I must have screwed it up when I tested trait usage before.

@b-hayes
Copy link
Author

b-hayes commented Feb 12, 2025

Actually, I still think there would be good merit in having uninitialized as a keyword as it would make many kinds of implementations a lot cleaner, can we change this feature request to be for that instead?

I'll edit the issue.

@b-hayes b-hayes reopened this Feb 12, 2025
@erickcomp
Copy link

erickcomp commented Feb 12, 2025

Hey, @b-hayes.

It can be done in userland like this:

<?php

final class uninitialized
{
    private function __construct() {}
    
    public static function instance(): self
    {
        return new self();
    }
}

define('uninitialized', uninitialized::instance());

class C1
{
    public uninitialized|string $prop1 = uninitialized;
}

$o1 = new C1();

var_dump($o1->prop1, $o1->prop1 === uninitialized);

https://3v4l.org/WHFB2

@b-hayes
Copy link
Author

b-hayes commented Feb 12, 2025

Yeah I did think of that @erickcomp but then you loose the advantages of uninitialized properties.

@iluuu1994
Copy link
Member

IMO, uninitialized properties after the object construction are pretty much always a bad idea. They introduce a hidden value into your type set that the user never knows to check for. It's much better to be explicit. Adding an explicit undefined constant would definitely make the situation worse, so I'm not in favor of it. Nonetheless, you can send an e-mail to the internals mailing list to ask for other opinions if you wish.

@b-hayes
Copy link
Author

b-hayes commented Feb 13, 2025

Your perspective is the right one when objects are meant to be a complete or having no value is fine. But in this situation all data points are optional id say its better practice to run into the uninitialized error.

Manually checking the data type and throwing an error vs expecting the only data types the value can be hold and hitting an error is the same and id argue that property can not be accessed before initiation is more direct. Both tell you that you didnt the data you need before using it.

Object with public properties with no constructor were always like this and can serve this purpose but immutability is desirable and that cant be done without a constructor.

@b-hayes b-hayes changed the title Reusable code for readonly classes Optional properties with readonly classes Feb 13, 2025
@erickcomp
Copy link

@b-hayes

If you want optional constructor params, you can use a named parameters constructor, like this:

<?php

trait HasConstructorWithOptionalParameters {
    public function __construct (...$args) {
        foreach ($args as $prop => $value) {
            if (!\is_string($prop)) {
                throw new \LogicException('Method [' . static::class . '::__construct] must be used with named parameters');
            }
            
            if (!\property_exists($this,$prop)) {
                throw new \RuntimeException("Property [$prop] does not exist on class [" . static::class . ']');
            }
            
            $this->$prop = $value;
        }
    }
}


readonly class DtoClass {
    use HasConstructorWithOptionalParameters;
    
    public string $id;
    public string $name;
}

var_dump(new DtoClass(id: "123", name: "Test"), new DtoClass(...['id' => '456', 'name' => 'Test2']));
$o = new DtoClass(id: '789');
var_dump($o->name);

In this case I did not implement positional arguments, but it's possible using reflection. I actually had to do something similar for one project.
By the way, I know this situation where we don't want "incomplete" objects, but sometimes it's out of our control and we just have to handle it the best way we can. ¯⁠\⁠_⁠(⁠ツ⁠)⁠_⁠/⁠¯

https://3v4l.org/6QTTu

@iluuu1994
Copy link
Member

iluuu1994 commented Feb 14, 2025

It would be great if there was some way to explicitly indicate an optional parameter without assuming a default value for a nice clean readonly class that allows properties to not have a value.

In reality, there is no such thing as "no value". undefined is just another unit value, but it is (unfortunately) implicitly included in all property types. As such, it should be avoided as much as possible. If the values contained in the property value change how the user should interact with the it (e.g. $o->prop ?? $default in this case), the type should reflect that.

But as mentioned, this is just my opinion. Please start a discussion on the mailing list if you think this is a worthwhile feature. Otherwise, the issue will go stale and auto-close in 3 months.

@iluuu1994 iluuu1994 changed the title Optional properties with readonly classes Explicit undefined/uninitialized default value for constructor property promotion Feb 14, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants