ZetCode

PHP Constructor Property Promotions

last modified October 22, 2026

In this article, we explore PHP's constructor property promotion, introduced in PHP 8.0. This feature simplifies the declaration and initialization of class properties in constructors.

Constructor property promotion lets you declare and initialize class properties directly in the constructor parameters. A parameter becomes a promoted property when it has a visibility modifier (public, protected or private) or the readonly modifier. This removes a lot of boilerplate code.

Basic syntax

The following example compares a traditional class with an equivalent class that uses promotion.

before_after.php
<?php

declare(strict_types=1);

// without promotion
class CustomerOld
{
    private string $fullName;
    private int $customerId;

    public function __construct(string $fullName, int $customerId)
    {
        $this->fullName = $fullName;
        $this->customerId = $customerId;
    }
}

// with promotion
class Customer
{
    public function __construct(
        private string $fullName,
        private int $customerId,
    ) {}
}

$c1 = new CustomerOld("John Doe", 1);
$c2 = new Customer("John Doe", 1);

print_r($c1);
print_r($c2);

In CustomerOld, each property is named four times: in the property declaration, in the parameter list, and twice in the assignment. In Customer, the private modifier on the parameters declares the properties and assigns them automatically. The trailing comma after the last parameter is allowed since PHP 8.0 and makes adding parameters easier. The print_r output shows that both objects have the same structure.

$ php before_after.php
CustomerOld Object
(
    [fullName:CustomerOld:private] => John Doe
    [customerId:CustomerOld:private] => 1
)
Customer Object
(
    [fullName:Customer:private] => John Doe
    [customerId:Customer:private] => 1
)

Basic usage

This example models a product in an online store with promoted properties.

product.php
<?php

declare(strict_types=1);

class Product
{
    public function __construct(
        private string $name,
        private float $price,
    ) {}

    public function getDetails(): string
    {
        return sprintf("Product: %s, Price: %.2f", $this->name, $this->price);
    }
}

$product = new Product("Laptop", 999.99);
echo $product->getDetails(), "\n";

The Product class uses constructor property promotion to define a product with a name and price. The properties are assigned when a new object is created, and the getDetails method reads them like any other properties.

$ php product.php
Product: Laptop, Price: 999.99

Mixed properties

Promoted and regular parameters can be combined in one constructor.

mixed.php
<?php

declare(strict_types=1);

class Order
{
    private DateTimeImmutable $orderDate;

    public function __construct(
        private int $orderId,
        private float $total,
        string $orderDate,
    ) {
        $this->orderDate = new DateTimeImmutable($orderDate);
    }

    public function getSummary(): string
    {
        return sprintf("Order ID: %d, Total: %.2f, Date: %s",
            $this->orderId, $this->total, $this->orderDate->format('M j, Y'));
    }
}

$order = new Order(1001, 49.99, "2026-10-22");
echo $order->getSummary(), "\n";

The $orderId and $total parameters are promoted. The $orderDate parameter is a plain parameter, because its value is converted to a DateTimeImmutable object before it is stored. Mixing the two styles is common when some values need processing.

$ php mixed.php
Order ID: 1001, Total: 49.99, Date: Oct 22, 2026

Default values

Promoted parameters can have default values.

defaults.php
<?php

declare(strict_types=1);

class User
{
    public function __construct(
        private string $username = "guest",
        private int $loginCount = 0,
    ) {}

    public function getProfile(): string
    {
        return "Username: $this->username, Logins: $this->loginCount";
    }
}

$guest = new User();
echo $guest->getProfile(), "\n";

$admin = new User("admin", 42);
echo $admin->getProfile(), "\n";

$user = new User(loginCount: 3);
echo $user->getProfile(), "\n";

If no arguments are provided, $username defaults to "guest" and $loginCount to 0. Named arguments let us skip the first parameter and set only the second one.

$ php defaults.php
Username: guest, Logins: 0
Username: admin, Logins: 42
Username: guest, Logins: 3

Readonly properties

The readonly modifier, available since PHP 8.1, works well with promotion. A readonly property can be initialized only once.

readonly_props.php
<?php

declare(strict_types=1);

