Table of Contents

PHP RFC: Primary Constructors

Introduction

Primary constructors let a class declare its constructor parameters directly in the class header, immediately after the class name. This generalizes PHP 8.0's constructor property promotion from the body of __construct() up into the class declaration itself, removing the last piece of boilerplate required to define small classes and value objects.

<?php
 
class Point(public int $x, public int $y = 0) {}
 
$p = new Point(3);
var_dump($p->x, $p->y); // int(3) int(0)
 
?>

Proposal

A class may declare a primary constructor by placing a parameter list in parentheses directly after the class name:

<?php
 
class Money(
    public readonly int $amount,
    public readonly string $currency = 'USD',
) {
    public function format(): string {
        return sprintf('%d %s', $this->amount, $this->currency);
    }
}
 
echo new Money(500)->format(); // 500 USD
 
?>

A primary constructor is exactly equivalent to writing a __construct() method with the same parameter list. The example above is identical to:

<?php
 
class Money {
    public function __construct(
        public readonly int $amount,
        public readonly string $currency = 'USD',
    ) {}
 
    public function format(): string {
        return sprintf('%d %s', $this->amount, $this->currency);
    }
}
 
?>

Everything that works in a promoted __construct() parameter list works identically in a primary constructor, because it is a constructor parameter list. The only additions are where the list lives (the class header) and an optional mechanism to forward arguments to a parent constructor (see Parent constructors).

Parameters and promotion

Each parameter in a primary constructor follows the same rules as a parameter of __construct():

<?php
 
class User(
    public string $name,        // promoted: public property $name
    private string $passwordHash, // promoted: private property $passwordHash
    int $id,                    // NOT promoted: ordinary parameter
) extends Entity($id) {}        // $id is forwarded to the parent constructor
 
?>

This matches the existing behaviour of constructor promotion precisely: a bare parameter is just a parameter, and promotion is opt-in via a modifier. Keeping promotion explicit is what makes forward-only parameters expressible, which in turn is what allows a value to be passed to a parent constructor without also becoming a property of the child (see Parent constructors).

All existing promotion rules and restrictions carry over unchanged:

Parent constructors

Because the constructor now lives in the class header, there is no method body in which to write parent::__construct(). Instead, arguments are forwarded to the parent constructor by supplying an argument list to the extends clause, in the same style as Kotlin, Scala and C# primary constructors:

<?php
 
class Animal(protected string $name) {
    public function describe(): string {
        return "I am {$this->name}";
    }
}
 
class Dog(
    public string $breed,
    string $name,             // forward-only parameter
) extends Animal($name) {      // calls parent::__construct($name)
    public function describe(): string {
        return parent::describe() . ", a {$this->breed}";
    }
}
 
echo (new Dog('Labrador', 'Rex'))->describe();
// I am Rex, a Labrador
 
?>

