Hello internals,
I stumbled upon this behavior, if I write this:
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:
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
Hello internals,
I stumbled upon this behavior, if I write this:
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: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 theBar
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
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
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
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
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
Le 23/10/2023 à 18:11, Saki Takamachi a écrit :
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.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:
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
Le 23/10/2023 à 18:11, Saki Takamachi a écrit :
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.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: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' }
andclass 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:
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