[PHP-DEV] Primary Constructors

Hello internals,

I was reminded of my records RFC today, and one of the features of the RFC was “short constructors” generally called “primary constructors” in C#/Kotlin.

They would look like this:

class Point(public int $x, int $id = 0) extends Base($id);

Which is just sugar for this:

class Point extends Base {
  public function __construct(public int $x, int $id = 0) {
    parent::__construct($id);
  }
}

or this:

class Point extends Base {
  public int $x;
  public function __construct(int $x, int $id = 0) {
    $this->x = $x;
    parent::__construct($id);
  }
}

A class with a primary constructor may not have a defined __construct function. Any special initialization must be done with hooks:

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

I’m sending this email to the list to gather additional feedback before pursuing a formal RFC proposal.

— Rob

On Wed, Jun 17, 2026, at 23:32, Rob Landers wrote:

Hello internals,

I was reminded of my records RFC today, and one of the features of the RFC was “short constructors” generally called “primary constructors” in C#/Kotlin.

They would look like this:

class Point(public int $x, int $id = 0) extends Base($id);

Which is just sugar for this:

class Point extends Base {
  public function __construct(public int $x, int $id = 0) {
    parent::__construct($id);
  }
}

or this:

class Point extends Base {
  public int $x;
  public function __construct(int $x, int $id = 0) {
    $this->x = $x;
    parent::__construct($id);
  }
}

A class with a primary constructor may not have a defined __construct function. Any special initialization must be done with hooks:

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

I’m sending this email to the list to gather additional feedback before pursuing a formal RFC proposal.

— Rob

I just realized that my examples did not demonstrate the classes with a body, but they may also have a body!

— Rob

On 18.06.26 04:32, Rob Landers wrote:

Hello internals,

I was reminded of my records RFC today, and one of the features of the RFC was “short constructors” generally called “primary constructors” in C#/Kotlin.

They would look like this:

class Point(public int $x, int $id = 0) extends Base($id);

Which is just sugar for this:

class Point extends Base {
  public function __construct(public int $x, int $id = 0) {
    parent::__construct($id);
  }
}

or this:

class Point extends Base {
  public int $x;
  public function __construct(int $x, int $id = 0) {
    $this->x = $x;
    parent::__construct($id);
  }
}

A class with a primary constructor may not have a defined __construct function. Any special initialization must be done with hooks:

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

I’m sending this email to the list to gather additional feedback before pursuing a formal RFC proposal.

— Rob

Hey Rob,

I was excited about the Records RFC when I read it first. The “inline constructors” part was one of the things I loved the most about it.
Glad to see you are picking it up as a stand-alone concept here!

Having the constructor up there makes a lot of sense to me.
Especially now with property hooks, that happen to move the constructor down in the file a lot – because people are used to define properties before the constructor.
Personally, I would love to see class construction to happen at the very top – as proposed here.

One thing I found weird about the “inline constructors” in the Records RFC was that it allowed primary (inline) and traditional constructors. Good to see that it is different in this proposal.

However, I have one concern…

Making this property hooks only has the downside that the classes cannot be readonly.
Which makes it unusable in many situations where it would be neat to use primary constructors.

I already back then wanted to propose the following, to avoid having two constructor types, but I think it also makes sense here because of the readonly-issue:

readonly class Point(public int $x, int $id = 0) => { // still allows hooks without readonly
// normal constructor body behaviour
} extends Base($id) {
// class body
}

Same as in your proposal it’s just sugar. I’d expect it to behave the exact same as a normal __construct body, and the => {} to be optional.
As mentioned above, the sole benefit for me would be to have class construction at the very top, co-located with the class definition itself.
You open a class → you know what it consumes, and how it is constructed. Would be awesome!

Would you be open to something like this?

Cheers
Nick

On 22 June 2026 08:27:46 BST, Nick Sdot <php@nicksdot.dev> wrote:

readonly class Point(public int $x, int $id = 0) => { // still allows hooks without readonly
   // normal constructor body behaviour
} extends Base($id) {
  // class body
}

Same as in your proposal it's just sugar. I'd expect it to behave the exact same as a normal `__construct` body, and the ` => {}` to be optional.
As mentioned above, the sole benefit for me would be to have class construction at the very top, co-located with the class definition itself.
You open a class -> you know what it consumes, and how it is constructed. Would be awesome!

I don't think we should add extra syntax to the language just to change people's habits. If you want a constructor body as the first thing in the class, you can do that right now.

In your proposed syntax, it puts an arbitrary amount of code between the class name and the "extends" and "implements" clauses, which seems to go against the aim.

We could just let the "extends" clause include parameters from a separate constructor:

readonly class Point extends Base($id) {
    public function __construct(public int $x, int $id = 0) {
        // normal constructor body behaviour
    }
    // class body
}

That does raise a question of sequencing though: it implies the parent constructor is run automatically, but is that *before* or *after* the child constructor?

On the whole, just making the new syntax "all or nothing" does seem a safer option.

Regards,

Rowan Tommins
[IMSoP]

On 22.06.26 17:38, Rowan Tommins [IMSoP] wrote:

I don't think we should add extra syntax to the language just to change people's habits. If you want a constructor body as the first thing in the class, you can do that right now.

In your proposed syntax, it puts an arbitrary amount of code between the class name and the "extends" and "implements" clauses, which seems to go against the aim.

We could just let the "extends" clause include parameters from a separate constructor:

