Constructor Property Promotion: Less Code, Same Type Safety
AI generated
<?php
8.4
PHP 8.4 · Constructor Promotion · Readonly · DTOs
Constructor Property Promotion: Less Code, Same Type Safety
how visibility, type, and readonly merge into the constructor head

Constructor property promotion collapses the classic threefold repetition of property declaration, constructor parameter, and body assignment into a single line per field, without giving up type safety, visibility, or readonly guarantees, and makes value objects and DTOs in particular noticeably more compact and maintainable.

11 min read Promotion · readonly · DTOs · Value Objects PHP 8.0 · 8.1 · 8.2 · 8.3 · 8.4

1. The boilerplate problem of classic constructors

Before PHP 8.0, every simple property of a class had to be written three times: once as a property declaration with visibility and type, once as a constructor parameter with the same type, and once as an assignment $this->property = $property; in the constructor body. For a class with eight fields, that meant over twenty lines of pure repetition before any business logic even began. Constructor property promotion was built exactly for this problem.

This repetition was not only tedious to type, it was also its own source of bugs: if a property's type changes, it must be kept in sync in two places, at the declaration and at the parameter. A refactoring tool handles that reliably, but a manual edit occasionally forgets one of the two spots, which can lead to a silent discrepancy between the declared property type and the actually accepted parameter type.

Since PHP 8.0, constructor property promotion solves this problem by turning visibility, type, and parameter name directly in the constructor head into the property declaration. PHP generates the property declaration and assignment automatically from that single spot. The class behaves exactly as before from the outside, only the source code is considerably shorter, and the synchrony between type and property is structurally guaranteed, no longer a matter of discipline.

2. Syntax: declaring visibility directly in the constructor

The syntax of constructor property promotion simply adds a visibility modifier, public, protected, or private, before a constructor parameter. That modifier is the signal to PHP to automatically create a class property from this parameter and assign the passed value at instantiation time, without the developer having to write a single line in the constructor body.

Parameters without a visibility modifier remain ordinary, non-promoted parameters that only exist inside the constructor, unless they are assigned manually. That allows promoted and non-promoted parameters to be mixed in the same constructor, for instance when a parameter is only used for validation but should not itself be stored as a property.


<?php

declare(strict_types=1);

// Before promotion: property, parameter, and assignment repeated three times
final class ProductBefore
{
    private string $sku;
    private string $name;
    private float $price;

    public function __construct(string $sku, string $name, float $price)
    {
        $this->sku = $sku;
        $this->name = $name;
        $this->price = $price;
    }
}

// With constructor property promotion: one declaration per field
final class Product
{
    public function __construct(
        private string $sku,
        private string $name,
        private float $price,
    ) {
    }

    public function sku(): string
    {
        return $this->sku;
    }
}

3. Combining readonly properties with constructor property promotion

Since PHP 8.1, the readonly keyword can be combined directly with constructor property promotion: public readonly string $sku in the constructor head creates a property that can never be changed again after initialization in the constructor. Any further write attempt outside the declaring scope throws an Error. This combination is today the standard way to build immutable objects in PHP.

The advantage over separately declared readonly properties lies again in compactness: instead of maintaining readonly on the property declaration and the type on the parameter separately, both sit together on a single line. For a class whose entire purpose is carrying immutable data, such as a monetary amount or an email address as a value object, this combination of constructor property promotion and readonly is today almost always the right choice.


<?php

declare(strict_types=1);

// Immutable value object: promoted, readonly properties, validated once
final class Money
{
    public function __construct(
        public readonly int $amountInCents,
        public readonly string $currency,
    ) {
        if ($this->amountInCents < 0) {
            throw new InvalidArgumentException('Amount must not be negative.');
        }
    }

    public function add(self $other): self
    {
        if ($other->currency !== $this->currency) {
            throw new InvalidArgumentException('Currency mismatch.');
        }

        // Returns a new instance instead of mutating the current one
        return new self($this->amountInCents + $other->amountInCents, $this->currency);
    }
}

$price = new Money(1999, 'EUR');
$shipping = new Money(495, 'EUR');
$total = $price->add($shipping);

4. Promoting default values, nullable types, and union types

