Created
February 14, 2026 13:43
-
-
Save MircoBabin/aaa574297c8d1baa879f19c99ce28e93 to your computer and use it in GitHub Desktop.
PHP RFC - The constructor must not return a value
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| ====== PHP RFC: The constructor must not return a value ====== | |
| * Version: 1.0 | |
| * Date: 2026-02-14 | |
| * Author: Mirco Babin, <mirco.babin@gmail.com> | |
| * Status: Draft | |
| * Implementation: [TODO] To be supplied | |
| ===== Prologue ===== | |
| The purpose of this RFC is: | |
| * Maximum warning | |
| * Minimal BC impact. | |
| * Minimal adjustments, only change the "new" functionality. | |
| * Acting from an untyped perspective. | |
| This RFC arose from a security-sensitive error made by the author. During an upgrade from Laravel 9 to Laravel 12, the author made the following error: | |
| <code php> | |
| // Laravel 9 | |
| class MyController extends Controller | |
| { | |
| public function __construct() | |
| { | |
| $this->middleware(function ($request, $next) { | |
| if (!Security::isLoggedIn()) { | |
| return redirect()->route('login'); | |
| } | |
| return $next($request); | |
| }); | |
| } | |
| } | |
| </code> | |
| The goal is to redirect guests to the login page, so that other functions within the controller aren't called. | |
| Laravel 12 removed constructor middleware. The author made the mistake of removing the %%$this->middleware()%% call during the upgrade. This resulted in the following: | |
| <code php> | |
| // Laravel 12 - the author's mistake, this is totally wrong! | |
| class MyController | |
| { | |
| public function __construct() | |
| { | |
| if (!Security::isLoggedIn()) { | |
| return redirect()->route('login'); | |
| } | |
| // Because the return value is pointless, no redirection | |
| // did find place. | |
| // The unauthorized user could actually call real controller | |
| // functions, which could be anything from showHomepage() to | |
| // rebootTheSystem() - exaggerated of course. | |
| // | |
| // But PHP did not warn, did not error, did not speak up, | |
| // did nothing to inform of the mistake. | |
| } | |
| public function showHomepage(Request $request) | |
| { | |
| } | |
| public function rebootTheSystem(Request $request) | |
| { | |
| } | |
| } | |
| </code> | |
| ===== Introduction ===== | |
| ==== __construct(): constructor and regular function ==== | |
| The %%__construct()%% function has two meanings: | |
| * a constructor called from the "new" keyword. | |
| * a regular function. | |
| This RFC explicitly addresses only the **constructor function** and leaves regular function calls untouched. | |
| <code php> | |
| class ConstructTwoMeanings | |
| { | |
| public function __construct() | |
| { | |
| } | |
| } | |
| // __construct() called as a constructor | |
| $it = new ConstructTwoMeanings(); | |
| // __construct() called as a regular function | |
| $important = $it->__construct(); | |
| </code> | |
| ==== __construct(): no return type ==== | |
| The %%__construct()%% function cannot have a return type declaration. This implicitly means the return type is **mixed**. | |
| This RFC explicitly does not change the return type to **void**. This is to minimize BC impact. | |
| <code php> | |
| class ConstructCanNotHaveReturnType | |
| { | |
| public function __construct() : mixed | |
| { | |
| } | |
| } | |
| // Fatal error: Method ConstructCanNotHaveReturnType::__construct() cannot declare a return type in ... | |
| </code> | |
| ==== "new" keyword ==== | |
| When an object is instantiated using the "new" keyword, the %%__construct()%% constructor is called subtly. The %%__construct()%% constructor can return a value. | |
| However, this value is silently lost in the "new" keyword, which is prone to errors. | |
| This RFC addresses the silent loss of return values. Starting in PHP 8.6, a deprecation message will be displayed. Starting in PHP 9, this will become a runtime error. | |
| <code php> | |
| class SomeTheoreticalExample | |
| { | |
| public $uniqueId; | |
| public function __construct(bool $returnSomeValue) | |
| { | |
| static $uniqueId = 0; | |
| $this->uniqueId = $uniqueId; | |
| $uniqueId++; | |
| if ($returnSomeValue) { | |
| // return some pointless value. Pointless, because it is | |
| // silently discarded by the "new" keyword. | |
| return 'abc'; | |
| // return 1; | |
| // return 1.23; | |
| // return null; | |
| // return true; | |
| // return false; | |
| // return ['a', 'b', 'c']; | |
| // return [6 => 'six', 7 => 'seven', | |
| // 67 => 'six seven']; | |
| // return ['a' => 'a', 'b' => 'b', | |
| // 'c' => 'c', 0 => 'Zero']; | |
| // return SomeEnum::Spades; | |
| // return function() {}; | |
| // return fn($a) => $a; | |
| // return new DateTimeImmutable(); | |
| // return fopen('php://memory', 'w+'); | |
| // return $this; | |
| // return &$this; | |
| // return new SomeTheoreticalExample(false); | |
| // Laravel 12 controller specific, of course this is very | |
| // very wrong. It will NEVER redirect, and the flow | |
| // continues to reach the (unauthorized) controller function. | |
| // return redirect()->route('login'); | |
| // This is a very terrible case. Try it out yourself and | |
| // watch the returned $newed->uniqueId. | |
| // Spoiler alert: it won't be 0. | |
| // yield 1; | |
| } | |
| } | |
| } | |
| // Before the RFC: nothing. | |
| // After the RFC: will issue a deprecation message and later will emit a | |
| // runtime error. | |
| $newed = new SomeTheoreticalExample(true); | |
| // Before the RFC: nothing. | |
| // After the RFC: nothing, because not called as a constructor. | |
| $someReasonToCall__ConstructAgain = $newed->__construct(true); | |
| </code> | |
| ===== Proposal ===== | |
| This RFC addresses the silent loss of constructor return values. Starting in PHP 8.6, a deprecation message will be displayed. Starting in PHP 9, this will become a runtime error. | |
| ==== Adjust the "new" keyword ==== | |
| High-level overview of the current "new" keyword: | |
| <code php> | |
| // High-level overview of the current "new" keyword. | |
| $newed = acquire_memory_and_initialize_object(); | |
| if (method_exists($newed, '__construct')) { | |
| $args = func_get_args(); | |
| $newed->__construct(...$args); | |
| // NOTICE that a return value from the __construct() constructor is | |
| // silently discarded. | |
| } | |
| return $newed; | |
| </code> | |
| This RFC changes the "new" keyword to: | |
| <code php> | |
| // High-level overview of the adjusted "new" keyword. | |
| $newed = acquire_memory_and_initialize_object(); | |
| if (method_exists($newed, '__construct')) { | |
| $args = func_get_args(); | |
| $__constructReturnValue = $newed->__construct(...$args); | |
| if ($__constructReturnValue !== void) { | |
| // PHP 8.6: Deprecated: Returning a value from the | |
| // __construct() constructor is deprecated. | |
| // PHP 9: throw new ConstructorError( | |
| // 'The __construct() constructor must not | |
| // return a value.'); | |
| }} | |
| return $newed; | |
| </code> | |
| ==== Adjust the ReflectionClass::newInstance() function ==== | |
| A class can be instantiated via newInstance(). Let Reflection behave as the "new" keyword. | |
| <code php> | |
| class SomeTheoreticalReflectionExample | |
| { | |
| public function __construct($value) | |
| { | |
| return $value; | |
| } | |
| } | |
| $class = new ReflectionClass(SomeTheoreticalReflectionExample::class); | |
| $it = $class->newInstance(['important']); | |
| // PHP 8.6 | |
| // Deprecated: Returning a value from the __construct() constructor is deprecated. in ... | |
| // PHP 9 | |
| // Fatal error: Uncaught ConstructorError: The __construct() constructor must not return a value. in ... | |
| </code> | |
| ==== Adjust the ReflectionClass::newInstanceArgs() function ==== | |
| A class can be instantiated via newInstanceArgs(). Let Reflection behave as the "new" keyword. | |
| <code php> | |
| class SomeTheoreticalReflectionArgsExample | |
| { | |
| public function __construct($value) | |
| { | |
| return $value; | |
| } | |
| } | |
| $class = new ReflectionClass(SomeTheoreticalReflectionArgsExample::class); | |
| $it = $class->newInstanceArgs([ ['important'] ]); | |
| // PHP 8.6 | |
| // Deprecated: Returning a value from the __construct() constructor is deprecated. in ... | |
| // PHP 9 | |
| // Fatal error: Uncaught ConstructorError: The __construct() constructor must not return a value. in ... | |
| </code> | |
| ==== Introduce ConstructorError ==== | |
| This RFC introduces the following exception hierarchy: | |
| <code php> | |
| class RuntimeError extends Error {}; | |
| class ConstructorError extends RuntimeError {}; | |
| </code> | |
| ===== Affected examples ===== | |
| ==== Affected yield/generator example ==== | |
| <code php> | |
| class SomeTheoreticalYieldExample | |
| { | |
| public $value; | |
| public function __construct() | |
| { | |
| $this->value = 'Some value'; | |
| yield 1; | |
| } | |
| } | |
| $it = new SomeTheoreticalYieldExample(); | |
| // PHP 8.6 | |
| // Deprecated: Returning a value from the __construct() constructor is deprecated. in ... | |
| // PHP 9 | |
| // Fatal error: Uncaught ConstructorError: The __construct() constructor must not return a value. in ... | |
| // PHP 8.5 (current) | |
| // This demonstrates that currently yield leaves the object in an invalid state! | |
| // Expected string(10) "Some value", actual value NULL. | |
| var_dump($it->value); | |
| // NULL | |
| </code> | |
| ==== Affected return a value example ==== | |
| <code php> | |
| class SomeTheoreticalReturnExample | |
| { | |
| public function __construct() | |
| { | |
| return ['important']; | |
| } | |
| } | |
| $it = new SomeTheoreticalReturnExample(); | |
| // PHP 8.6 | |
| // Deprecated: Returning a value from the __construct() constructor is deprecated. in ... | |
| // PHP 9 | |
| // Fatal error: Uncaught ConstructorError: The __construct() constructor must not return a value. in ... | |
| </code> | |
| ==== Affected return $this example ==== | |
| <code php> | |
| class SomeTheoreticalReturnExample | |
| { | |
| public function __construct() | |
| { | |
| return $this; | |
| } | |
| } | |
| $it = new SomeTheoreticalReturnExample(); | |
| // PHP 8.6 | |
| // Deprecated: Returning a value from the __construct() constructor is deprecated. in ... | |
| // PHP 9 | |
| // Fatal error: Uncaught ConstructorError: The __construct() constructor must not return a value. in ... | |
| </code> | |
| ==== Affected lazy proxy example ==== | |
| The lazy proxy is affected, because the factory function must "new" an instance: | |
| <code php> | |
| class SomeTheoreticalLazyProxyExample { | |
| public function __construct(public int $prop) { | |
| echo __METHOD__, "\n"; | |
| return ['important']; | |
| } | |
| } | |
| $reflector = new ReflectionClass(SomeTheoreticalLazyProxyExample::class); | |
| $object = $reflector->newLazyProxy(function (SomeTheoreticalLazyProxyExample $object) { | |
| echo 1; | |
| $realInstance = new SomeTheoreticalLazyProxyExample(1); | |
| return $realInstance; | |
| }); | |
| // Triggers initialization, and forwards the property fetch to the real instance | |
| var_dump($object->prop); | |
| // PHP 8.6 | |
| // Deprecated: Returning a value from the __construct() constructor is deprecated. in ... | |
| // PHP 9 | |
| // Fatal error: Uncaught ConstructorError: The __construct() constructor must not return a value. in ... | |
| </code> | |
| ===== Unaffected examples ===== | |
| ==== Unaffected no return example ==== | |
| <code php> | |
| class UnAffectedNoReturnExample | |
| { | |
| public function __construct() | |
| { | |
| } | |
| } | |
| $it = new UnAffectedNoReturnExample(); | |
| </code> | |
| ==== Unaffected return without a value example ==== | |
| <code php> | |
| class UnaffectedReturnWithoutValueExample | |
| { | |
| public function __construct() | |
| { | |
| return; | |
| } | |
| } | |
| $it = new UnaffectedReturnWithoutValueExample(); | |
| </code> | |
| ==== Unaffected calling the parent constructor example ==== | |
| <code php> | |
| class UnaffectedBaseClass | |
| { | |
| public function __construct() | |
| { | |
| return ['important']; | |
| } | |
| } | |
| class UnaffectedCallParentConstructor extends UnaffectedBaseClass | |
| { | |
| public function __construct() | |
| { | |
| $important = parent::__construct(); | |
| } | |
| } | |
| $it = new UnaffectedCallParentConstructor(); | |
| </code> | |
| ==== Unaffected using __construct() as a regular function example ==== | |
| <code php> | |
| class Unaffected__ConstructAsRegularFunctionExample | |
| { | |
| public function __construct() | |
| { | |
| } | |
| } | |
| $it = new Unaffected__ConstructAsRegularFunctionExample(); | |
| $important = $it->__construct(); | |
| </code> | |
| ==== Unaffected lazy ghost example ==== | |
| <code php> | |
| class UnaffectedLazyGhostExample { | |
| public function __construct(public int $prop) { | |
| echo __METHOD__, "\n"; | |
| return ['important']; | |
| } | |
| } | |
| $reflector = new ReflectionClass(UnaffectedLazyGhostExample::class); | |
| $object = $reflector->newLazyGhost(function (UnaffectedLazyGhostExample $object) { | |
| $important = $object->__construct(1); | |
| }); | |
| // Triggers initialization, and forwards the property fetch to the real instance | |
| var_dump($object->prop); | |
| </code> | |
| ==== Unaffected ReflectionClass::newInstanceWithoutConstructor() example ==== | |
| <code php> | |
| class SomeTheoreticalReflectionWithoutConstructorExample | |
| { | |
| public function __construct($value) | |
| { | |
| return $value; | |
| } | |
| } | |
| $class = new ReflectionClass(SomeTheoreticalReflectionWithoutConstructorExample::class); | |
| $it = $class->newInstanceWithoutConstructor(); | |
| $it->__construct(['important']); | |
| </code> | |
| ===== Backward Incompatible Changes ===== | |
| Returning a value from the %%__construct()%% constructor will break. This will only happen in a "new" keyword context. | |
| The various examples in this RFC demonstrate that a return value is meaningless. And the "Affected yield/generator example" even shows that it currently goes wrong. | |
| ===== Proposed PHP Version(s) ===== | |
| * Deprecation in PHP 8.6. | |
| * ConstructorError in PHP 9. | |
| ===== RFC Impact ===== | |
| ==== To the Ecosystem ==== | |
| None. | |
| * PHP Code Sniffer already has the Universal.CodeAnalysis.ConstructorDestructorReturn sniffer available to detect return values [1]. The ecosystem already recognizes its futility. | |
| ==== To Existing Extensions ==== | |
| None. | |
| ==== To SAPIs ==== | |
| None. | |
| ===== Open Issues ===== | |
| None. | |
| ===== Future Scope ===== | |
| None. | |
| ===== Voting Choices ===== | |
| [TODO] open vote after discussion. | |
| Primary Vote requiring a 2/3 majority to accept the RFC: | |
| <doodle title="Implement The constructor must not return a value as outlined in the RFC?" voteType="single" closed="true" closeon="2026-01-01T15:30:00Z"> | |
| * Yes | |
| * No | |
| * Abstain | |
| </doodle> | |
| ===== Patches and Tests ===== | |
| [TODO] To be supplied | |
| ===== 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 ===== | |
| * [1] [[https://github.com/PHPCSStandards/PHPCSExtra#universalcodeanalysisconstructordestructorreturn-wrench-books|PHP Code Sniffer - sniff]] | |
| * [[https://github.com/php/php-src/issues/21090|Original reported issue]] | |
| * [[https://wiki.php.net/rfc/make_ctor_ret_void|PHP RFC: Make constructors and destructors return void]] | |
| * [[https://news-web.php.net/php.internals/129980|[IDEA for RFC] discussion on Internals mailinglist]] | |
| ===== Rejected Features ===== | |
| * Aborting during compilation upon detecting return values. It's too complex; it's very difficult to distinguish between calling it as a constructor and calling it as a regular function. | |
| ===== Changelog ===== | |
| * 2026-[TODO]: Published on wiki |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment