-
Notifications
You must be signed in to change notification settings - Fork 7.9k
Add method to ReflectionClass to instantiate with the same semantics as PDOStatement::fetchObject()
#19401
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Add method to ReflectionClass to instantiate with the same semantics as PDOStatement::fetchObject()
#19401
Changes from all commits
4066651
f33b64e
0380b54
87572e9
16b685d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5127,6 +5127,67 @@ ZEND_METHOD(ReflectionClass, newInstanceArgs) | |
} | ||
/* }}} */ | ||
|
||
/* {{{ Returns an instance of this class whose properties are filled with the given data before the constructor is called */ | ||
ZEND_METHOD(ReflectionClass, newInstanceFromData) | ||
{ | ||
reflection_object *intern; | ||
zend_class_entry *ce; | ||
int argc = 0; | ||
HashTable *data, *args = NULL; | ||
zend_function *constructor; | ||
zend_string *key; | ||
zval *val; | ||
|
||
GET_REFLECTION_OBJECT_PTR(ce); | ||
|
||
if (ce->type == ZEND_INTERNAL_CLASS) { | ||
zend_throw_exception_ex(reflection_exception_ptr, 0, "Class %s is an internal class that cannot be instantiated from data", ZSTR_VAL(ce->name)); | ||
RETURN_THROWS(); | ||
} | ||
|
||
ZEND_PARSE_PARAMETERS_START(1, 2) | ||
Z_PARAM_ARRAY_HT(data) | ||
Z_PARAM_OPTIONAL | ||
Z_PARAM_ARRAY_HT(args) | ||
ZEND_PARSE_PARAMETERS_END(); | ||
|
||
if (args) { | ||
argc = zend_hash_num_elements(args); | ||
} | ||
|
||
if (UNEXPECTED(object_init_ex(return_value, ce) != SUCCESS)) { | ||
return; | ||
} | ||
|
||
ZEND_HASH_FOREACH_STR_KEY_VAL(data, key, val) { | ||
zend_update_property_ex(ce, Z_OBJ_P(return_value), key, val); | ||
} ZEND_HASH_FOREACH_END(); | ||
|
||
const zend_class_entry *old_scope = EG(fake_scope); | ||
EG(fake_scope) = ce; | ||
constructor = Z_OBJ_HT_P(return_value)->get_constructor(Z_OBJ_P(return_value)); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This may throw for internal classes. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's the same code used by There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not sure whether that's really a BC break? It would be throwing an exception either way? We had a discussion a while ago with @DanielEScherzer and I think @nielsdos that it would probably be better to do something to fix the semantics regarding failing Probably the best solution is that if the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh I see what you mean - because this method sets properties before calling constructors that can have weird (and potentially vulnerable) behaviour. I've added a check for There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That discussion is at #17796 Just so that I understand the semantics correctly, is there any difference between the proposed function and something like (class is given as an argument but would be from $this) function newInstanceFromData(string $class, array $data, array $args = []) {
$ref = new ReflectionClass($class);
$instance = $ref->newInstanceWithoutConstructor();
foreach ($data as $propName => $val) {
$propAccess = $ref->getProperty($propName);
$propAccess->setValue($instance, $val);
}
$instance->__construct(...$args);
return $instance;
} |
||
EG(fake_scope) = old_scope; | ||
|
||
/* Run the constructor if there is one */ | ||
if (constructor) { | ||
if (!(constructor->common.fn_flags & ZEND_ACC_PUBLIC)) { | ||
zend_throw_exception_ex(reflection_exception_ptr, 0, "Access to non-public constructor of class %s", ZSTR_VAL(ce->name)); | ||
zval_ptr_dtor(return_value); | ||
RETURN_NULL(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should be RETURN_THROWS() |
||
} | ||
|
||
zend_call_known_function( | ||
constructor, Z_OBJ_P(return_value), Z_OBJCE_P(return_value), NULL, 0, NULL, args); | ||
|
||
if (EG(exception)) { | ||
zend_object_store_ctor_failed(Z_OBJ_P(return_value)); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. and also RETURN_THROWS() |
||
} | ||
} else if (argc) { | ||
zend_throw_exception_ex(reflection_exception_ptr, 0, "Class %s does not have a constructor, so you cannot pass any constructor arguments", ZSTR_VAL(ce->name)); | ||
} | ||
} | ||
/* }}} */ | ||
|
||
void reflection_class_new_lazy(INTERNAL_FUNCTION_PARAMETERS, | ||
int strategy, bool is_reset) | ||
{ | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -370,6 +370,8 @@ public function newInstanceWithoutConstructor(): object {} | |
/** @tentative-return-type */ | ||
public function newInstanceArgs(array $args = []): ?object {} | ||
|
||
public function newInstanceFromData(array $data, array $args = []): ?object {} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should always return an |
||
|
||
public function newLazyGhost(callable $initializer, int $options = 0): object {} | ||
|
||
public function newLazyProxy(callable $factory, int $options = 0): object {} | ||
|
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
--TEST-- | ||
ReflectionClass::newInstanceFromData | ||
--FILE-- | ||
<?php | ||
|
||
class A | ||
{ | ||
public int $a; | ||
public string $b; | ||
|
||
public function __construct($c, $d) | ||
{ | ||
echo "In constructor of class A\n"; | ||
} | ||
} | ||
|
||
class B | ||
{ | ||
public int $a; | ||
public string $b; | ||
} | ||
|
||
class C | ||
{ | ||
} | ||
|
||
#[\AllowDynamicProperties] | ||
class D | ||
{ | ||
} | ||
|
||
|
||
$rcA = new ReflectionClass('A'); | ||
$rcB = new ReflectionClass('B'); | ||
$rcC = new ReflectionClass('C'); | ||
$rcD = new ReflectionClass('D'); | ||
|
||
try | ||
{ | ||
$rcA->newInstanceFromData(['a' => 'bad', 'b' => 123], ['foo', 1337]); | ||
} | ||
catch(Throwable $e) | ||
{ | ||
echo "Exception: " . $e->getMessage() . "\n"; | ||
} | ||
|
||
var_dump($rcA->newInstanceFromData(['a' => 123, 'b' => 'good'], ['foo', 1337])); | ||
|
||
var_dump($rcB->newInstanceFromData(['a' => 123, 'b' => 'good'])); | ||
|
||
var_dump($rcC->newInstanceFromData(['a' => 123, 'b' => 'good'])); | ||
|
||
var_dump($rcC->newInstanceFromData([])); | ||
|
||
var_dump($rcD->newInstanceFromData(['a' => 123, 'b' => 'good'])); | ||
|
||
?> | ||
--EXPECTF-- | ||
Exception: Cannot assign string to property A::$a of type int | ||
In constructor of class A | ||
object(A)#5 (2) { | ||
["a"]=> | ||
int(123) | ||
["b"]=> | ||
string(4) "good" | ||
} | ||
object(B)#5 (2) { | ||
["a"]=> | ||
int(123) | ||
["b"]=> | ||
string(4) "good" | ||
} | ||
|
||
Deprecated: Creation of dynamic property C::$a is deprecated in %s on line %d | ||
|
||
Deprecated: Creation of dynamic property C::$b is deprecated in %s on line %d | ||
object(C)#5 (2) { | ||
["a"]=> | ||
int(123) | ||
["b"]=> | ||
string(4) "good" | ||
} | ||
object(C)#5 (0) { | ||
} | ||
object(D)#5 (2) { | ||
["a"]=> | ||
int(123) | ||
["b"]=> | ||
string(4) "good" | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
--TEST-- | ||
ReflectionClass::newInstanceFromData - internal class | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. please also add tests for
|
||
--FILE-- | ||
<?php | ||
|
||
$rcDateTime = new ReflectionClass('DateTime'); | ||
$rcPDOStatement = new ReflectionClass('PDOStatement'); | ||
|
||
try | ||
{ | ||
$rcDateTime->newInstanceFromData([], ['now', new DateTimeZone('UTC')]); | ||
} | ||
catch(Throwable $e) | ||
{ | ||
echo "Exception: " . $e->getMessage() . "\n"; | ||
} | ||
|
||
try | ||
{ | ||
$rcPDOStatement->newInstanceFromData(['a' => 123]); | ||
} | ||
catch(Throwable $e) | ||
{ | ||
echo "Exception: " . $e->getMessage() . "\n"; | ||
} | ||
|
||
?> | ||
--EXPECTF-- | ||
Exception: Class DateTime is an internal class that cannot be instantiated from data | ||
Exception: Class PDOStatement is an internal class that cannot be instantiated from data |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if the initialization isn't successful, we should confirm that there is an exception, and then RETURN_THROWS()