The argument list after extends ParentClass is evaluated in the scope of the constructor (the primary constructor's parameters and $this are available) and the result is passed to the parent constructor.

The presence of an argument list is significant:

An argument list on extends is only permitted when the class declares a primary constructor. Writing class Foo extends Bar($x) {} without a primary constructor is a compile-time error.

Order of operations

When a class with a primary constructor is instantiated, the synthesized constructor performs the following steps, in order:

  1. Promoted parameters are assigned to their properties, invoking any set hooks.
  2. If the extends clause has an argument list, the parent constructor is invoked with the forwarded arguments.

This ordering makes a primary constructor a transparent desugaring of a hand-written promoted constructor whose only body statement is the parent::__construct() call, and is consistent with how every existing promoted __construct() already behaves (promoted assignments happen before body statements).

Initialization and validation

A primary constructor has no statement body. Per-property validation and normalization are expressed using property hooks on promoted parameters; a set hook runs when the promoted value is assigned during construction:

<?php
 
class Temperature(
    public float $celsius {
        set {
            if ($value < -273.15) {
                throw new ValueError('below absolute zero');
            }
            $this->celsius = $value;
        }
    }
) {}
 
new Temperature(20.0);   // ok
new Temperature(-300.0); // ValueError: below absolute zero
 
?>

Validation that spans multiple parameters (for example, “$start must precede $end”) or initialization of derived state computed from several parameters is possible only when it can be expressed in terms of the primary constructor's assignment order. Promoted parameters are assigned left-to-right, so a later property's set hook can read earlier promoted properties and may initialize derived state:

<?php
 
class Range(
    public int $start,
    public int $end {
        set {
            if ($this->start > $value) {
                throw new ValueError('start must precede end');
            }
            $this->end = $value;
            $this->length = $value - $this->start;
        }
    }
) {
    public readonly int $length;
}
 
?>

The inverse is not true: an earlier hook cannot read a later promoted property before it has been initialized. If validation or initialization depends on values that are not yet initialized, cannot be naturally ordered, or would be clearer as ordinary statements, the class should use a conventional __construct() method instead.

This is especially relevant for readonly classes: primary constructors work for straightforward readonly value objects, but PHP does not allow hooked properties in readonly classes. A class can still be made semantically readonly while using hooks by omitting the readonly class modifier and using restricted write visibility (for example public private(set) or protected(set), as appropriate) on its promoted properties. A class that specifically needs the readonly modifier together with non-trivial validation or normalization should use a conventional constructor, where it can validate first and then assign readonly properties.

Relationship to __construct()

A class that declares a primary constructor may not also declare a __construct() method. The primary constructor is the constructor, and PHP has no constructor overloading, so a second declaration is unambiguously a conflict:

<?php
 
class Bad(public int $x) {
    public function __construct(public int $x) {} // Fatal error
}
// Fatal error: Cannot redeclare Bad::__construct()
 
?>

A class without a primary constructor continues to work exactly as today.

Where primary constructors are allowed

Primary constructors may be declared on:

They may not be declared on:

<?php
 
interface I(public int $x) {} // Parse error: syntax error, unexpected token "(", expecting "{"
enum E(public int $x) {}       // Parse error: syntax error, unexpected token "(", expecting "{"
 
?>

Inheritance and reflection

A subclass that does not declare its own constructor inherits the synthesized constructor, exactly as it would inherit a normal __construct(). A subclass may override it with its own primary constructor or a conventional __construct().

No new reflection API is required. ReflectionClass::getConstructor() returns the synthesized constructor, and ReflectionParameter::isPromoted() reports true for promoted parameters and false for bare ones, consistent with promoted __construct() parameters today.

Examples

A small value object with named arguments:

<?php
 
class Coordinate(
    public readonly float $latitude,
    public readonly float $longitude,
) {}
 
$c = new Coordinate(longitude: 4.9, latitude: 52.4);
 
?>

A readonly class — every promoted property is readonly:

<?php
 
readonly class Range(public int $min, public int $max) {
    public function contains(int $n): bool {
        return $n >= $this->min && $n <= $this->max;
    }
}
 
?>

Inheritance with parent forwarding and a forward-only parameter:

<?php
 
abstract class Shape(protected string $name) {
    abstract public function area(): float;
    public function name(): string { return $this->name; }
}
 
final class Circle(
    public readonly float $radius,
    string $name = 'circle',
) extends Shape($name) {
    public function area(): float {
        return M_PI * $this->radius ** 2;
    }
}
 
echo (new Circle(2.0))->name(); // circle
 
?>

Backward Incompatible Changes

None. This RFC introduces no new keywords. The token sequences it gives meaning to — a parenthesized parameter list following a class name (class Name(), and an argument list following an extends clause (extends Base() — are currently parse errors, so no existing program can be affected.

Proposed PHP Version(s)

Next PHP 8.x.

RFC Impact

To the Ecosystem

IDEs and Language Servers will need to recognize the header parameter list as a constructor signature (for autocomplete, signature help, and “go to constructor”), and the extends Base(...) argument list as a parent-constructor call.

Static Analyzers (PHPStan, Psalm) will need to parse the new syntax. Because a primary constructor desugars directly to a promoted __construct() plus an optional parent::__construct() call, the analysis model is unchanged once parsed.

Auto-formatters and linters (PHP-CS-Fixer, etc.) will need formatting rules for header parameter lists.

To Existing Extensions

No impact at runtime. The synthesized constructor is an ordinary __construct() as far as the engine, opcache, and reflection are concerned.

To SAPIs

No impact.

Open Issues

Unaffected PHP Functionality

Future Scope

Voting Choices

Primary vote, requiring a 2/3 majority to accept:

Accept Primary Constructors as described in this RFC?
Real name Yes No Abstain
Final result: 0 0 0
This poll has been closed.

Patches and Tests

Proof-of-concept implementation: https://github.com/php/php-src/pull/XXXX

The implementation requires:

Implementation

After the RFC is implemented, this section should contain:

  1. the version(s) it was merged into
  2. a link to the git commit(s)
  3. a link to the PHP manual entry for the feature

References

Rejected Features

Changelog