Skip to content

The "new" operator should emit TypeErrors on __construct() return values. #21090

@MircoBabin

Description

@MircoBabin

Edited because the original example code contained an error, as pointed out by @iluuu1994 .
2nd edit because the "true" testcase was not handled correctly. I guess "third time's a charm".

Description

The "new" operator and __construct() return values

  • Returning anything in the "new" context does not fail. Which is very strange. This is only in combination with the "new" operator. The "new" operator should emit TypeError for all return values.
  • return $this; also does not fail. But if any return value is discarded by "new", as shown with testcase 4, why allow any return value from the "new" context? return $this should also error.
  • When calling __construct() as a regular function a return value might have some purpose. But why not deprecate calling __construct() outside the new operator?

P.S. If it should be a TypeError or some other error is debatable. But at least it should error.

Reproduce

The following code:

https://3v4l.org/svfeO#v8.5.2

<?php

enum SpecialReturnValue
{
    case NoReturn;
    case ReturnWithoutValue;
    case ReturnDollarThis;
    case ReturnVoid;
    case ReturnNever;
}

class ShouldTypeError
{
    public $uniqueId;
    public function __construct($returnValue)
    {
        static $uniqueId = 0;
        $this->uniqueId = $uniqueId;
        $uniqueId++;
        
        if ($returnValue !== SpecialReturnValue::NoReturn) {
            if ($returnValue === SpecialReturnValue::ReturnWithoutValue) {
                return;
            } elseif ($returnValue === SpecialReturnValue::ReturnDollarThis) {
                return $this;
            } elseif ($returnValue === SpecialReturnValue::ReturnVoid) {
                return void;
            } elseif ($returnValue === SpecialReturnValue::ReturnNever) {
                return never;
            }
            
            return $returnValue;
        }
    }
}

const SOMECONST = 'Const hello';

enum SomeEnum 
{
    case Hearts;
    case Diamonds;
    case Clubs;
    case Spades;
}

function SomeFunction()
{
}

$SomeFunctionClosure = function()
{
};

$SomeFunctionFirstClassCallable = SomeFunction(...);

$someFunctionClosureFromCallabe = Closure::fromCallable('SomeFunction');

$arrowFunction = fn($a) => $a;

$testcases = [
    SpecialReturnValue::NoReturn,
    SpecialReturnValue::ReturnWithoutValue,
    SpecialReturnValue::ReturnDollarThis,
    new ShouldTypeError(SpecialReturnValue::NoReturn),

    SOMECONST,    
    'abc',
    1,
    1.23,
    null,
    true,
    false,
    
    ['a', 'b', 'c'],
    [6 => 'six', 7 => 'seven', 67 => 'six seven'],
    ['a' => 'a', 'b' => 'b', 'c' => 'c', 0 => 'Zero'],
    
    SpecialReturnValue::ReturnVoid,
    SpecialReturnValue::ReturnNever,

    SomeEnum::Spades,
    
    function() {},
    $SomeFunctionClosure,
    $SomeFunctionFirstClassCallable,
    $someFunctionClosureFromCallabe,
    $arrowFunction,
    
    new DateTimeImmutable(),
    fopen('php://memory', 'w+'),
];

foreach($testcases as $testcase) {
    echo "--------------[ TESTCASE ]--------------\n";
    var_dump($testcase);
    echo "\n";
    
    try {
        $didReturn = new ShouldTypeError($testcase);
        
        if ($testcase === SpecialReturnValue::NoReturn) {
            echo "Success: without a return statement is always valid.\n";
        } elseif ($testcase === SpecialReturnValue::ReturnWithoutValue) {
            echo "Success: a return statement without a value is always valid.\n";
        } elseif ($testcase === SpecialReturnValue::ReturnDollarThis) {
            echo "Dubious: return \$this is dubious.\n";
            echo "- it fullfills the return type, so it could be allowed.\n";
            echo "- but returning anything from a constructor is nonsense, because it is discarded by \"new\".\n";
            echo "  As shown by the fourth testcase new SomeTypeError(SpecialReturnValue::NoReturn).\n";
        } else {
            echo "Error: why is it not a return TypeError?\n";
        }
        
        if ($didReturn instanceof ShouldTypeError) {
            echo "Created uniqueId ".$didReturn->uniqueId."\n";
        } else {
            echo "Failed to new a ShouldTypeError.\n";
        }
    } catch (Throwable $ex) {
        echo "Success: throwable: ".$ex->getMessage()."\n";
    }
}

Resulted in this output (php 8.5.2):

--------------[ TESTCASE ]--------------
enum(SpecialReturnValue::NoReturn)

Success: without a return statement is always valid.
Created uniqueId 1
--------------[ TESTCASE ]--------------
enum(SpecialReturnValue::ReturnWithoutValue)