class Invoice
{
    public function __construct(
        public readonly string $invoiceNumber,
        public readonly float $amount,
    ) {}
}

$invoice = new Invoice("INV-001", 150.75);
echo "Invoice: $invoice->invoiceNumber, Amount: $invoice->amount\n";

try {
    $invoice->amount = 99.0;
} catch (Error $e) {
    echo $e->getMessage(), "\n";
}

The Invoice properties are public, so they can be read directly, but any attempt to change them throws an Error. This is useful for records that must not change after creation.

$ php readonly_props.php
Invoice: INV-001, Amount: 150.75
Cannot modify readonly property Invoice::$amount

Readonly classes

Since PHP 8.2, a whole class can be declared readonly. All promoted properties then become readonly without repeating the modifier.

readonly_class.php
<?php

declare(strict_types=1);

final readonly class Money
{
    public function __construct(
        public int $amount,
        public string $currency,
    ) {}

    public function add(Money $other): static
    {
        return new static($this->amount + $other->amount, $this->currency);
    }
}

$a = new Money(1500, 'EUR');
$b = $a->add(new Money(250, 'EUR'));

echo "$a->amount $a->currency\n";
echo "$b->amount $b->currency\n";

Money is an immutable value object. The add method does not modify the current object; it returns a new one.

$ php readonly_class.php
1500 EUR
1750 EUR

Union types

Promoted properties can use any type that a property declaration allows, including union types.

union_types.php
<?php

declare(strict_types=1);

class Customer
{
    public function __construct(
        private string|int $customerId,
        private string $contactName,
    ) {}

    public function getContactInfo(): string
    {
        return "Customer ID: $this->customerId, Contact: $this->contactName";
    }
}

$customer1 = new Customer("CUST123", "Jane Smith");
$customer2 = new Customer(4021, "Paul Novak");

echo $customer1->getContactInfo(), "\n";
echo $customer2->getContactInfo(), "\n";

The string|int type lets $customerId accept either a numeric or a string ID, which is common when data come from several sources.

$ php union_types.php
Customer ID: CUST123, Contact: Jane Smith
Customer ID: 4021, Contact: Paul Novak

Nullable properties

This example models an employee with an optional department.

nullable.php
<?php

declare(strict_types=1);

class Employee
{
    public function __construct(
        private string $name,
        private ?string $department = null,
    ) {}

    public function getEmployeeInfo(): string
    {
        $dept = $this->department ?? "Not assigned";

        return "Employee: $this->name, Department: $dept";
    }
}

$employee1 = new Employee("Alice Brown");
$employee2 = new Employee("Tom Hill", "Sales");

echo $employee1->getEmployeeInfo(), "\n";
echo $employee2->getEmployeeInfo(), "\n";

The nullable promoted property $department defaults to null. The null coalescing operator provides a fallback text when no department is assigned.

$ php nullable.php
Employee: Alice Brown, Department: Not assigned
Employee: Tom Hill, Department: Sales

Static factory methods

PHP has no constructor overloading. Static factory methods are the usual way to offer alternative ways of creating an object.

factory.php
<?php

declare(strict_types=1);

class Subscription
{
    public function __construct(
        private string $plan,
        private float $monthlyFee,
    ) {}

    public static function freeTrial(): self
    {
        return new self("Trial", 0.0);
    }

    public function getPlanDetails(): string
    {
        return sprintf("Plan: %s, Fee: \$%.2f/month", $this->plan, $this->monthlyFee);
    }
}

$trial = Subscription::freeTrial();
$premium = new Subscription("Premium", 12.99);

echo $trial->getPlanDetails(), "\n";
echo $premium->getPlanDetails(), "\n";

The freeTrial method creates a subscription with predefined values, while the constructor creates custom plans. The dollar sign in the format string is escaped so that PHP does not treat it as the start of a variable.

$ php factory.php
Plan: Trial, Fee: $0.00/month
Plan: Premium, Fee: $12.99/month

Inheritance

A child class can promote its own parameters and pass the rest to the parent constructor.

inheritance.php
<?php

declare(strict_types=1);

class Member
{
    public function __construct(
        protected string $memberName,
        protected int $joinYear,
    ) {}
}

class PremiumMember extends Member
{
    public function __construct(
        string $memberName,
        int $joinYear,
        private string $membershipLevel,
    ) {
        parent::__construct($memberName, $joinYear);
    }

    public function getMembershipInfo(): string
    {
        return "Name: $this->memberName, Joined: $this->joinYear, Level: $this->membershipLevel";
    }
}

$premium = new PremiumMember("Bob Wilson", 2023, "Gold");
echo $premium->getMembershipInfo(), "\n";

The Member base class promotes $memberName and $joinYear as protected properties. PremiumMember receives them as plain parameters, forwards them to parent::__construct, and promotes only its own $membershipLevel. Promoting the inherited parameters again would redeclare the parent's properties.

$ php inheritance.php
Name: Bob Wilson, Joined: 2023, Level: Gold

Constructor with validation

A constructor that uses promotion can still have a body, for instance for validation.

validation.php
<?php

declare(strict_types=1);

class Booking
{
    public function __construct(
        private string $guestName,
        private int $roomNumber,
        private string $checkInDate,
    ) {
        if ($roomNumber < 1 || $roomNumber > 500) {
            throw new InvalidArgumentException("Room number must be between 1 and 500");
        }
    }

    public function getBookingInfo(): string
    {
        return "Guest: $this->guestName, Room: $this->roomNumber, Check-in: $this->checkInDate";
    }
}

$requests = [
    ["Emma Lee", 214, "2026-11-01"],
    ["Mark Ross", 501, "2026-11-02"],
];

foreach ($requests as [$name, $room, $date]) {
    try {
        $booking = new Booking($name, $room, $date);
        echo $booking->getBookingInfo(), "\n";
    } catch (InvalidArgumentException $e) {
        echo "Error: ", $e->getMessage(), "\n";
    }
}

Promoted properties are assigned before the constructor body runs. The body then checks the room number and throws an InvalidArgumentException if it is out of range, so no invalid object escapes the constructor.

$ php validation.php
Guest: Emma Lee, Room: 214, Check-in: 2026-11-01
Error: Room number must be between 1 and 500

Array properties

This example models a shopping cart with an array of items.

array_prop.php
<?php

declare(strict_types=1);

class ShoppingCart
{
    public function __construct(
        private string $cartId,
        private array $items = [],
    ) {}

    public function add(string $item): void
    {
        $this->items[] = $item;
    }

    public function getCartSummary(): string
    {
        return sprintf("Cart %s: %d items (%s)", $this->cartId,
            count($this->items), implode(', ', $this->items));
    }
}

$cart = new ShoppingCart("CART-XYZ", ["Shirt", "Pants"]);
$cart->add("Shoes");

echo $cart->getCartSummary(), "\n";

The $items property is a promoted array with an empty array as default value. The add method appends to it later.

$ php array_prop.php
Cart CART-XYZ: 3 items (Shirt, Pants, Shoes)

Promotion with property hooks

PHP 8.4 added property hooks and asymmetric visibility. Both can be used on promoted properties.

hooks.php
<?php

declare(strict_types=1);

class Temperature
{
    public function __construct(
        public private(set) float $celsius {
            set {
                if ($value < -273.15) {
                    throw new InvalidArgumentException("Below absolute zero");
                }
                $this->celsius = $value;
            }
        },
    ) {}
}

$t = new Temperature(21.5);
echo $t->celsius, "\n";

try {
    new Temperature(-300);
} catch (InvalidArgumentException $e) {
    echo $e->getMessage(), "\n";
}

try {
    $t->celsius = 30;
} catch (Error $e) {
    echo $e->getMessage(), "\n";
}

The set hook validates every assignment, including the one made by the constructor. The public private(set) visibility allows reading the property from outside but writing it only from inside the class.

$ php hooks.php
21.5
Below absolute zero
Cannot modify private(set) property Temperature::$celsius from global scope

Rules and restrictions

Source

PHP Constructor Promotion - Documentation

In this article, we explored PHP's constructor property promotion, a feature that simplifies property declaration and initialization, and its combination with readonly properties, readonly classes and property hooks.

Author

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all PHP tutorials.