11

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?

Robert
  • 1,206
  • 1
  • 17
  • 33
  • "I might want to have an object with typed properties without any values in them to loop over them and populate them" - this seems like a strange approach to population of class members. Can you share an example? – El_Vanja Mar 29 '20 at 13:00
  • Doing some wider searching from [here](https://stitcher.io/blog/typed-properties-in-php-74) - *Fatal error: Uncaught Error: Typed property Foo::$bar must not be accessed before initialization*. So in your case this would say that you cannot access the `$goku` value as it's not initialized. – Nigel Ren Mar 29 '20 at 13:44
  • Also possible of interest - https://stackoverflow.com/questions/59265625/why-i-am-suddenly-getting-a-typed-property-must-not-be-accessed-before-initiali – Nigel Ren Mar 29 '20 at 13:44
  • @El_Vanja - this is quite a straight forward use case. I using Symfony's Param converter Tool. I have a bunch of custom DTO/Request objects like the example one I gave you which have properties and validation rules - vessels for my incoming transformed and validated data. They normally have lot's of additional functionality I need in my project. They are populated upon request. – Robert Mar 29 '20 at 13:51
  • @NigelRen it would only say `$bar must not be accessed before initialization` if I tried to directly access my property. If I just loop over object it's omitted. But yes - this is inconsistency on PHP's part I think. – Robert Mar 29 '20 at 13:53
  • Hmmm... not sure - it would be inconsistent to be able to be able to access it within the `foreach()` and not be able to access it directly. It is consistent in that both don't give you access to the variable. – Nigel Ren Mar 29 '20 at 13:54
  • @NigelRen - yes part you mentioned is consistent - in both cases, you don't get access to the uninitialized property but on the other hand, one case gives you the error and the other one silently fails which is never good. – Robert Mar 29 '20 at 13:57
  • The point of typed properties is to always initialize them. – Rain Mar 29 '20 at 22:26
  • I don't think so. I think the point is to have strictly controlled types. – Robert Mar 29 '20 at 23:34
  • 1
    This is a comment from NikiC in this question https://stackoverflow.com/questions/60112627/how-to-let-phpunit-test-property-initialization-in-php7-4 "FWIW I would also recommend you to initialize the properties to null. That is write public ?Type $prop = null. The basic premise behind uninitialized properties is that you should not have them. They are there to blow up your code if you accidentally forget to initialize it. Unless you have some rather specific requirements, all properties should be initialized once your constructor has finished." – Rain Mar 30 '20 at 01:01
  • @Rain now that you've given this context I see what you mean and I agree now. Although feel kinda strange that this is the main reason for them. – Robert Mar 30 '20 at 17:00

4 Answers4

11

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"
// }
Casimir et Hippolyte
  • 88,009
  • 5
  • 94
  • 125
  • That is a very good point for null cases, thanks for that! However, in my particular case, they shouldn't have any values. `GetPostsRequest ` is only a vessel for transformed request data. This is a purely visual/convenience thing. These classes are designed to be as lean as possible - assigning null values just for the sake of workaround seems like polluting this nice and slim class. But I really like the point about valid default `NULL` value. – Robert Mar 29 '20 at 13:04
  • However, I also think that your solution here might be the best one at the moment. – Robert Mar 29 '20 at 13:08
  • 1
    @Robert: all you can do if you absolutely want to avoid assignements in attributes declaration is to force assignements in the constructor, it just moves the "problem" elsewhere. – Casimir et Hippolyte Mar 29 '20 at 13:09
  • yep - I think that that is the only way. So officially I would say that your solution is the quickest and the cleanest one - making them nullable and assigning `NULL`. Just for fun and a bit of a challenge, I am now writing a reflection solution for that. Since my requests extend parent request I might just as well solve that in parent constructor. I will post my answer below if I manage to get this done. – Robert Mar 29 '20 at 13:47
  • 1
    @Robert: also think that giving default values is after all a precision about the type you want to give to your attributes. In other words a type string with empty string as default value is a different type than a type string with NULL as default value; a kind of other side of the typing. – Casimir et Hippolyte Mar 29 '20 at 13:52
  • Totally agreed! They are different things and therefore we need to be careful using them. However, in my case, I am mostly after the possibility of looping over uninitialized properties. Just so we are clear - I know I don't have to. At this point, I am just curious about what I can do. – Robert Mar 29 '20 at 13:56
2

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);
            }
        }
  • Thanks for your answer. I have to disagree with you. This looks slicker to me. There's nothing wrong with using this operator like that. No one said that you have to use the value it generates. As for the `&&` also have to disagree. It does exactly what it's supposed to do. – Robert Jul 02 '20 at 10:28
  • 1
    @Robert: Thanks. In any case, the way you break line is misleading (`$a ?? $b ? $c : $d` is evaluated as `($a ?? $b) ? $c : $d`, not as `$a ?? ($b ? $c : $d)`). As for the `&&`-vs-`??`: `!$property->isInitialized($this)` gives a `bool`, never `null`, and `$nonnull ?? foo()` will *always* give `$nonnull` without ever calling `foo()`; so in your current code, `$property->getType()->allowsNull()` *is actually never checked* (which is equivalent to [the first version](https://stackoverflow.com/revisions/60915602/1)). – user13852734 Jul 02 '20 at 11:23
  • I am quite sure it works fine. I will look into that during the weekend and I will let you know. If it's wrong I will be happy to fix it but I need to see my whole context in the code. – Robert Jul 02 '20 at 11:26
  • That's quite possible it works fine for your use cases. An example where it makes a difference is https://3v4l.org/5fqUR vs https://3v4l.org/LKikT, but if all your properties are nullable (or have default values, or are initialized in the constructor) then you can just delete the `?? $property->getType()->allowsNull()` dead code ;) *I have added a disclaimer at the top of my answer.* Cheers – user13852734 Jul 02 '20 at 11:52
  • Well spotted, I just looked at my code(written a while ago) and in fact, I have something closer to your second option in your answer. Good job. Most importantly actual code in my app is fine. – Robert Jul 02 '20 at 18:22
  • @Robert: Glad to hear that! ^^ And thanks for letting us know =) – user13852734 Jul 03 '20 at 07:11
  • No worries, that's how things get done. I must have changed it after my initial answer was posted. Again good work man. – Robert Jul 03 '20 at 07:13
1

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.

Robert
  • 1,206
  • 1
  • 17
  • 33
0

Probably not what you want, but you could use reflection:

<?php

class DragonBallClass 
{
    public string $goku;
    public string $bulma = 'Bulma';
    public string $vegeta = 'Vegeta';
}
$ob        = new DragonBallClass;
$reflector = new ReflectionClass($ob);
foreach($reflector->getProperties(ReflectionProperty::IS_PUBLIC) as $prop) {
    echo $prop->name, ':', ($ob->{$prop->name} ?? 'NOT INITIALISED'), "\n";
}

Output:

goku:NOT INITIALISED
bulma:Bulma
vegeta:Vegeta
Progrock
  • 7,373
  • 1
  • 19
  • 25
  • The idea of using reflection is cool, this is what I went for. Check out my answer. I wouldn't populate it with `NOT INITIALISED`. This forces you to do some extra error handling. But Reflections - Yes sir :) – Robert Mar 29 '20 at 15:44
  • 1
    @Robert just noticed your answer. Using ReflectionProperty::isInitialized as you have is probably more explicit. No necessary need this way for the nullable types. – Progrock Mar 29 '20 at 15:45