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.
<?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.
<?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.
<?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.
<?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.
<?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.
<?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.
<?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.
<?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.
<?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.
<?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.
<?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.
<?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.
<?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
- Promotion is only allowed in constructors, and not in abstract constructors or interface constructors.
- A promoted property cannot also be declared in the class body; this is a redeclaration error.
- Variadic parameters cannot be promoted.
- The
callabletype is not allowed, because properties cannot have this type. - A default value of a promoted parameter is used only for the parameter; the property itself has no default value.
- Since PHP 8.5, promoted properties can also be marked
final.
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
List all PHP tutorials.