# Constructor promoted property and default value

- Thread: https://externals.io/message/121442
- Original archive: https://news-web.php.net/php.internals/121442
- Started by Pierre on 2023-10-23T14:18:58+00:00
- Messages: 8
- Reply by email to internals@lists.php.net
- More for agents: search with https://externals.io/search.md?q={query} · JSON API at https://externals.io/api · documentation at https://externals.io/llms.txt

---

## #121442 — Pierre — 2023-10-23T14:18:58+00:00

URL: https://externals.io/message/121442#121442

Hello internals,

I stumbled upon this behavior, if I write this:

```php
class Foo
{
     public ?string $prop = null;

     public function __construct(?string $prop = null)
     {
         $this->prop = $prop;
     }
}

class Bar extends Foo
{
     public function __construct(
         public ?string $bar = null,
     ) {}
}

// Echoes nothing, but it works as expected.
echo (new Bar())->prop;
```

It works as intended, but if I replace the `Foo` class using:

```php
class Foo
{
     public function __construct(
         public ?string $prop = null,
     ) {}
}
```

It won't work anymore and I have the following error:

```
PHP Warning:  Uncaught Error: Typed property Foo::$prop must not be 
accessed before initialization in php shell code:9
```

If I understand it correctly:
  - in the first case, default value is attached to the object property, 
so if I omit its constructor, I have the default,
  - in the second case, default value is attached to the constructor 
parameter, and not to the object property, which means that in case the 
parent constructor is not called in the `Bar` class, `$prop` remains 
initialized.

It doesn't sound like a bug, but I think that many people would actually 
expect otherwise: that the constructor promoted property keep their 
default even when constructor is not explicitly called.

Is there any good reason behind this ? Wouldn't it be best to change 
this behavior ? Would it be a risk for backward compatibility (my guess 
is "not that much, probably not a all even") ?

Regards,

--
Pierre

---

## #121444 — Larry Garfield — 2023-10-23T15:16:26+00:00

URL: https://externals.io/message/121442#121444 · In reply to: #121442

> Hello internals,
>
> I stumbled upon this behavior, if I write this:
>
> ```php
> class Foo
> {
>      public ?string $prop = null;
>
>      public function __construct(?string $prop = null)
>      {
>          $this->prop = $prop;
>      }
> }
>
> class Bar extends Foo
> {
>      public function __construct(
>          public ?string $bar = null,
>      ) {}
> }
>
> // Echoes nothing, but it works as expected.
> echo (new Bar())->prop;
> ```
>
> It works as intended, but if I replace the `Foo` class using:
>
> ```php
> class Foo
> {
>      public function __construct(
>          public ?string $prop = null,
>      ) {}
> }
> ```
>
> It won't work anymore and I have the following error:
>
> ```
> PHP Warning:  Uncaught Error: Typed property Foo::$prop must not be 
> accessed before initialization in php shell code:9
> ```
>
> If I understand it correctly:
>   - in the first case, default value is attached to the object property, 
> so if I omit its constructor, I have the default,
>   - in the second case, default value is attached to the constructor 
> parameter, and not to the object property, which means that in case the 
> parent constructor is not called in the `Bar` class, `$prop` remains 
> initialized.
>
> It doesn't sound like a bug, but I think that many people would actually 
> expect otherwise: that the constructor promoted property keep their 
> default even when constructor is not explicitly called.
>
> Is there any good reason behind this ? Wouldn't it be best to change 
> this behavior ? Would it be a risk for backward compatibility (my guess 
> is "not that much, probably not a all even") ?

Where this becomes a problem is readonly properties, since those are not allowed to have default values.  (That would make them constants with worse performance.)  A solution would need to be able to detect that the parent::__construct() isn't called, and then call it anyway, or at least partially call it.  Unfortunately, I can think of many cases where such a call would result in unexpected behavior.

It might be possible to resolve, but it's definitely not simple, and it could easily lead to weird behavior.

--Larry Garfield

---

## #121445 — Pierre — 2023-10-23T15:27:05+00:00

URL: https://externals.io/message/121442#121445 · In reply to: #121444

Le 23/10/2023 à 17:16, Larry Garfield a écrit :
> Where this becomes a problem is readonly properties, since those are not allowed to have default values.  (That would make them constants with worse performance.)  A solution would need to be able to detect that the parent::__construct() isn't called, and then call it anyway, or at least partially call it.  Unfortunately, I can think of many cases where such a call would result in unexpected behavior.
>
> It might be possible to resolve, but it's definitely not simple, and it could easily lead to weird behavior.
>
> --Larry Garfield

Thanks for the explanation !

I see, there is no easy answer, yet current state is, in my opinion, 
unintuitive for developers, and eventually unsatisfying when falling in 
this trap.

That's sad that readonly properties make this not trivial. Maybe some 
kind of object construct-post-hook could detect those case and affect 
variables with default values ? I don't know enough PHP internals to 
give a rational answer to this problem through...

Regards,

--

Pierre

---

## #121446 — Saki Takamachi — 2023-10-23T15:35:36+00:00

URL: https://externals.io/message/121442#121446 · In reply to: #121442

Hi, Pierre

You may have overlooked the existence of the magic method `__unserialize()`. Constructor is not the only way to create instances.

When rebuilding a serialized object, you may need the initial values ​​of properties. This can easily happen if you are using rolling updates and using Redis. The constructor is not called if `__unserialize()` is called. In this case, defining the initial value of a property as an argument to a constructor that is never called confuses us.

Regards.

Saki

---

## #121447 — Pierre — 2023-10-23T15:39:34+00:00

URL: https://externals.io/message/121442#121447 · In reply to: #121446

Le 23/10/2023 à 17:35, Saki Takamachi a écrit :
> Hi, Pierre
>
> You may have overlooked the existence of the magic method `__unserialize()`. Constructor is not the only way to create instances.
>
> When rebuilding a serialized object, you may need the initial values ​​of properties. This can easily happen if you are using rolling updates and using Redis. The constructor is not called if `__unserialize()` is called. In this case, defining the initial value of a property as an argument to a constructor that is never called confuses us.
>
> Regards.
>
> Saki

If I understand your use case properly, you should be confused by 
properties with default values that are not constructor-promoted as well 
? Am I wrong ? In this case, your problem is not with promoted properties ?

Regards,

--

Pierre

---

## #121449 — Saki Takamachi — 2023-10-23T16:11:54+00:00

URL: https://externals.io/message/121442#121449 · In reply to: #121447

> If I understand your use case properly, you should be confused by properties with default values that are not constructor-promoted as well ? Am I wrong ? In this case, your problem is not with promoted properties ?

If we specify it the way you say, the initial values ​​of the constructor arguments will be available even when the constructor is not called.

```
class Foo
{
    public function __construct()
    {
    }
}

$foo = serialize(new Foo()); 
// save to redis

---- update ----

class Foo
{
    public function __construct(
        public $val = 'abc'
    ) {
    }
}

$foo = unserialize($redis_foo);
var_dump($foo->val);
// string(3) "abc"
// Doesn't it look like the constructor is being called?
```

Such behavior felt a little counterintuitive.

Regards.

Saki

---

## #121450 — Pierre — 2023-10-23T16:22:45+00:00

URL: https://externals.io/message/121442#121450 · In reply to: #121449

Le 23/10/2023 à 18:11, Saki Takamachi a écrit :
> > […]
> If we specify it the way you say, the initial values ​​of the constructor arguments will be available even when the constructor is not called.
>
> Such behavior felt a little counterintuitive.

Which then would simply be the same behavior as properties when not 
promoted but declared in the class body instead:

```php

class Foo
{
     public $val = 'abc';
}

$redis_foo = serialize(new Foo());

$foo = unserialize($redis_foo);
var_dump($foo->val);
// string(3) "abc"

```

Right ?

What's the most disturbing in my opinion is that: `class Foo { public 
string $val = 'abc' }` and `class Foo { public function 
__construct(public string $val = 'abc' ) {}}` don't yield the same 
behavior at the time.

Regards,

--

Pierre

---

## #121465 — Robert Landers — 2023-10-25T19:17:47+00:00

URL: https://externals.io/message/121442#121465 · In reply to: #121450

>
> Le 23/10/2023 à 18:11, Saki Takamachi a écrit :
> > […]
>
> Which then would simply be the same behavior as properties when not
> promoted but declared in the class body instead:
>
> ```php
>
> class Foo
> {
>      public $val = 'abc';
> }
>
> $redis_foo = serialize(new Foo());
>
> $foo = unserialize($redis_foo);
> var_dump($foo->val);
> // string(3) "abc"
>
> ```
>
> Right ?
>
> What's the most disturbing in my opinion is that: `class Foo { public
> string $val = 'abc' }` and `class Foo { public function
> __construct(public string $val = 'abc' ) {}}` don't yield the same
> behavior at the time.
>
> Regards,
>
> --
>
> Pierre
>
> --
> 
> To unsubscribe, visit: https://www.php.net/unsub.php
>

Here's a nice and simple example:

https://3v4l.org/DU0tG

class A {
    public string $default = 'default';
}

class B {
    public function __construct(public string $default = 'default') {}
}

$a = new A();
echo "Original A: {$a->default}\n";
$b = new B();
echo "Original B: {$b->default}\n";

$a = (new ReflectionClass($a))->newInstanceWithoutConstructor();
echo "New A: {$a->default}\n";
$b = (new ReflectionClass($b))->newInstanceWithoutConstructor();
echo "New B: {$b->default}\n"; // crashes here

Robert Landers
Software Engineer
Utrecht NL
