Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save MircoBabin/aaa574297c8d1baa879f19c99ce28e93 to your computer and use it in GitHub Desktop.

Select an option

Save MircoBabin/aaa574297c8d1baa879f19c99ce28e93 to your computer and use it in GitHub Desktop.
PHP RFC - The constructor must not return a value
====== PHP RFC: The constructor must not return a value ======
* Version: 1.4
* Date: 2026-03-06
* Author: Mirco Babin, <mirco.babin@gmail.com>
* Status: Rejected by the author
* Implementation: None
===== RFC is rejected by the author =====
The proposed detection of a ''void'' return value in this RFC is not implementable in PHP 8.5.3.
**There is none ''is_void(%%$__constructReturnValue%%)'' function or construct. A comparison with ''void'' is not possible.**
<code php>
$__constructReturnValue = $newed->__construct(...$args);
if ($__constructReturnValue !== void) {
</code>
Because ''void'' is not a type like ''int'' or ''string'', it is a return-only type. And it is enforced by the compiler.
The compiler aborts when it detects a ''return expression'' construct.
<code php>
function TruelyVoid() : void {
}
function AbortedByCompiler() : void {
return TruelyVoid(); // Fatal error: A void function must not return a value in ...
}
</code>
In addition, the void RFC [5] describes that the return value of a ''void'' function is ''null''.
**No distinction can be made between ''void'' and ''null''.**
<code php>
function NoReturn() {
}
function ReturnWithoutValue() {
return;
}
function ReturnNull() {
return null;
}
echo "No-return : ";
var_dump(NoReturn());
echo "null === return-value : ";
var_dump(null === NoReturn());
echo "\n\n";
echo "Return without a value: ";
var_dump(ReturnWithoutValue());
echo "null === return-value : ";
var_dump(null === ReturnWithoutValue());
echo "\n\n";
echo "Return null : ";
var_dump(ReturnNull());
echo "null === return-value : ";
var_dump(null === ReturnNull());
echo "\n\n";
/*
OUTPUTS:
No-return : NULL
null === return-value : bool(true)
Return without a value: NULL
null === return-value : bool(true)
Return null : NULL
null === return-value : bool(true)
*/
</code>
===== Prologue =====
==== goals ====
The purpose of this RFC is to addresses the silent loss of return values from the constructor, with the following goals:
* Minimal BC impact.
* Maximum warning.
* Minimal adjustments, only change the "new" functionality.
* Acting from an untyped perspective.
The minimal BC impact and maximum warning goals are conflicting. This RFC prioritizes minimal BC impact above all else.
==== a security-senstive mistake ====
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 abstract parent constructor example ====
<code php>
abstract class SomeTheoreticalAbstractBaseClass
{
public function __construct()
{
return ['important'];
}
}
class SomeTheoreticalValidSubClass extends SomeTheoreticalAbstractBaseClass
{
}
$it = new SomeTheoreticalValidSubClass();
// 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) {
return ['important'];
}
}
$reflector = new ReflectionClass(SomeTheoreticalLazyProxyExample::class);
$object = $reflector->newLazyProxy(function (SomeTheoreticalLazyProxyExample $object) {
$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($input = null)
{
if ($input !== null) {
return ['important', $input];
}
}
}
class UnaffectedCallParentConstructor extends UnaffectedBaseClass
{
public function __construct()
{
$important = parent::__construct('important to the childclass');
}
}
$it = new UnaffectedCallParentConstructor();
$it2 = new UnaffectedBaseClass();
</code>
==== Unaffected constructor and regular function example ====
<code php>
class UnaffectedConstructorAndRegularFunction
{
public function __construct($input = null)
{
if ($input !== null) {
return ['important', $input];
}
}
}
$it = new UnaffectedConstructorAndRegularFunction();
$important = $it->__construct('more important');
</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) {
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 =====
None, never formally discussed on the internals mailinglist.
===== Patches and Tests =====
None.
===== 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]]
* [2] [[https://github.com/php/php-src/issues/21090|Original reported issue]]
* [3] [[https://wiki.php.net/rfc/make_ctor_ret_void|PHP RFC: Make constructors and destructors return void]]
* [4] [[https://news-web.php.net/php.internals/129980|[IDEA for RFC] discussion on Internals mailinglist]]
* [5] [[https://wiki.php.net/rfc/noreturn_type|PHP RFC: noreturn type (void)]]
===== Rejected Features for this RFC =====
==== Changing to "void" return type declaration ====
Changing the implicitly return type declaration from "mixed" to "void" is a rejected feature for this RFC.
* The goal of this RFC is minimal BC impact. The implicit ''void'' return-only type is enforced by the compiler. The compiler aborts if a return value is detected; typed and untyped sources behave the same. This would break most unaffected examples defined in this RFC. This would violate the minimal BC impact.
* As described in the "%%__construct()%%: constructor and regular function" section, this RFC explicitly addresses only the constructor functionality. It does not address %%__construct()%% as a regular function. The "void" option would also modify the regular function, which is outside the scope of this RFC. And it also falls outside the minimal impact on backwards compatibility.
* The rejected PHP RFC: Make constructors and destructors return void [3] from 2020 tried to introduce the "void" return type declaration and failed.
* However, if someone wants to follow the "void" path again, this RFC is not a block.
==== Ban __construct() as a regular function call ====
Banning the %%__construct()%% as regular function call, except when called as "parent::%%__construct()%%", is a rejected feature for this RFC.
* Only the "new" keyword and "parent::%%__construct()%%" would then be allowed to call the %%__construct()%% constructor.
* The %%__construct()%% would only serve as constructor. It would become truly a magic function.
* Lazy ghost and ReflectionClass::newInstanceWithoutConstructor() would be a challenge.
* This would go hand in hand with a implicit change to "void" return type declaration.
* The goal of this RFC is minimal BC impact. This would have major impact.
==== Aborting during compilation ====
Aborting during compilation upon detecting return values is a rejected feature for this RFC. It's too complex;
it's very difficult to distinguish between calling it as a constructor and calling it as a regular function.
==== Adjust __destruct() destructor ====
Adjust the %%__destruct()%% destructor function so that also no return values are silently lost is a rejected feature for this RFC.
* The details of the destructor are completely different; it is called if the zval reference count reaches 0 and is called by the garbage collector.
* Also the destructor does not have security-sensitive impact when returning values.
* Also, the use of destructors is very unusual in Php; the author never uses them.
* This requires a separate RFC.
===== Changelog =====
* 2026-02-14: first draft version 1.0
* 2026-02-17: version 1.1, changes:
* Cleanup php code of "Affected lazy proxy example".
* Cleanup php code of "Unaffected lazy ghost example".
* Renamed "Rejected Features" to "Rejected Feature for this RFC".
* Added rejected feature "Adjust the %%__destruct()%% destructor function".
* 2026-02-18: version 1.2, changes:
* Added "Affected abstract parent constructor example".
* Cleanup "Rejected features".
* Added rejected feature "Changing to "void" return type declaration".
* Added rejected feature "Ban %%__construct()%% as a regular function call".
* Adjusted the prologue to clarify minimal BC impact is prioritized above all else.
* 2026-02-20: version 1.3, changes:
* Added "Unaffected constructor and regular function example".
* Adjusted "Unaffected calling the parent constructor example".
* 2026-03-06: version 1.4, changes:
* Status changed to rejected by the author.
* Added section "RFC is rejected by the author".
* Adjusted the rejected Changing to "void" return type declaration.
@MircoBabin
Copy link
Author

This RFC is in DokuWiki format.

Us the following procedure for previewing:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment