PHP 7.4 Typed properties iteration

I just found something "kinda strange" about PHP 7.4 and I am not sure if it is just me missing something or maybe if it is an actual bug. Mostly I am interested in your opinion/confirmation.

So in PHP, you can iterate over objects properties like this:

class DragonBallClass 
{
    public $goku;
    public $bulma = 'Bulma';
    public $vegeta = 'Vegeta';
}

$dragonBall = new DragonBallClass();

foreach ($dragonBall as $character) {
  var_dump($character);

}

RESULT

NULL
string(5) "Bulma"
string(6) "Vegeta"

Now if we start using strongly typed properties like that:

class DragonBallClass 
{
    public string $goku;
    public string $bulma = 'Bulma';
    public string $vegeta = 'Vegeta';
}

$dragonBall = new DragonBallClass();

foreach ($dragonBall as $character) {
  var_dump($character);

}

We will get a different result:

string(5) "Bulma"
string(6) "Vegeta"

Now what is different:

When you DO NOT assign a default value to strongly typed property it will be of Uninitialized type. Which of course makes sense. The problem is though that if they end up like this you cannot loop over them they will simply be omitted - no error, no anything as you can see in the second example. So I just lose access to them.

It makes sense but just imagine that you have a custom Request/Data class like this:

namespace App\Request\Request\Post;

use App\Request\Request\Request;

class GetPostsRequest extends Request
{
    public string $title = '';
}

Do you see that ugly string assignment? If I want to make my properties on the class iterable then I have to either:

  • drop types
  • assign dummy values

I might want to have an object with typed properties without any values in them to loop over them and populate them if that makes sense.

Is there any better way of doing this? Is there any option to keep types and keep em iterable without having to do this dummy value abomination?

Answers

Update: this answer may be obsolete, but the comments contain an interesting discussion.

@Robert's workaround is buggy; in this part:

        foreach ($reflectedProperties as $property) {
            !$property->isInitialized($this) ??
            $property->getType()->allowsNull() ? $property->setValue($this, null) : null;
        }

the ?? must be corrected to &&.

Moreover that's a misuse of the ternary conditional; just use a classic if:

        foreach ($reflectedProperties as $property) {
            if (!$property->isInitialized($this)
                && $property->getType()->allowsNull()
            ) {
                $property->setValue($this, null);
            }
        }

or:

        foreach ($reflectedProperties as $property) {
            if (!$property->isInitialized($this) && $property->getType()->allowsNull()) {
                $property->setValue($this, null);
            }
        }
Posted on by Anonymous

If you want to allow a typed attribute to be nullable you can simply add a ? before the type and give NULL as default value like that:

class DragonBallClass 
{
    public ?string $goku = NULL;
    public string $bulma = 'Bulma';
    public string $vegeta = 'Vegeta';
}

In this case NULL is a perfectly legitimate value (and not a dummy value).

demo


Also without using ?, you can always merge the class properties with the object properties lists:

class DragonBallClass 
{
    public string $goku;
    public string $bulma = 'Bulma';
    public string $vegeta = 'Vegeta';
}

$dragonBall = new DragonBallClass();

$classProperties = get_class_vars(get_class($dragonBall));
$objectProperties = get_object_vars($dragonBall);

var_dump(array_merge($classProperties, $objectProperties));

// array(3) {
//  ["goku"]=>
//  NULL
//  ["bulma"]=>
//  string(5) "Bulma"
//  ["vegeta"]=>
//  string(6) "Vegeta"
// }

Posted on by Casimir et Hippolyte

Before we start - I think that the answer accepted by me and provided by Casimir is better and more correct than what I came up with(that also goes for the comments).

I just wanted to share my thoughts and since this is a working solution to some degree at least we can call it an answer.

This is what I came up with for my specific needs and just for fun. I was curious about what I can do to make it more the way I want it to be so don't freak out about it ;P I think that this is a quite clean workaround - I know it's not perfect though.

class MyRequest
{
    public function __construct()
    {    
        $reflectedProperties = (new \ReflectionClass($this))->getProperties();
        foreach ($reflectedProperties as $property) {
            !$property->isInitialized($this) ??
            $property->getType()->allowsNull() ? $property->setValue($this, null) : null;
        }
    }

}


class PostRequest extends MyRequest 
{
	public ?string $title;

}

$postRequest = new PostRequest();

// works fine - I have access here!
foreach($postRequest as $property) {
	var_dump($property);
}

The downfall of this solution is that you always have to make types nullable in your class. *However for me and my specific needs* that is totally ok. I don't mind, they would end up as nullables anyway and it might be a nice workaround for a short deadline situation if someone is in a hurry.

It still keeps the original PHP not initialized error though when the type is not nullable. I think that is actually kinda cool now. You get to keep all the things: Slim and lean classes, PHP error indicating the true nature of the problem and possibility to loop over typed properties if you agree to keep them nullable. All governed by native PHP 7 nullable operator.

Of course, this can be changed or extended to be more type-specific if that makes any sense.

Posted on by Robert