====== 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 [[https://wiki.php.net/rfc/constructor_promotion|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. 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: 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: 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|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/or ''%%readonly%%'' is **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. 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|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. * **''%%readonly%%''** modifiers are supported, as are ''%%readonly%%'' classes (every promoted property becomes ''%%readonly%%''). * **Property hooks** are supported on promoted parameters (see [[#initialization_and_validation|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: 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 to ''%%parent::__construct($x)%%''. * ''%%extends Base()%%'' generates a call to ''%%parent::__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 ''%%set%%'' hooks. - 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: 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: 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: A class without a primary constructor continues to work exactly as today. ==== Where primary constructors are allowed ==== Primary constructors may be declared on: * ''%%class%%'', including ''%%abstract%%'', ''%%final%%'' and ''%%readonly%%'' classes. (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 ''%%new%%'' and have no constructor. * **Traits** — left to [[#future_scope|Future Scope]] to keep this proposal focused; use a promoted ''%%__construct()%%'' in a trait for now. ==== 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: A ''%%readonly%%'' class — every promoted property is ''%%readonly%%'': = $this->min && $n <= $this->max; } } ?> Inheritance with parent forwarding and a forward-only parameter: 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 Base%%'' without 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 ''%%init%%'' blocks 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%%'' / ''%%implements%%'' without 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: * Yes * No * Abstain ===== 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 the ''%%parent::__construct()%%'' call from the ''%%extends%%'' argument 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 ===== * [[https://wiki.php.net/rfc/constructor_promotion|PHP RFC: Constructor Property Promotion]] (PHP 8.0) * [[https://wiki.php.net/rfc/property-hooks|PHP RFC: Property Hooks]] (PHP 8.4) * [[https://kotlinlang.org/docs/classes.html#constructors|Kotlin: primary constructors]] * [[https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/instance-constructors#primary-constructors|C# 12: primary constructors]] ===== 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 ''%%init%%'' blocks 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 with ''%%extends Base(...)%%''. Classes that need a constructor body should use a conventional promoted constructor instead. ===== Changelog ===== * 2026-06-17 v0.1: Initial draft.