readonly class Point extends Base($id) {
     public function __construct(public int $x, int $id = 0) {
         // normal constructor body behaviour
     }
     // class body
}

That does raise a question of sequencing though: it implies the parent constructor is run automatically, but is that *before* or *after* the child constructor?

On the whole, just making the new syntax "all or nothing" does seem a safer option.

Regards,

Rowan Tommins
[IMSoP]

Hey Rowan,

In your proposed syntax, it puts an arbitrary amount of code between the class name and the "extends" and "implements" clauses, which seems to go against the aim.

I do not have any strong feelings on how it should look like. Let's say it would be:

readonly class Point(public int $x, int $id = 0) extends Base($id) implements Foo {
    // normal constructor body behaviour
} => {
   // class body
}

Also fine. Beautiful, in fact.

readonly class Point extends Base($id) {
     public function __construct(public int $x, int $id = 0) {
         // normal constructor body behaviour
     }
     // class body
}

That does raise a question of sequencing though: it implies the parent constructor is run automatically, but is that *before* or *after* the child constructor?

According to Robs proposal `__construct` is disallowed.

The before/after question is general and not exclusive to what I asked, I believe?
But yes, I also thought about it. There are multiple options, I have two for now:

A) `Base($id)` will not work in combination with `=> {}` (or ` {} =>`); parent controller would need to be called as in `__construct`
B) Control it via `Base($id)` and `Base($this->id)`

But I guess we would first need to hear from Rob how it is supposed to work without my addition.

On the whole, just making the new syntax "all or nothing" does seem a safer option.

But how to solve the `readonly` issue? I think if we have a chance for some new neat syntax -- and solve a problem with it, why not?

---

Cheers
Nick

On Wed, Jun 17, 2026, at 4:32 PM, Rob Landers wrote:

Hello internals,

I was reminded of my records RFC today, and one of the features of the
RFC was "short constructors" generally called "primary constructors" in
C#/Kotlin.

They would look like this:

class Point(public int $x, int $id = 0) extends Base($id);

Which is just sugar for this:

class Point extends Base {
  public function __construct(public int $x, int $id = 0) {
    parent::__construct($id);
  }
}

or this:

class Point extends Base {
  public int $x;
  public function __construct(int $x, int $id = 0) {
    $this->x = $x;
    parent::__construct($id);
  }
}

A class with a primary constructor *may not* have a defined
`__construct` function. Any special initialization must be done with
hooks:

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

I'm sending this email to the list to gather additional feedback before
pursuing a formal RFC proposal.

— Rob

I would support this, and I agree that KISS is the best way to go for now.

I don't know if it makes sense to include in the initial design, but for reference here's how Kotlin handles construction:

class Person(val name: String, val age: Int) { // Primary constructor, equivalent to promotion and what's proposed here.
  init {
    // Argument-less code block/method that runs after the primary constructor is assigned.
    // This could be useful to include for more complex cases, like where hooks are useful.
  }

  // Secondary constructor
  constructor(name: String): this(name, 0) {
    // This constructor gets called first, but is forced to call the primary constructor syntactically.
    // Then it can do other stuff if it wants.
  }
}

cf: Classes | Kotlin Documentation

I don't think we should include the "secondary constructor calls primary constructor and you have to make sure it works" logic. However, I do think we can/should consider an init block. Though how that interacts with inheritance I'm not sure yet.

Putting non-trivial property hooks inside a constructor promotion is already strongly discouraged, and I would recommend continuing to discourage them even with this syntax. However, that does raise a concern that we would really benefit from some way to allow property hooks to be defined "on their own" on a property, AND link the property to a promoted constructor argument. I'm not sure off hand how we would do that, and it's somewhat tangential to this syntax, but I am pointing it out for completeness.

As for the readonly/hooks incompatibility, honestly, I don't care. :slight_smile: Readonly is frequently problematic, Internals has already rejected making readonly and hooks compatible, and `protected(set)` gets you almost the same rules with none of the conflicts. So I'm good there.

--Larry Garfield

Hi

Am 2026-06-22 17:15, schrieb Larry Garfield:

I don't think we should include the "secondary constructor calls primary constructor and you have to make sure it works" logic.

PHP doesn't (and cannot easily) support method overloading, thus there cannot be such a thing as “primary and secondary” constructor. It is unclear to me what you are getting at here.

However, I do think we can/should consider an init block.

At the point where extra logic is required maybe just write a plain old regular __construct() method instead of inventing yet another “method-like” construct.

Best regards
Tim Düsterhus

On Mon, Jun 22, 2026, at 1:40 PM, Tim Düsterhus wrote:

Hi

Am 2026-06-22 17:15, schrieb Larry Garfield:

I don't think we should include the "secondary constructor calls
primary constructor and you have to make sure it works" logic.

PHP doesn't (and cannot easily) support method overloading, thus there
cannot be such a thing as “primary and secondary” constructor. It is
unclear to me what you are getting at here.

That we shouldn't do that part. :slight_smile: I was showing what Kotlin does, and calling out the parts we should and should not consider stealing.

However, I do think we can/should consider an init block.

At the point where extra logic is required maybe just write a plain old
regular __construct() method instead of inventing yet another
“method-like” construct.

Best regards
Tim Düsterhus

Allowing a __construct() method with no arguments when a "primary constructor" is in use would be fine with me, if that's the way we decide to go.

--Larry Garfield