Constructor Property Promotion
What Is Constructor Property Promotion? In simple terms: Many constructors do nothing except copy their parameters into properties with the same names. Constructor property promotion (PHP 8.0+) lets y
What Is Constructor Property Promotion?
In simple terms: Many constructors do nothing except copy their parameters into properties with the same names. Constructor property promotion (PHP 8.0+) lets you declare the property, the parameter, and the assignment in one place, by adding a visibility keyword in front of the constructor parameter.
Technically: A constructor parameter preceded by a visibility modifier (public, protected, private), optionally with readonly, is a promoted parameter. PHP automatically declares a property with the same name and type, and assigns the argument to it before the constructor body runs.
The Problem It Solves
Before PHP 8.0, a simple class repeated every name three times:
class Customer
{
private string $name;
private string $email;
private string $city;
public function __construct(string $name, string $email, string $city)
{
$this->name = $name;
$this->email = $email;
$this->city = $city;
}
}
With promotion:
class Customer
{
public function __construct(
private string $name,
private string $email,
private string $city,
) {}
}
Both classes are identical in behavior: the same private, typed properties, assigned from the same constructor arguments. The second version has a third of the code and no chance of a mismatch between declaration and assignment.
Syntax and Behavior
public function __construct(
visibility [readonly] Type $name [= default],
...
) {
// optional body: runs after promoted properties are assigned
}
Example
<?php
class Point
{
public function __construct(
public float $x = 0.0,
public float $y = 0.0,
) {}
public function distanceTo(Point $other): float
{
return sqrt(($other->x - $this->x) ** 2 + ($other->y - $this->y) ** 2);
}
}
$origin = new Point();
$target = new Point(3, 4);
var_dump($target);
echo $origin->distanceTo($target) . "\n";
Output
object(Point)#2 (2) {
["x"]=>
float(3)
["y"]=>
float(4)
}
5
How It Works
public float $x = 0.0in the parameter list declares a publicfloatproperty$xand a constructor parameter$xwith the default0.0.- When the object is created, PHP assigns each argument to its property before running the (here empty) constructor body.
- The integer arguments
3and4are converted to floats, exactly as for a declaredfloatproperty. var_dump()shows that real properties were created. (#2is the object's ID:$originwas object#1.)
Mixing Promoted and Regular Parameters
Not every parameter has to be promoted. Regular parameters are useful for values that are only needed to calculate properties. The constructor body can validate promoted values and set other properties:
<?php
class Product
{
private string $slug;
public function __construct(
public readonly string $sku,
private string $name,
private int $pricePaise,
string $category = 'general',
) {
if ($pricePaise <= 0) {
throw new InvalidArgumentException('Price must be positive.');
}
$this->slug = $category . '/' . strtolower(str_replace(' ', '-', $name));
}
public function slug(): string
{
return $this->slug;
}
public function price(): string
{
return '₹' . number_format($this->pricePaise / 100, 2);
}
}
$lamp = new Product('LAMP-01', 'Desk Lamp', 129900, 'home');
echo $lamp->sku . "\n";
echo $lamp->slug() . "\n";
echo $lamp->price() . "\n";
try {
new Product('PEN-01', 'Gel Pen', 0);
} catch (InvalidArgumentException $e) {
echo "Error: " . $e->getMessage() . "\n";
}
Output
LAMP-01
home/desk-lamp
₹1,299.00
Error: Price must be positive.
Points to Notice
$sku,$name, and$pricePaiseare promoted;$categoryis a plain parameter used only to build the slug and is not stored as a property.- The constructor body runs after promotion, so
$pricePaiseis already assigned when it is validated. The body uses the parameter variable$pricePaise(also available as$this->pricePaise). - If validation fails, the exception prevents the object from being created, even though the property had already been assigned internally.
public readonlypromotion creates a property that can be read publicly but never changed (see Readonly Properties).
Readonly Data Objects
Promotion is especially convenient for classes that simply carry data — data transfer objects (DTOs) and value objects:
<?php
final class CreateOrderRequest
{
public function __construct(
public readonly int $customerId,
public readonly array $items,
public readonly ?string $couponCode = null,
public readonly string $paymentMethod = 'upi',
) {}
}
$request = new CreateOrderRequest(
customerId: 7,
items: [['sku' => 'PEN-01', 'qty' => 3]],
paymentMethod: 'card',
);
echo "Customer {$request->customerId} pays by {$request->paymentMethod}";
echo $request->couponCode === null ? ", no coupon\n" : ", coupon {$request->couponCode}\n";
Output
Customer 7 pays by card, no coupon
Combined with named arguments, the call reads almost like a configuration file, and each value is clearly labeled. Readonly properties guarantee that the request cannot be changed after it has been validated and created.
Rules and Restrictions
| Rule | Error If Violated |
|---|---|
| Promotion is only allowed in constructors | Compile error |
| Not allowed in abstract constructors or interfaces | Cannot declare promoted property in an abstract constructor |
| A promoted property must not also be declared separately | Cannot redeclare ClassName::$property |
| Variadic parameters cannot be promoted | Cannot declare variadic promoted property |
The type callable is not allowed for properties | Property ClassName::$property cannot have type callable (use Closure instead) |
| A nullable parameter type must be written explicitly | ?string $x = null, not string $x = null |
Defaults Belong to the Parameter
A default value like public float $x = 0.0 is a parameter default: it applies when the argument is omitted. The property itself has no default; it is assigned when the constructor runs. This matters only in rare situations (for example, when objects are created without calling the constructor, as some serialization libraries do).
Attributes and Doc Comments
Attributes placed on a promoted parameter apply to both the parameter and the property, which is how validation and serialization libraries read rules from them:
public function __construct(
#[Assert\Email]
public readonly string $email,
) {}
Formatting Conventions
For readability, the PER Coding Style recommends:
- one promoted parameter per line,
- a trailing comma after the last parameter,
- an empty body written as
{}on the same line as the closing parenthesis.
public function __construct(
private Mailer $mailer,
private Logger $logger,
private int $retryLimit = 3,
) {}
When to Use Promotion
| Use Promotion | Use Regular Properties |
|---|---|
| Values passed in and stored unchanged | Properties computed from several inputs |
| Dependencies (services) | Properties with complex initialization |
| DTOs and value objects | Properties not set through the constructor at all |
| Simple validation in the body | Classes where explicit declarations aid documentation |
Mixing both styles in one class is normal: promote what comes in directly, declare the rest normally.
Common Mistakes
Forgetting the Visibility Keyword
Effect: __construct(string $name) is a plain parameter; no property is created, and $this->name is undefined (or creates a deprecated dynamic property).
Fix: Add private, protected, or public.
Declaring the Property Twice
Effect: Compile error "Cannot redeclare".
Fix: Either promote or declare, not both.
Assigning in the Body Again
Effect: $this->name = $name; repeats what promotion already did.
Fix: Remove the redundant assignment, or assign a normalized value intentionally (for example, $this->name = trim($name); — but then consider not promoting that parameter).
Promoting Everything Publicly
Effect: Data can be changed from outside without validation.
Fix: Prefer private or public readonly.
Best Practices
- Use promotion for constructor parameters that are stored as-is.
- Prefer
privateorpublic readonlyvisibility for promoted properties. - Keep validation in the constructor body; it runs after promotion.
- Put one parameter per line with a trailing comma.
- Combine promotion with named arguments to create readable value objects and DTOs.
Official reference: https://www.php.net/manual/en/language.oop5.decon.php#language.oop5.decon.constructor.promotion