Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[POC] Support calling functions from most constant expressions
I can think of two main approaches to adding function call support to PHP. This implements the latter. 1. Only allow functions that are actually deterministic and don't depend on ini settings to be used in constant declarations. For example, allow `\count()`, `\strlen()`, `\array_merge()`, and `\in_array()`, but possibly don't allow functions such as `strtolower()` (different in Turkish locale), sprintf() (Depends on `ini_get('precision')`, but so does `(string)EXPR`), `json_encode()` (The `json` extension can be disabled), or calls which aren't unambiguously resolved with `\` or `use function`. 2. Allow any function (user-defined or internal) to be called, leave it to coding practice guidelines to assert that constants are only used in safe ways. ------- - This POC can handle fully qualified, namespace relative, and not-FQ calls. - Argument unpacking is supported - Parameter defaults are evaluated every time if the expression contains function calls. - This handles recursive definitions. It throws if a function call being evaluated ends up reaching the same call. - Static method calls with known classes (other than static::) are probably easy to implement, but omitted from this RFC. - This also constrains function return values to be valid constants, (i.e. they can be the same values that define() would accept) and throws an Error otherwise. In the future, `const X = [my_call()->x];` may be possible, but returning an object in any call is forbidden here. - Variables and functions such as `func_get_args()` aren't supported, because they depend on the declaration's scope. - TODO: Forbid backtick string syntax for shell_exec, which gets converted to a ZEND_AST_CALL node. ------- It turns out that function calls can already be invoked when evaluating a constant, e.g. from php's error handler. So it seems viable for PHP's engine to support regular calls, which this POC does (Imagine an error handler defining SOME_DYNAMIC_CONST123 to be a dynamic value if a notice is emitted for the expression `const X = [[]['undefined_index'], SOME_DYNAMIC_CONST123][1];`) Function calls are allowed in the following types of constant expressions - Defaults of static properties, but not instance properties, due to changes required to the PHP internals expanding the scope of this RFC too much. - Parameter defaults - Global constants and class constants - Defaults of static variables
- Loading branch information
1 parent
3796597
commit bd34c05
Showing
26 changed files
with
638 additions
and
4 deletions.
There are no files selected for viewing
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
--TEST-- | ||
Can call internal functions from class constants | ||
--FILE-- | ||
<?php | ||
class Example { | ||
const X = sprintf("Hello, %s\n", "World"); | ||
|
||
public static function main() { | ||
echo "X is " . self::X . "\n"; | ||
} | ||
} | ||
Example::main(); | ||
?> | ||
--EXPECT-- | ||
X is Hello, World |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
--TEST-- | ||
Can call internal functions from class constants | ||
--FILE-- | ||
<?php | ||
function normalize_keys(array $x) { | ||
// Should only invoke normalize_keys once | ||
echo "Normalizing the keys\n"; | ||
$result = []; | ||
foreach ($x as $k => $value) { | ||
$result["prefix_$k"] = $value; | ||
} | ||
return $result; | ||
} | ||
class Example { | ||
const X = [ | ||
'key1' => 'value1', | ||
'key2' => 'value2', | ||
]; | ||
const Y = array_flip(self::X); | ||
const Z = normalize_keys(self::Y); | ||
} | ||
var_export(Example::Z); | ||
var_export(Example::Z); | ||
var_export(Example::Y); | ||
?> | ||
--EXPECT-- | ||
Normalizing the keys | ||
array ( | ||
'prefix_value1' => 'key1', | ||
'prefix_value2' => 'key2', | ||
)array ( | ||
'prefix_value1' => 'key1', | ||
'prefix_value2' => 'key2', | ||
)array ( | ||
'value1' => 'key1', | ||
'value2' => 'key2', | ||
) |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
--TEST-- | ||
Cannot call functions accessing the variable scope in class constants | ||
--FILE-- | ||
<?php | ||
class Example { | ||
const X = func_get_args(); | ||
|
||
public static function main($value) { | ||
try { | ||
var_export(self::X); | ||
} catch (Error $e) { | ||
printf("Caught %s on line %d\n", $e->getMessage(), $e->getLine()); | ||
} | ||
try { | ||
var_export(self::X); | ||
} catch (Error $e) { | ||
printf("Caught %s on line %d\n", $e->getMessage(), $e->getLine()); | ||
} | ||
} | ||
} | ||
Example::main('test'); | ||
?> | ||
--EXPECT-- | ||
Caught Cannot call func_get_args() dynamically on line 7 | ||
Caught Cannot call func_get_args() dynamically on line 12 |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
--TEST-- | ||
Can call internal functions from global constants | ||
--FILE-- | ||
<?php | ||
const NIL = var_export(null, true); | ||
|
||
echo "NIL is " . NIL . "\n"; | ||
?> | ||
--EXPECT-- | ||
NIL is NULL |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
--TEST-- | ||
Cannot declare constants with function calls that contain objects | ||
--FILE-- | ||
<?php | ||
function make_object_array() { | ||
return [new stdClass()]; | ||
} | ||
const OBJECT_VALUES = make_object_array(); | ||
?> | ||
--EXPECTF-- | ||
Fatal error: Uncaught Error: Calls in constants may only evaluate to scalar values, arrays or resources in %s:5 | ||
Stack trace: | ||
#0 {main} | ||
thrown in %s on line 5 |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
--TEST-- | ||
Can call internal functions from parameter default | ||
--FILE-- | ||
<?php | ||
function evaluated_user_function() { | ||
// NOTE: PHP only caches non-refcounted values in the RECV_INIT value, | ||
// meaning that if the returned value is dynamic, this will get called every time. | ||
// TODO: Would it be worth it to convert refcounted values to immutable values? | ||
echo "Evaluating default\n"; | ||
return sprintf("%s default", "Dynamic"); | ||
} | ||
function test_default($x = evaluated_user_function()) { | ||
echo "x is $x\n"; | ||
} | ||
test_default(); | ||
test_default(2); | ||
test_default(); | ||
?> | ||
--EXPECT-- | ||
Evaluating default | ||
x is Dynamic default | ||
x is 2 | ||
Evaluating default | ||
x is Dynamic default |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
--TEST-- | ||
Cannot access variable scope in parameter defaults | ||
--FILE-- | ||
<?php | ||
|
||
namespace NS; | ||
|
||
use Error; | ||
|
||
function test_default($x = func_get_args()) { | ||
var_dump($x); | ||
} | ||
try { | ||
test_default(); | ||
} catch (Error $e) { | ||
printf("Caught %s on line %d\n", $e->getMessage(), $e->getLine()); | ||
} | ||
test_default('overriding the default'); | ||
try { | ||
test_default(); | ||
} catch (Error $e) { | ||
printf("Caught %s on line %d\n", $e->getMessage(), $e->getLine()); | ||
} | ||
?> | ||
--EXPECT-- | ||
Caught Cannot call func_get_args() dynamically on line 7 | ||
string(22) "overriding the default" | ||
Caught Cannot call func_get_args() dynamically on line 7 |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
--TEST-- | ||
User-defined functions are evaluated every time in param defaults | ||
--FILE-- | ||
<?php | ||
|
||
function generate_new_id() : int { | ||
static $id = 100; | ||
++$id; | ||
return $id; | ||
} | ||
|
||
function test_id(int $id = generate_new_id()) { | ||
echo "id is $id\n"; | ||
} | ||
test_id(); | ||
test_id(-1); | ||
test_id(); | ||
?> | ||
--EXPECT-- | ||
id is 101 | ||
id is -1 | ||
id is 102 |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
--TEST-- | ||
Warn when calling function expecting a reference as an argument | ||
--INI-- | ||
error_reporting=E_ALL | ||
--FILE-- | ||
<?php | ||
class Example { | ||
const VALUES = []; | ||
const IS_MATCH = preg_match('/test/', 'testing'); | ||
const IS_MATCH_V2 = preg_match('/test/', 'testing', self::VALUES); | ||
|
||
public static function main() { | ||
echo "X is " . self::X . "\n"; | ||
} | ||
} | ||
var_dump(Example::IS_MATCH); | ||
var_dump(Example::IS_MATCH_V2); | ||
var_dump(Example::VALUES); | ||
--EXPECTF-- | ||
int(1) | ||
|
||
Warning: Parameter 3 to preg_match() expected to be a reference, value given in %s on line 12 | ||
int(1) | ||
array(0) { | ||
} |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
--TEST-- | ||
Can call user-defined functions from defaults of static properties | ||
--FILE-- | ||
<?php | ||
namespace NS; | ||
|
||
function log_call($arg) { | ||
echo "log_call(" . var_export($arg, true) . ")\n"; | ||
return $arg; | ||
} | ||
|
||
class MyClass { | ||
public static $DEBUG = log_call(true); | ||
public static $DEBUG2 = namespace\log_call(range(1,2)); | ||
} | ||
echo "Start\n"; | ||
var_export(MyClass::$DEBUG); echo "\n"; | ||
var_export(MyClass::$DEBUG2); echo "\n"; | ||
var_export(MyClass::$DEBUG); echo "\n"; | ||
MyClass::$DEBUG = "New value"; | ||
echo MyClass::$DEBUG . "\n"; | ||
?> | ||
--EXPECT-- | ||
Start | ||
log_call(true) | ||
log_call(array ( | ||
0 => 1, | ||
1 => 2, | ||
)) | ||
true | ||
array ( | ||
0 => 1, | ||
1 => 2, | ||
) | ||
true | ||
New value |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
--TEST-- | ||
Cannot call functions from defaults of instance properties | ||
--FILE-- | ||
<?php | ||
// Currently, php will evaluate all of the instance property defaults at once and cache them | ||
// the first time a class gets instantiated, in _object_and_properties_init. | ||
// | ||
// Authors of future RFCs may wish to change PHP | ||
// to allow `count()` or `generate_unique_id()` or `public $fields = new stdClass();` | ||
// as the defaults of instance properties. | ||
class MyClass { | ||
public $v1 = count([]); | ||
} | ||
?> | ||
--EXPECTF-- | ||
Fatal error: Default value for instance property MyClass::$v1 cannot contain function calls in %s on line 9 |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
--TEST-- | ||
Recursion in calls in class constants causes an error | ||
--FILE-- | ||
<?php | ||
function x_plus_1() { | ||
echo "Computing X + 1\n"; | ||
return Recursion::X + 1; | ||
} | ||
class Recursion { | ||
const X = x_plus_1(); | ||
const MISSING = MISSING_GLOBAL + 1; | ||
} | ||
try { | ||
echo "Recursion::X=" . Recursion::X . "\n"; | ||
} catch (Error $e) { | ||
printf("Caught %s: %s\n", get_class($e), $e->getMessage()); | ||
} | ||
try { | ||
echo "Recursion::X=" . Recursion::X . "\n"; | ||
} catch (Error $e) { | ||
printf("Second call caught %s: %s\n", get_class($e), $e->getMessage()); | ||
} | ||
try { | ||
echo "Recursion::MISSING=" . Recursion::MISSING; | ||
} catch (Error $e) { | ||
printf("Caught %s: %s\n", get_class($e), $e->getMessage()); | ||
} | ||
try { | ||
echo "Recursion::MISSING=" . Recursion::MISSING; | ||
} catch (Error $e) { | ||
printf("Second call caught %s: %s\n", get_class($e), $e->getMessage()); | ||
} | ||
?> | ||
--EXPECT-- | ||
Computing X + 1 | ||
Caught Error: Unrecoverable error calling x_plus_1() in recursive constant definition | ||
Computing X + 1 | ||
Second call caught Error: Unrecoverable error calling x_plus_1() in recursive constant definition | ||
Caught Error: Undefined constant 'MISSING_GLOBAL' | ||
Second call caught Error: Undefined constant 'MISSING_GLOBAL' |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
--TEST-- | ||
Can call internal functions from defaults of static variables | ||
--FILE-- | ||
<?php | ||
function main() { | ||
static $call = SPRINTF("%s!", sprintf("Hello, %s", "World")); | ||
echo "$call\n"; | ||
} | ||
main(); | ||
main(); | ||
?> | ||
--EXPECT-- | ||
Hello, World! | ||
Hello, World! |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
--TEST-- | ||
Can call user-defined functions from defaults of static variables | ||
--FILE-- | ||
<?php | ||
function log_call(string $arg) { | ||
echo "log_call('$arg')\n"; | ||
return $arg; | ||
} | ||
|
||
$f = function () { | ||
static $call = log_call(sprintf("Hello, %s", "World")); | ||
echo "$call\n"; | ||
}; | ||
$f(); | ||
$f(); | ||
?> | ||
--EXPECT-- | ||
log_call('Hello, World') | ||
Hello, World | ||
Hello, World |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
--TEST-- | ||
Can call internal functions from defaults of static variables | ||
--FILE-- | ||
<?php | ||
function main() { | ||
static $call = missing_sprintf("%s!", sprintf("Hello, %s", "World")); | ||
echo "$call\n"; | ||
} | ||
for ($i = 0; $i < 2; $i++) { | ||
try { | ||
main(); | ||
} catch (Error $e) { | ||
printf("Caught %s: %s at line %d\n", get_class($e), $e->getMessage(), $e->getLine()); | ||
} | ||
} | ||
?> | ||
--EXPECT-- | ||
Caught Error: Call to undefined function missing_sprintf() at line 3 | ||
Caught Error: Call to undefined function missing_sprintf() at line 3 |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
--TEST-- | ||
ArgumentCountError thrown if constant contains call with too few arguments | ||
--FILE-- | ||
<?php | ||
class Example { | ||
const X = sprintf(); | ||
|
||
public static function main() { | ||
echo "X is " . self::X . "\n"; | ||
} | ||
} | ||
for ($i = 0; $i < 2; $i++) { | ||
try { | ||
Example::main(); | ||
} catch (ArgumentCountError $e) { | ||
printf("Caught %s on line %d\n", $e->getMessage(), $e->getLine()); | ||
} | ||
} | ||
?> | ||
--EXPECT-- | ||
Caught sprintf() expects at least 1 parameter, 0 given on line 6 | ||
Caught sprintf() expects at least 1 parameter, 0 given on line 6 |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
--TEST-- | ||
Allow varargs in calls in constants | ||
--INI-- | ||
error_reporting=E_ALL | ||
--FILE-- | ||
<?php | ||
const ARGS = ['Hello, %s', 'World']; | ||
const RESULT = sprintf(...ARGS); | ||
const RESULT_LINE = sprintf("%s\n", ...[RESULT]); | ||
echo RESULT_LINE; | ||
echo RESULT_LINE; | ||
--EXPECTF-- | ||
Hello, World | ||
Hello, World |
Oops, something went wrong.