Promoted parameters support the same default values as ordinary constructor parameters. public readonly ?string $note = null shows how nullable types and a default value can be combined, without any additional code being needed in the body. The default value is maintained exactly once at this spot, not additionally repeated at a separate property declaration.

Union types also work unchanged with constructor property promotion: public readonly int|string $identifier promotes a parameter that accepts either an integer or a string. Important here: the default value of a promoted parameter must still be a constant expression, such as null, a literal, or a class constant, the same restriction that applies to ordinary parameter defaults in PHP.


<?php

declare(strict_types=1);

final class Comment
{
    public function __construct(
        public readonly string $body,
        // Nullable with a default: promoted, no body assignment needed
        public readonly ?string $note = null,
        // Union type promoted just like on a classic parameter
        public readonly int|string $authorId = 'anonymous',
        // Default backed by a class constant, a constant expression
        public readonly string $status = self::STATUS_PENDING,
    ) {
    }

    private const string STATUS_PENDING = 'pending';
}

$comment = new Comment(body: 'Great article!');

5. Promotion with interfaces and typed object types

Promoted parameters are not limited to scalar types. A parameter can be declared as an interface, as a concrete class, or even as a typed array element. public readonly LoggerInterface $logger promotes an injected dependency directly into a property, without any separate line being needed in the body, which makes constructor property promotion a natural tool especially for dependency injection.

This combination of promotion and interface typing decouples the class from a concrete implementation, exactly like with a classically declared constructor parameter. The only difference is the more compact notation: instead of maintaining the interface import, property declaration, parameter, and assignment across four separate lines, the complete information sits in a single parameter line in the constructor head.

6. Attaching attributes to promoted properties

Since a promoted property remains, syntactically, a regular property, PHP attributes can be placed directly before it exactly like on a classically declared property. #[Sensitive] public readonly string $iban shows how constructor property promotion and attributes combine without losing the compactness of promotion. The reflection API reads such attributes via ReflectionParameter::getAttributes() just as reliably as on a classic property.

This combination is especially common in validation and serialization systems that use attributes to bind rules directly to DTOs. A DTO with eight promoted, individually attributed properties still stays readable, because each line shows all relevant information about exactly one field at a glance: visibility, type, attribute, and, if applicable, a default value.


<?php

declare(strict_types=1);

// Attributes combine cleanly with promoted, readonly properties
final class CreateCustomerRequest
{
    public function __construct(
        #[NotBlank]
        public readonly string $name,

        #[NotBlank]
        #[Sensitive('email address')]
        public readonly string $email,

        #[Length(min: 8, max: 128)]
        public readonly string $password,

        public readonly ?string $referralCode = null,
    ) {
    }
}

7. What CANNOT be promoted

Constructor property promotion is bound by concrete limits. Static properties can never be promoted, since promotion is conceptually tied to a concrete instantiation, while static properties exist class-wide and independently of any instance. Attempting to write public static string $x in a constructor head results in a parse error.

Computed default values cannot be promoted either. An expression like public readonly DateTimeImmutable $createdAt = new DateTimeImmutable() is not allowed as a parameter default, since PHP requires constant expressions for defaults, not function calls or object instantiations evaluated at call time. Anyone who needs a computed default must make the parameter nullable and perform the computation in the constructor body, which undoes part of the compactness of constructor property promotion.

Properties with complex, multi-line initialization logic, such as validating several fields against each other, also still belong in the constructor body, not in the parameter line itself. Promotion replaces plain assignment, not business logic. If a property's initialization involves more than a simple direct assignment, the body remains the right place for it, regardless of whether the parameter itself is promoted.

Task Classic (lots of code) With constructor property promotion Benefit
Initialize 3 properties 3x declaration plus 3x assignment in the body (6 lines) 3 parameters in the constructor head (3 lines) Half the code, fewer error sources
Immutable value object Private property, assigned manually in the body public readonly promoted directly Immutability enforced by the compiler
Property type safety Property type maintained separately, can diverge from parameter type One type for parameter and property at once No divergence between types possible
DTO with 8 fields Roughly 24 lines (declaration, parameter, assignment) 8 lines in the constructor head Noticeably more readable, less maintenance
Attributes per property Extra lines before each property declaration Attributes directly before the promoted parameter More compact, same expressiveness

