objects namespaces and application architecture
Property Promotion
Constructor property promotion lets you declare a constructor parameter and a class property in one place. It removes boilerplate when a constructor mainly stores incoming values.
Without promotion, the same name appears several times:
<?php
declare(strict_types=1);
final class UserProfile
{
public string $name;
public string $email;
public function __construct(string $name, string $email)
{
$this->name = $name;
$this->email = $email;
}
}
With property promotion, visibility on the constructor parameter creates the property automatically.
<?php
declare(strict_types=1);
final class UserProfile
{
public function __construct(
public string $name,
public string $email,
) {
}
}
$profile = new UserProfile('Ada', 'ada@example.com');
echo $profile->name . ' <' . $profile->email . '>' . PHP_EOL;
// Prints:
// Ada <ada@example.com>
The promoted properties are real properties. They have visibility, types, and optional default values.
Visibility Still Matters
Promotion works with public, protected, and private.
<?php
declare(strict_types=1);
final class RegisterUser
{
public function __construct(
private Mailer $mailer,
) {
}
}
interface Mailer
{
public function send(string $email, string $message): void;
}
For dependencies, private is usually appropriate because callers should not reach into the service and use its collaborators directly.
For simple data transfer objects or value objects, public readonly properties may be reasonable.
Validation Still Goes In The Constructor
Promotion does not remove the need for validation.
<?php
declare(strict_types=1);
final class EmailAddress
{
public function __construct(
public readonly string $value,
) {
if (!filter_var($value, FILTER_VALIDATE_EMAIL)) {
throw new InvalidArgumentException('Email address is not valid.');
}
}
}
$email = new EmailAddress('ada@example.com');
echo $email->value . PHP_EOL;
// Prints:
// ada@example.com
The assignment to the promoted property happens before the constructor body runs, but the constructor body can still validate and throw if the value is not acceptable.
Defaults
Promoted properties can have default values, just like normal constructor parameters.
<?php
declare(strict_types=1);
final class SearchOptions
{
public function __construct(
public readonly int $page = 1,
public readonly int $perPage = 20,
) {
if ($page < 1) {
throw new InvalidArgumentException('Page must be at least 1.');
}
if ($perPage < 1 || $perPage > 100) {
throw new InvalidArgumentException('Per-page must be between 1 and 100.');
}
}
}
$options = new SearchOptions(perPage: 50);
echo $options->page . ', ' . $options->perPage . PHP_EOL;
// Prints:
// 1, 50
Defaults should represent real defaults, not hide missing required data.
When Not To Promote
Promotion is best when storing constructor values is the main job. It can become harder to read when the constructor has many parameters, complex transformation, or properties that need different names from the input.
This can be clearer without promotion:
<?php
declare(strict_types=1);
final class NormalisedEmail
{
public readonly string $value;
public function __construct(string $email)
{
$email = strtolower(trim($email));
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
throw new InvalidArgumentException('Email address is not valid.');
}
$this->value = $email;
}
}
The property is not just storing the raw constructor parameter, so explicit assignment makes the transformation obvious.
What You Should Be Able To Do
After this lesson, you should be able to use constructor property promotion, choose appropriate visibility, keep validation in the constructor, and recognise when explicit properties are clearer.
For junior work, this matters because modern PHP code uses promotion heavily in services, value objects, DTOs, commands, and event objects.
Practice
Practice: Use Property Promotion In A Value Object
Create a small value object using constructor property promotion.
Task
Build an EmailAddress class that:
- promotes a public readonly
string $value - validates the email address in the constructor body
- throws a clear exception for invalid email addresses
Use strict types. Keep the expected output in the PHP code block as printed lines or comments.
Check Your Work
Run cases for:
- a valid email address
- an invalid email address
Afterward, explain why promotion does not replace validation.
Show solution
This solution uses promotion to declare and assign the property, then validates the promoted value in the constructor body.
<?php
declare(strict_types=1);
final class EmailAddress
{
public function __construct(
public readonly string $value,
) {
if (!filter_var($value, FILTER_VALIDATE_EMAIL)) {
throw new InvalidArgumentException('Email address is not valid.');
}
}
}
$email = new EmailAddress('ada@example.com');
echo $email->value . PHP_EOL;
try {
new EmailAddress('not-an-email');
} catch (InvalidArgumentException $exception) {
echo $exception->getMessage() . PHP_EOL;
}
// Prints:
// ada@example.com
// Email address is not valid.
Promotion removes repeated property assignment code, but it does not decide whether a value is valid. The constructor still owns that rule.