Success: a return statement without a value is always valid.
Created uniqueId 2
--------------[ TESTCASE ]--------------
enum(SpecialReturnValue::ReturnDollarThis)

Dubious: return $this is dubious.
- it fullfills the return type, so it could be allowed.
- but returning anything from a constructor is nonsense, because it is discarded by "new".
  As shown by the fourth testcase new SomeTypeError(SpecialReturnValue::NoReturn).
Created uniqueId 3
--------------[ TESTCASE ]--------------
object(ShouldTypeError)#8 (1) {
  ["uniqueId"]=>
  int(0)
}

Error: why is it not a return TypeError?
Created uniqueId 4
--------------[ TESTCASE ]--------------
string(11) "Const hello"

Error: why is it not a return TypeError?
Created uniqueId 5
--------------[ TESTCASE ]--------------
string(3) "abc"

Error: why is it not a return TypeError?
Created uniqueId 6
--------------[ TESTCASE ]--------------
int(1)

Error: why is it not a return TypeError?
Created uniqueId 7
--------------[ TESTCASE ]--------------
float(1.23)

Error: why is it not a return TypeError?
Created uniqueId 8
--------------[ TESTCASE ]--------------
NULL

Error: why is it not a return TypeError?
Created uniqueId 9
--------------[ TESTCASE ]--------------
bool(true)

Error: why is it not a return TypeError?
Created uniqueId 10
--------------[ TESTCASE ]--------------
bool(false)

Error: why is it not a return TypeError?
Created uniqueId 11
--------------[ TESTCASE ]--------------
array(3) {
  [0]=>
  string(1) "a"
  [1]=>
  string(1) "b"
  [2]=>
  string(1) "c"
}

Error: why is it not a return TypeError?
Created uniqueId 12
--------------[ TESTCASE ]--------------
array(3) {
  [6]=>
  string(3) "six"
  [7]=>
  string(5) "seven"
  [67]=>
  string(9) "six seven"
}

Error: why is it not a return TypeError?
Created uniqueId 13
--------------[ TESTCASE ]--------------
array(4) {
  ["a"]=>
  string(1) "a"
  ["b"]=>
  string(1) "b"
  ["c"]=>
  string(1) "c"
  [0]=>
  string(4) "Zero"
}

Error: why is it not a return TypeError?
Created uniqueId 14
--------------[ TESTCASE ]--------------
enum(SpecialReturnValue::ReturnVoid)

Success: throwable: Undefined constant "void"
--------------[ TESTCASE ]--------------
enum(SpecialReturnValue::ReturnNever)

Success: throwable: Undefined constant "never"
--------------[ TESTCASE ]--------------
enum(SomeEnum::Spades)

Error: why is it not a return TypeError?
Created uniqueId 17
--------------[ TESTCASE ]--------------
object(Closure)#12 (3) {
  ["name"]=>
  string(22) "{closure:/in/svfeO:84}"
  ["file"]=>
  string(9) "/in/svfeO"
  ["line"]=>
  int(84)
}

Error: why is it not a return TypeError?
Created uniqueId 18
--------------[ TESTCASE ]--------------
object(Closure)#1 (3) {
  ["name"]=>
  string(22) "{closure:/in/svfeO:51}"
  ["file"]=>
  string(9) "/in/svfeO"
  ["line"]=>
  int(51)
}

Error: why is it not a return TypeError?
Created uniqueId 19
--------------[ TESTCASE ]--------------
object(Closure)#2 (1) {
  ["function"]=>
  string(12) "SomeFunction"
}

Error: why is it not a return TypeError?
Created uniqueId 20
--------------[ TESTCASE ]--------------
object(Closure)#3 (1) {
  ["function"]=>
  string(12) "SomeFunction"
}

Error: why is it not a return TypeError?
Created uniqueId 21
--------------[ TESTCASE ]--------------
object(Closure)#4 (4) {
  ["name"]=>
  string(22) "{closure:/in/svfeO:59}"
  ["file"]=>
  string(9) "/in/svfeO"
  ["line"]=>
  int(59)
  ["parameter"]=>
  array(1) {
    ["$a"]=>
    string(10) "<required>"
  }
}

Error: why is it not a return TypeError?
Created uniqueId 22
--------------[ TESTCASE ]--------------
object(DateTimeImmutable)#13 (3) {
  ["date"]=>
  string(26) "2026-01-31 12:46:56.009085"
  ["timezone_type"]=>
  int(3)
  ["timezone"]=>
  string(16) "Europe/Amsterdam"
}

Error: why is it not a return TypeError?
Created uniqueId 23
--------------[ TESTCASE ]--------------
resource(5) of type (stream)

Error: why is it not a return TypeError?
Created uniqueId 24

PHP Version

PHP 8.5.2 via 3v4l

Operating System

3v4l

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions