PHP 8: Constructor property promotion

Personally, I use value objects and data transfer objects all the time in my projects. I even wrote a dedicated post on how to treat data in our code a while back.

Naturally, I'm very happy with the constructor property promotion RFC, it's passed and will be added in PHP 8. You see, this feature reduces a lot of boilerplate code when constructing simple objects such as VOs and DTOs.

In short: property promotion allows you to combine class fields, constructor definition and variable assignments all into one syntax, in the construct parameter list.

So instead of doing this:

class CustomerDTO
{
    public string $name;

    public string $email;

    public DateTimeImmutable $birth_date;

    public function __construct(
        string $name, 
        string $email, 
        DateTimeImmutable $birth_date
    ) {
        $this->name = $name;
        $this->email = $email;
        $this->birth_date = $birth_date;
    }
}

You would write this:

class CustomerDTO
{
    public function __construct(
        public string $name, 
        public string $email, 
        public DateTimeImmutable $birth_date,
    ) {}
}

Let's look at how it works!


# How it works

The basic idea is simple: ditch all the class properties and the variable assignments, and prefix the constructor parameters with public, protected or private. PHP will take that new syntax, and transform it to normal syntax under the hood, before actually executing the code.

So it goes from this:

class MyDTO
{
    public function __construct(
        public string $name = 'Brent',
    ) {}
}

To this:

class MyDTO
{
    public string $name;

    public function __construct(
        string $name = 'Brent'
    ) {
        $this->name = $name;
    }
}

And only executes it afterwards.

Note by the way that the default value is not set on the class property, but on the method argument in the constructor.

So let's look at what promoted properties can and can't do, there's quite a lot of little intricacies worth mentioning!


# Only in constructors

Promoted properties can only be used in constructors. That might seem obvious but I thought it was worth mentioning this, just to be clear.

Noticed a tpyo? You can submit a PR to fix it. If you want to stay up to date about what's happening on this blog, you can subscribe to my mailing list: send an email to brendt@stitcher.io, and I'll add you to the list.

# No duplicates allowed

You're not able to declare a class property and a promoted property with the same name. That's also rather logical, since the promoted property is simply transpiled to a class property at runtime.

class MyClass
{
    public string $a;

    public function __construct(
        public string $a,
    ) {}
}

# Untyped properties are allowed

You're allowed to promote untyped properties, though I'd argue that these days with modern PHP, you're better off typing everything.

class MyDTO
{
    public function __construct(
        public $untyped,
    ) {}
}

# Simple defaults

Promoted properties can have default values, but expressions like new … are not allowed.

public function __construct(
    public string $name = 'Brent',
    public DateTimeImmutable $date = new DateTimeImmutable(),
) {}

# Combining promoted- and normal properties

Not all constructor properties should be promoted, you can mix and match.

class MyClass
{
    public string $b;

    public function __construct(
        public string $a,
        string $b,
    ) {
        $this->b = $b;
    }
}

I'd say: be careful mixing the syntaxes, if it makes the code less clear, consider using a normal constructor instead.


# Access promoted properties from the constructor body

You're allowed to read the promoted properties in the constructor body. This can be useful if you want to do extra validation checks. You can use both the local variable and the instance variable, both work fine.

public function __construct(
    public int $a,
    public int $b,
) {
    assert($this->a >= 100);

    if ($b >= 0) {
        throw new InvalidArgumentException('…');
    }
}

# Doc comments on promoted properties

You can add doc comments on promoted properties, and they are still available via reflection.

class MyClass 
{
    public function __construct(
        /** @var string */
        public $a,
    ) {}
}
$property = new ReflectionProperty(MyClass::class, 'a');

$property->getDocComment(); // "/** @var string */"

# Attributes

Just like doc blocks, attributes are allowed on promoted properties. When transpiled, they will be present both on the constructor parameter, as well as the class property.

class MyClass
{
    public function __construct(
        #[MyAttribute]
        public $a,  
    ) {}
}

Will be transpiled to:

class MyClass 
{
    #[MyAttribute]
    public $a;
 
    public function __construct(
        #[MyAttribute]
        $a,
    ) {
        $this->a = $a;
    }
}

# Not allowed in abstract constructors

I didn't even know abstract constructors were a thing, but here goes! Promoted properties are not allowed in them.

abstract class A
{
    abstract public function __construct(
        public string $a,
    ) {}
}

# Allowed in traits

On the other hand, they are allowed in traits. This makes sense, since the transpiled syntax is also valid in traits.

trait MyTrait
{
    public function __construct(
        public string $a,
    ) {}
}

# var is not supported

Old, I mean, experienced PHP developers might have used var in a distant past to declare class variables. It's not allowed with constructor promotion. Only public, protected and private are valid keywords.

public function __construct(
    var string $a,
) {}

# Variadic parameters cannot be promoted

Since you can't convert to a type that's array of type, it's not possible to promote variadic parameters.

public function __construct(
    public string ...$a,
) {}

Still waiting for generics…


# Reflection for isPromoted

Both ReflectionProperty and ReflectionParameter have a new isPromoted method to check whether the class property or method parameter is promoted.


# Inheritance

Since PHP constructors don't need to follow the declaration of their parent constructor, there's little to be said: inheritance is allowed. If you need to pass properties from the child constructor to the parent constructor though, you'll need to manually pass them:

class A
{
    public function __construct(
        public $a,
    ) {}
}

class B extends A
{
    public function __construct(
        $a,
        public $b,    
    ) {
        parent::__construct($a);
    }
}

That's about it for property promotion! I for sure will use them, what about you? Let me know via Twitter or e-mail!