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

Uninitialized promoted constructor property even with default value #18106

Closed
Pierstoval opened this issue Mar 18, 2025 · 4 comments
Closed

Uninitialized promoted constructor property even with default value #18106

Pierstoval opened this issue Mar 18, 2025 · 4 comments

Comments

@Pierstoval
Copy link

Pierstoval commented Mar 18, 2025

Description

TL;DR: 3v4l link here to reproduce: https://3v4l.org/8XAEr

When having a class with promoted properties (readonly or not) having default values, the construction with ReflectionClass::newInstanceWithoutConstructor() doesn't initialize the properties.

Example with this class:

<?php
class WithPromotedProps {
    public function __construct(
        private tring $data = '',
    ) {}
}

var_dump(new WithPromotedProps());

/* Output:
object(WithPromotedProps)#1 (1) {
  ["data":"WithPromotedProps":private]=>
  string(0) ""
}
*/

Normal construction does properly defines the property with the default value.

However, when using the Reflection API, the default value isn't provided:

$ref = new ReflectionClass(WithPromotedProps::class);

$instance = $ref->newInstanceWithoutConstructor();

var_dump($instance);

This will result in this inconsistent object state:

object(WithPromotedProps)#3 (0) {
  ["data":"WithPromotedProps":private]=>
  uninitialized(string)
}

Here, since the promoted property has a default value, we should expect the same output as the previous example.

This does not happen if the property isn't promoted:

<?php

class WithoutPromotedProps {
    private string $data = '';
    public function __construct(string $data = '') {
        $this->data = $data;
    }
}

$ref = new ReflectionClass(WithoutPromotedProps::class);

$instance = $ref->newInstanceWithoutConstructor();

// Value is initialized with empty string
var_dump($instance);

/* Output: 
object(WithoutPromotedProps)#1 (1) {
  ["data":"WithoutPromotedProps":private]=>
  string(0) ""
}
*/

Note: the exact same happens with readonly properties, the code compiles even though readonly properties should not have a default value, but creating the object with the Reflection API without its constructor will still result in this uninitialized state.

PHP Version

Starts at 8.2.0 upwards

Operating System

No response

@iluuu1994
Copy link
Member

Hi @Pierstoval. This is essentially a duplicate of #16546. It does not qualify as a bug, given that it was explicitly proposed this way:

https://wiki.php.net/rfc/constructor_promotion#desugaring

Notably, the property is declared without a default value (i.e. it starts out in an uninitialized state), and the default value is only specified on the constructor parameter. While repeating the default value on the property declaration would currently appear harmless, there are forward-compatibility reasons why it is preferable to only specify the default once.

@Pierstoval
Copy link
Author

Pierstoval commented Mar 19, 2025

Should I instead suggest a new RFC "fixing" this behavior for DX? I mean, the only solutions to apply default values would be hacks (like using the Reflection API to enforce using the default value promoted in the constructor, or, as I did on a project, using a bounded closure after construction time to enforce an initialized state on props), or completely removing the properties having a default value.

At first it sounds counter-intuitive since we're talking about default class properties values, so we should expect default values to be applied both in the constructor and in the property itself.

@iluuu1994
Copy link
Member

@Pierstoval Please check the RFC and the rationale for why it wasn't done this way in the first place.

  • Not all constant expressions that are supported for argument default values are supported for property default values. Most notably new isn't allowed for the latter. Blindly applying it in all cases would break existing code:
class C {
    public function __construct(
        public $prop = new \stdClass();
    ) {}
}

// desugars to

class C {
    public $prop = new \stdClass(); // This is not valid code
    ...
}
  • Even if the above were allowed, it would construct the stdClass twice. Once when creating the object, and once when calling the constructor. If the constructor of the class has side-effects, this is clearly unexpected.
  • readonly isn't compatible with property default values. Similar to the above, applying the default value there is incompatible. Even if this were allowed, we'd again have two assignments, with the second one failing due to the property already being initialized.

@Pierstoval
Copy link
Author

After your comment, and more discussion on the public Slack, turns out PHP isn't compatible with fixing this issue (at least "yet"), because constructor has lots of side-effects, is public (therefore can be called more than once), and the current implementation for readonly also prevents changing this behavior.

Closing, thanks for your comment @iluuu1994

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

3 participants