8. Promotion in value objects and immutable data structures

Value objects are the use case where constructor property promotion delivers its greatest benefit. A value object, by definition, has no mutable state after creation, every apparent change returns a new instance. That property matches exactly what promoted readonly properties already enforce, so syntax and concept fit together seamlessly.

For DTOs that carry data between layers, for example from an HTTP request to a domain object, the combination is equally common. A DTO with ten or more fields stays readable through constructor property promotion as a single, compact parameter list, while the same class without promotion could easily need thirty or forty lines of pure boilerplate before the first line of business logic even begins.

9. Migrating existing classes to promotion incrementally

Migrating existing classes to constructor property promotion is backward compatible and can happen class by class, without callers noticing anything. As long as visibility, type, and parameter order do not change, the class's public interface stays identical, only the internal implementation becomes more compact. For new classes, promotion is practically standard in current PHP code today.

Tools like PHP-CS-Fixer or Rector offer automated rules that mechanically rewrite classic constructors into promoted parameters, including correctly carrying over types and visibilities. For large, grown codebases, this automated path is considerably safer than a manual migration, because it eliminates the risk of accidentally changing a property's type or visibility during the conversion.


<?php

declare(strict_types=1);

// Before: classic constructor, candidate for automated migration
final class AddressBefore
{
    private string $street;
    private string $city;
    private string $postalCode;

    public function __construct(string $street, string $city, string $postalCode)
    {
        $this->street = $street;
        $this->city = $city;
        $this->postalCode = $postalCode;
    }
}

// After: Rector's constructor promotion rule produces exactly this shape
final class Address
{
    public function __construct(
        private readonly string $street,
        private readonly string $city,
        private readonly string $postalCode,
    ) {
    }
}

10. Summary

Constructor property promotion replaces the classic threefold repetition of property declaration, constructor parameter, and body assignment with a single line per field in the constructor head. Visibility modifiers, type declarations, readonly, default values, and even PHP attributes can be combined directly at this spot, without losing type safety or expressiveness.

The limits lie with static properties, which can never be promoted, and with computed default values, which still need to be initialized in the constructor body. For value objects, DTOs, and classes with injected dependencies, constructor property promotion is today the standard approach, especially combined with readonly for true immutability.

Constructor Property Promotion, the Essentials

Syntax

A visibility modifier directly before the constructor parameter automatically creates the property declaration and assignment.

Combining readonly

public readonly directly in the constructor head is the standard approach for immutable value objects since PHP 8.1.

Limits

No static properties, no computed default values. Complex initialization stays in the constructor body.

Migration

Backward compatible, possible class by class. Rector and PHP-CS-Fixer automate the conversion safely.

11. FAQ: Constructor Property Promotion

1Since which version does promotion exist?
Since PHP 8.0. Combination with readonly since PHP 8.1, today the standard approach for immutable objects.
2Mix promoted and non-promoted parameters?
Yes, freely combinable in the same constructor, for instance for parameters used only for validation.
3Promote a static property?
No, promotion is tied to instantiation, static properties exist independently of instances.
4Computed default value possible?
No, only constant expressions. Computation must happen in the constructor body, make the parameter nullable instead.
5Attributes on promoted properties?
Yes, directly before the promoted parameter, reliably readable via ReflectionParameter::getAttributes().
6Is promotion just syntactic sugar?
Yes, identical class structure at runtime as a classic constructor, no difference in performance or behavior.
7Union types on promoted parameters?
Yes, work exactly like on classically declared parameters, no additional restriction.
8Must all properties be promoted?
No, partial promotion is allowed, for instance when a property is computed from several parameters.
9Safely migrate large codebases?
With Rector or PHP-CS-Fixer automation, safer than manual migration with risk of typos.
10Does promotion fit value objects?
Very well, matches immutable state after creation exactly, combined with readonly today the standard.

Mironsoft

PHP architecture, code quality, and Magento development

Want to migrate existing classes to constructor property promotion?

We automate the migration of existing constructors to constructor property promotion with readonly, including Rector rules, code review, and full PHPStan coverage for your codebase.

Automated migration

Rector rules for class-wide conversion to promotion

Value object design

Designing immutable DTOs and value objects with readonly

Code review

Reviewing existing constructors for migration potential