[php-src] Issue #20253: Constructor property promotion and hooks

From: Date: Tue, 21 Oct 2025 10:32:37 +0000
Subject: [php-src] Issue #20253: Constructor property promotion and hooks
Groups: php.bugs 
Request: Send a blank email to php-bugs+get-251096@lists.php.net to get a copy of this message
Issue: https://github.com/php/php-src/issues/20253 Author: DirkGerigk ### Description --- **Title:** Feature Request: Improve Constructor-Promoted Property Hooks Behavior and Lazy Initialization --- **Hello,** Over the past few days, I’ve been working extensively with PHP's property hooks—especially with *constructor-promoted* property hooks. Consider the following example from the PHP documentation: 🔗 https://www.php.net/manual/en/language.oop5.property-hooks.php ```php <?php declare(strict_types=1); class Example { public function __construct( public private(set) DateTimeInterface $created = new DateTimeImmutable() { set(string|DateTimeInterface $value) { if (is_string($value)) { $value = new DateTimeImmutable($value); } $this->created = $value; } }, ) { // Within the constructor, I can set a string $this->created = '2025-01-01'; } } var_export(new Example(new DateTimeImmutable())); ``` ### Problem In the current implementation, I am required to pass a DateTimeInterface into the constructor. My alternatives are: * **A)** Change the constructor parameter to nullable: ```php ?DateTimeInterface $created = null ``` But then I must always handle null later on—something I might want to avoid, especially since the get hook should always return a DateTimeInterface. * **B)** Set a default value directly: ```php DateTimeInterface $created = new DateTimeImmutable() ``` However, this risks creating unnecessary instances (one via the constructor, another via the set hook). Alternatively, calling it like new Example(new DateTimeImmutable($date)) leads to code duplication. ### Conclusion **Constructor-promoted property hooks should be handled differently from regular property hooks.** I propose the following changes: --- ### ✅ Proposed Behavior 1. **Use set(string|DateTimeInterface $value) to process constructor values** during engine decomposition (de-sugaring). Since constructors are called only once per instance, this hook should be triggered during construction. 2. **Allow the raw property value to hold any type accepted by the set hook**, not just the final type. This enables: * Lazy conversion (e.g., string → DateTimeImmutable in the get hook) * Simplified debugging via tools like get_mangled_object_vars() * Deferring heavy instantiations until the value is actually accessed --- ### 🔧 Feature Request Since the documentation already shows normalization inside set (like using strtoupper() or converting a string to DateTimeImmutable), it seems consistent and useful to also allow type casts like string → int, or similar. There should be **no practical difference** between: * Using a string to construct an object (new DateTimeImmutable($string)) * Or converting a primitive type to the expected type in the setter Both are valid forms of input processing. Additionally, deferring instantiation until the value is accessed (lazy loading) would improve performance and debugging flexibility. --- ### 🙋‍♂️ What I’d Like to Do I’d like to be able to write code like this: ```php <?php declare(strict_types=1); class Example { public function __construct( public private(set) ?DateTimeInterface $created = null { get { if (is_null($this->created)) { $this->created = 'now'; } if (is_string($this->created)) { $this->created = new DateTimeImmutable($this->created); } return $this->created; } set(null|string|DateTimeInterface $value) { if (is_string($value) && !is_proper_datetime_string($value)) { throw new InvalidArgumentException("Cannot use $value for DateTimeInterface constructor"); } // Store raw value (for introspection, etc.) $this->created = $value; } }, ) { $this->created = '2025-01-01'; } } ``` This would decompose to the following: ```php class Example { public private(set) DateTimeInterface $created { get { if (is_null($this->created)) { $this->created = 'now'; } if (is_string($this->created)) { $this->created = new DateTimeImmutable($this->created); } return $this->created; } set(null|string|DateTimeInterface $value) { if (is_string($value) && !is_proper_datetime_string($value)) { throw new InvalidArgumentException("Cannot use $value for DateTimeInterface constructor"); } $this->created = $value; } } public function __construct( ?DateTimeInterface $created = null ) { $this->created = $created; } } ``` --- ### 🧩 Suggestion: Extended Hook Syntax To support this behavior more formally, we could extend the syntax of property hooks as follows: Instead of: ```php get { ... } ``` Allow: ```php get: DateTimeInterface { ... } ``` **Benefits:** 1. The get type (e.g., DateTimeInterface) is used for the decomposed property type. 2. If no type is defined explicitly, default to the existing behavior. --- I would like to write more about this, but English is not my native language, so I'm not sure if I can clearly express all the thoughts I have on the topic. But what i have in mind are simple data holder objects with auto casting, like: ```php class DataStruct { public function __construct( public private(set) ?int $userId = null { get : int => (int)$this->userId; set(string|int $value) => $value; }, ...$args ) { //do some with the unkown args, maybe optional parameter } } var_export(new DataStruct(...['userId'=>'12','foo'=>'bar'])); ``` Or when we have for example an enum field in a table, we can easy convert it to a real PHP Enum. ```php var_export(new DataStruct(...['role'=>'admin'])); #Can and will be convert to an PHP Enum internally. ``` Let me know what you think! Thanks 🙏 ---

« previous php.bugs (#251096) next »