PHP RFC: Primary Constructors
- Version: 0.1
- Date: 2026-06-29
- Author: Robert Landers, rob@getswytch.com
- Status: Under Discussion
- Implementation: https://github.com/php/php-src/pull/XXXX TBD
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():
- A parameter prefixed with a visibility modifier (
public,protected,private) and/orreadonlyis promoted to a property of that visibility, exactly as in PHP 8.0 constructor property promotion. - A parameter with no modifier is an ordinary constructor parameter. It is not promoted to a property; it exists only for the duration of construction. Such parameters are primarily useful for forwarding values to a parent constructor.
<?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:
- Default values are supported:
class Point(public int $x, public int $y = 0). - Types, including nullable, union, intersection and DNF types, are supported.
readonlymodifiers are supported, as arereadonlyclasses (every promoted property becomesreadonly).- Property hooks are supported on promoted parameters (see Initialization and validation).
- Attributes may be applied to individual parameters and follow the same target rules as promoted parameters.
- A promoted parameter may not be
callable-typed and may not be variadic, identical to the existing promotion restriction. A bare (non-promoted) parameter has no such restriction.
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:
extends Base($x)generates a call toparent::__construct($x).extends Base()generates a call toparent::__construct()with no arguments.extends Base(no parentheses) generates no automatic parent call. This is the existing PHP behaviour, where a child constructor does not implicitly invoke the parent constructor.
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:
- Promoted parameters are assigned to their properties, invoking any
sethooks. - If the
extendsclause 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:
class, includingabstract,finalandreadonlyclasses. (On an abstract class the synthesized constructor is concrete, exactly as a promoted__construct()is today.)
They may not be declared on:
- Interfaces — interfaces cannot contain a constructor implementation.
- Enums — enums cannot be instantiated with
newand have no constructor. - Traits — left to Future Scope to keep this proposal focused; use a promoted
__construct()in a trait for now.
<?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
- Whether
extends Basewithout parentheses should remain a silent “no parent call” (as today) or whether a primary-constructor class extending a parent with a required constructor should emit a diagnostic. - Whether a future RFC should add a separate initialization mechanism, such as Kotlin-style
initblocks or another way to associate separately-declared hooked properties with promoted parameters. This RFC intentionally does not include such a mechanism.
Unaffected PHP Functionality
- Classes without a primary constructor behave exactly as before.
- Conventional
__construct()methods, including constructor property promotion, are unchanged and remain fully supported. extends/implementswithout an argument list behave exactly as before.
Future Scope
- Primary constructors on traits.
- Constructor-level attributes. Header syntax currently offers no way to attach an attribute to the synthesized constructor method (only to individual parameters); a class needing this must use a conventional
__construct(). - Body-declared properties initialized from primary-constructor parameters. A future RFC could explore syntax for declaring hooks in the class body while binding the property to a primary-constructor parameter, rather than requiring the hook to appear inline in the header. For example, a body property might explicitly declare that it is initialized from a constructor parameter. This RFC leaves such a mechanism out of scope to keep primary constructors a direct desugaring of constructor property promotion.
Voting Choices
Primary vote, requiring a 2/3 majority to accept:
Patches and Tests
Proof-of-concept implementation: https://github.com/php/php-src/pull/XXXX
The implementation requires:
- Parser changes to accept a parameter list after the class name and an argument list after
extends. - Compiler changes to synthesize
__construct()from the header parameter list and to emit theparent::__construct()call from theextendsargument list.
Implementation
After the RFC is implemented, this section should contain:
- the version(s) it was merged into
- a link to the git commit(s)
- a link to the PHP manual entry for the feature
References
- PHP RFC: Constructor Property Promotion (PHP 8.0)
- PHP RFC: Property Hooks (PHP 8.4)
Rejected Features
- Implicitly promoting bare parameters to public properties. Rejected because it makes forward-only parameters inexpressible and conflicts with parent forwarding; promotion is therefore explicit, matching existing constructor promotion.
- A statement body for the primary constructor. Rejected; per-property hooks cover the common validation case, and a class needing more can use a conventional
__construct(). This avoids introducing a second place to write constructor logic. - Kotlin-style
initblocks or secondary constructors. Rejected for this RFC to keep the feature a simple desugaring to one constructor. PHP has no constructor overloading, and adding another method-like initialization construct would substantially expand the proposal. - Allowing an explicit
__construct()alongside a primary constructor. Rejected because it would create two places for constructor logic and unclear ordering/forwarding rules, especially withextends Base(...). Classes that need a constructor body should use a conventional promoted constructor instead.
Changelog
- 2026-06-17 v0.1: Initial draft.