rfc:constructor_promotion

PHP RFC: Constructor Property Promotion

Introduction

Currently, the definition of simple value objects requires a lot of boilerplate, because all properties need to be repeated at least four times. Consider the following simple class:

class Point {
    public float $x;
    public float $y;
    public float $z;
 
    public function __construct(
        float $x = 0.0,
        float $y = 0.0,
        float $z = 0.0
    ) {
        $this->x = $x;
        $this->y = $y;
        $this->z = $z;
    }
}

The properties are repeated 1) in the property declaration, 2) the constructor parameters, and 3) two times in the property assignment. Additionally, the property type is repeated twice.

Especially for value objects, which commonly do not contain anything more than property declarations and a constructor, this results in a lot of boilerplate, and makes changes more complicated and error prone.

This RFC proposes to introduce a short hand syntax, which allows combining the definition of properties and the constructor:

class Point {
    public function __construct(
        public float $x = 0.0,
        public float $y = 0.0,
        public float $z = 0.0
    ) {}
}

This short-hand code is strictly equivalent to the previous example, but more concise. The choice of syntax is adopted from our sister language Hack.

Proposal

When a method parameter is prefixed with one of the visibility keywords public, protected or private, it is considered to be “promoted”. For each promoted parameter, a property with the same name will be added, and a forwarding assignment to that property included in the body of the constructor, according to the detailed rules outlined in the following.

Constraints

Promoted parameters may only occur inside non-abstract constructors. As such, all of the following are illegal:

// Error: Not a constructor.
function test(private $x) {}
 
abstract class Test {
    // Error: Abstract constructor.
    abstract public function __construct(private $x) {}
}
 
interface Test {
    // Error: Abstract constructor.
    public function __construct(private $x) {}
}

While unusual, promoted parameters may occur inside trait constructors.

Properties declared through promoted parameters are subject to the same restrictions as normal property declarations. In particular, it is not possible to declare the same property twice:

class Test {
    public $prop;
 
    // Error: Redeclaration of property.
    public function __construct(public $prop) {}
}

It is also not possible to use the callable type, even though it would ordinarily be permitted for parameters:

class Test {
    // Error: Callable type not supported for properties.
    public function __construct(public callable $callback) {}
}

Similarly, because promoted parameters imply a property declaration, nullability must be explicitly declared, and is not inferred from a null default value:

class Test {
    // Error: Using null default on non-nullable property
    public function __construct(public Type $prop = null) {}
 
    // Correct: Make the type explicitly nullable instead
    public function __construct(public ?Type $prop = null) {}
}

Variadic parameters cannot be promoted:

class Test {
    // Error: Variadic parameter.
    public function __construct(public string ...$strings) {}
}

The reason is that in this case the type of the individual arguments (here: string), and the type of the variadic parameter into which they are collected (here: array of string) differ. While we could implicitly give the $strings property an array type for variadic parameters, this makes the transform less transparent.

Explicit property declarations and properties promoted from constructor arguments may be combined. A constructor may also have both promoted and non-promoted parameters.

// Legal.
class Test {
    public string $explicitProp;
 
    public function __construct(public int $promotedProp, int $normalArg) {
        $this->explicitProp = (string) $normalArg;
    }
}

Desugaring

Promoted properties follow a simple desugaring, where the following transformation is applied for all promoted parameters:

// From:
class Test {
    public function __construct(public Type $prop = DEFAULT) {}
}
 
// To:
class Test {
    public Type $prop;
 
    public function __construct(Type $prop = DEFAULT) {
        $this->prop = $prop;
    }
}

The visibility and type of the automatically declared property match that of the promoted parameter. 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.

The first is a possible future extension to allow arbitrary expression in parameter and property defaults:

// From:
class Test {
    public function __construct(public Dependency $prop = new Dependency()) {}
}
 
// To:
class Test {
    public Dependency $prop /* = new Dependency() */;
 
    public function __construct(Dependency $prop = new Dependency()) {
        $this->prop = $prop;
    }
}

In this case, if the default value were duplicated to the property declaration, we would end up constructing the optional Dependency object twice, which is undesirable and violates the single-evaluation rule.

Additionally, under the rules of the recent readonly property proposal the assignment in the constructor would not be legal if the property declared a default value.

If the promoted parameter is passed by reference, then the forwarding assignment is also performed by reference:

// From:
class Test {
    public function __construct(public array &$array) {}
}
 
// To:
class Test {
    public array $array;
 
    public function __construct(array &$array) {
        $this->array =& $array;
    }
}

The forwarding property assignments occur at the start of the constructor. As such, it is possible to access both the parameter and the property in the constructor, for example to enforce additional validation:

// This works.
class PositivePoint {
    public function __construct(public float $x, public float $y) {
        assert($x >= 0.0);
        assert($y >= 0.0);
    }
}
 
// This also works.
class PositivePoint {
    public function __construct(public float $x, public float $y) {
        assert($this->x >= 0.0);
        assert($this->y >= 0.0);
    }
}

Coding Style Considerations

This section gives non-normative coding style recommendations.

If constructor property promotion is used, it is recommended that the constructor be placed as the first method in the class, and directly following any explicit property declarations. This ensures that all declared properties are grouped together and visible at a glance. Coding standards that currently require static methods to be placed first should be adjusted to place the class constructor first.

If @param annotations on promoted properties are used, these annotations should also be treated as @var annotations by PHP documentation tooling:

// From:
class Point {
    /**
     * Create a 3D point.
     *
     * @param float $x The X coordinate.
     * @param float $y The Y coordinate.
     * @param float $z The Z coordinate.
     */
    public function __construct(
        public float $x = 0.0,
        public float $y = 0.0,
        public float $z = 0.0
    ) {}
}
 
// To:
class Point {
    /**
     * @var float $x The X coordinate.
     */
    public float $x;
 
    /**
     * @var float $y The Y coordinate.
     */
    public float $y;
 
    /**
     * @var float $z The Z coordinate.
     */
    public float $z;
 
    /**
     * Create a 3D point.
     *
     * @param float $x The X coordinate.
     * @param float $y The Y coordinate.
     * @param float $z The Z coordinate.
     */
    public function __construct(
        float $x = 0.0,
        float $y = 0.0,
        float $z = 0.0
    ) {
        $this->x = $x;
        $this->y = $y;
        $this->z = $z;
    }
}

Finally, it should be noted that constructor property promotion is just a convenient short-hand notation that covers the most common cases. A promoted property can always be converted into an explicit property with custom initialization logic at a later point in time. Such a change does not constitute a backwards-compatibility break.

Backward Incompatible Changes

None.

Future Scope

Larry provided some broader vision on how this feature can be combined with other features to improve our object initialization story in https://hive.blog/php/@crell/improving-php-s-object-ergonomics.

Because constructor signatures that include promoted properties are likely to become long enough to require line-breaks, it would be beneficial to allow a trailing comma in function parameter lists:

class Point {
    public function __construct(
        public float $x = 0.0,
        public float $y = 0.0,
        public float $z = 0.0, // <-- Allow this comma.
    ) {}
}

Optional trailing commas in this position were previously declined, but I believe we have a stronger case for them nowadays, especially in conjunction with this feature.

Prior Art

This feature, or something very similar, is already supported by a number of other languages.

There have also been three previous RFCs on related topics:

Vote

Yes/No.

rfc/constructor_promotion.txt · Last modified: 2020/03/26 17:49 by nikic