skip to content

In PHP 8, what does constructor property promotion do, and which rules limit what a promoted parameter may declare?

level: middleimportance: must knowfreq 65%

answer

  1. a modifier on a constructor parameter
  2. declares the property and assigns it
  3. assigned before the constructor body runs
  4. default belongs to the parameter only
  5. no callable, variadic or abstract constructors

basics

~20 s

A constructor parameter with a modifier such as private declares a property of the same name and type and assigns the argument to it before the body runs. Promotion is not allowed for callable types, variadics or abstract constructors.

solid answer

~50 s

Since PHP 8.0, writing a modifier on a constructor parameter, `public function __construct(private string $iban, private int $balanceCents = 0) {}`, **promotes** it: PHP declares a property with that name, visibility and type, and assigns the argument to it **before** the constructor body runs, so the body can already read `$this->iban`. Any single modifier promotes (usually `public`, `protected` or `private`; `readonly` alone works too). Promoted and ordinary parameters can be mixed in any order. The limits: only in a constructor (`Cannot declare promoted property outside a constructor`), not in an abstract or interface constructor, not variadic, not typed `callable`, and not for a name the class already declares. A default value is copied to the **parameter only**, not to the property, and `public int $x = null` is a compile error rather than an implicitly nullable type. Attributes on a promoted parameter apply to both the property and the parameter.

code

php · 24 lines
php
<?php
declare(strict_types=1);

final class Account
{
    public function __construct(
        private string $iban,
        private int $balanceCents = 0,
        string $label = '',             // ordinary parameter, not a property
    ) {
        // promoted properties are already assigned here
        if (!preg_match('/^[A-Z]{2}\d{2}[A-Z0-9]{10,30}$/', $this->iban)) {
            throw new InvalidArgumentException('Invalid IBAN format');
        }
    }

    public function balanceCents(): int
    {
        return $this->balanceCents;
    }
}

$acc = new Account('GB82WEST12345698765432');
echo $acc->balanceCents(), PHP_EOL; // 0

go deeper

for a junior

Recall that a visibility keyword on a constructor parameter declares and assigns a property of the same name.

for a middle

Explain the assignment order, the parameter-only default, and the compile errors for callable, variadic, abstract and duplicate promotion.

for a senior

Spot uninitialized promoted properties when objects bypass the constructor, and decide when a derived or normalised value needs a separate declaration.

for a principal

Set a convention for constructor shape across the codebase, including when a long promoted signature signals a class that should be split.

## The boilerplate it removes Before PHP 8.0, a class that stores its constructor arguments repeated each name three times: in the property declaration, in the parameter list and in the assignment. ```php final class Account { private string $iban; private int $balanceCents; public function __construct(string $iban, int $balanceCents = 0) { $this->iban = $iban; $this->balanceCents = $balanceCents; } } ``` **Constructor property promotion** collapses that into the signature: `public function __construct(private string $iban, private int $balanceCents = 0) {}`. The generated class is equivalent: two typed private properties and a constructor that assigns them. ## How it works 1. A constructor parameter that carries a **modifier** is promoted. Visibility (`public`, `protected`, `private`) is the usual one; the manual notes that any other single modifier such as `readonly` has the same effect. 2. PHP declares a property with the parameter's **name** and **type**. 3. At the start of each constructor call, before the first statement of the body, the argument is assigned to the property. 4. The body runs afterwards, so it can validate or derive values from already-assigned promoted properties. Promoted and non-promoted parameters can be mixed in any order, and callers see no difference: the constructor's parameter list is the same either way. ## The rules and their errors | Rule | Compile error | |---|---| | Only in a constructor | `Cannot declare promoted property outside a constructor` | | Not in an abstract or interface constructor | `Cannot declare promoted property in an abstract constructor` | | No variadic promotion | `Cannot declare variadic promoted property` | | No `callable` type | `Property Account::$onChange cannot have type callable` | | No duplicate of a declared property | `Cannot redeclare Account::$iban` | | `static` is not a promotion modifier | `Cannot use the static modifier on a parameter` | The `callable` ban is not specific to promotion: properties can never be typed `callable`, and a promoted parameter is also a property. ## Defaults are the parameter's, not the property's A default value on a promoted parameter belongs to the **parameter**. The property itself gets no default: - a typed promoted property is *uninitialized* until the constructor runs; - an untyped promoted property starts as `null`. This matters whenever an object is created without its constructor running: a subclass constructor that never calls the parent constructor, `ReflectionClass::newInstanceWithoutConstructor()`, or deserialization. The property is then uninitialized and reading it throws. A second trap: `public int $x = null` is a **compile error** (`Cannot use null as default value for parameter $x of type int`). For ordinary parameters that spelling was an implicitly nullable type, deprecated since PHP 8.4; for promoted parameters it was never allowed. Write `public ?int $x = null`. ## Attributes and documentation An attribute written on a promoted parameter is **replicated** to both the property and the parameter, so validation or mapping attributes read by a library see it in both places. ## When to use it, and when not - **Use it** for value objects and services whose constructor mainly stores its arguments. It is the default style in modern PHP. - **Keep a body** for validation: promotion assigns first, the body checks and throws. - **Declare the property separately** when it needs a type or default that differs from the parameter, or when the stored value is derived from the argument (normalised, parsed or computed). - **Watch readability**: a constructor with a dozen promoted parameters is a signature that is hard to read and review, which is a design smell rather than a syntax problem. ## What tools see Promotion is resolved at compile time, so everything downstream sees ordinary members: - `ReflectionProperty::isPromoted()` returns `true` for the generated property, and `ReflectionParameter::isPromoted()` for the parameter. - `var_dump()` and `get_object_vars()` list promoted properties like declared ones. - Docblock descriptions for a promoted property go on the parameter, since there is no separate declaration to annotate. So migrating a class to promotion changes neither its public API nor what serializers and mappers see, as long as names, types and visibility stay the same. ## Summary Promotion is a compile-time shorthand: a modifier on a constructor parameter declares a same-named, same-typed property and assigns it before the body runs. It cannot be used outside a constructor, on variadics, on `callable` types or in abstract constructors, and its default value never becomes the property's default.

  • In what order do promotion and the constructor body run?
    All promoted properties are assigned first, at the start of the call, and then the body's statements run. So the body can validate `$this->iban` and throw; the property is already set at that point, but if the constructor throws, `new` produces no object for anyone to use.
  • Why is `public int $x = null` rejected on a promoted parameter?
    A promoted parameter is also a property, and the engine refuses `null` as the default of a non-nullable promoted type with `Cannot use null as default value for parameter $x of type int`. Ordinary parameters used to accept that spelling as an implicitly nullable type, which PHP 8.4 deprecated. Write `?int $x = null` in both cases.
  • A promoted `private int $balanceCents = 0` is read on an object created with `ReflectionClass::newInstanceWithoutConstructor()`. What happens?
    It throws an `Error` saying the typed property must not be accessed before initialization. The `= 0` belongs to the constructor parameter only, so without a constructor call the property has no value. Declare the property with its own default if it must be set even when the constructor is skipped.

saying these in an interview costs you the question

  • Only public, protected or private can trigger promotion; readonly alone cannot.
  • The default value of a promoted parameter becomes the property's default.
  • Promoted properties are assigned after the constructor body finishes.
  • A promoted parameter can be typed callable like any other parameter.
  • Promotion changes how callers must pass arguments to the constructor.