-
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
abf4bb2
7158ac9
917e223
c736c6d
7fa88f8
d10df05
a3d6450
b2518be
6af7ab4
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 |
---|---|---|
|
@@ -5132,6 +5132,69 @@ 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_THROWS(); | ||
} | ||
|
||
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_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)); | ||
mattdinthehouse marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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)); | ||
RETURN_THROWS(); | ||
} | ||
} | ||
/* }}} */ | ||
|
||
void reflection_class_new_lazy(INTERNAL_FUNCTION_PARAMETERS, | ||
int strategy, bool is_reset) | ||
{ | ||
|
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,216 @@ | ||
--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 readonly string $b; | ||
} | ||
|
||
class C | ||
{ | ||
} | ||
|
||
#[\AllowDynamicProperties] | ||
class D | ||
{ | ||
} | ||
|
||
class E | ||
{ | ||
public function __construct( | ||
public int $a, | ||
public string $b, | ||
) | ||
{} | ||
} | ||
|
||
class F | ||
{ | ||
public readonly int $a; | ||
|
||
public function __construct( | ||
public readonly string $b, | ||
) | ||
{} | ||
} | ||
|
||
class G | ||
{ | ||
public readonly int $a; | ||
public readonly string $b; | ||
|
||
public function __construct() | ||
{ | ||
$this->b = 456; | ||
} | ||
} | ||
|
||
class H | ||
{ | ||
public int $a { | ||
set(int $value) => $value + 1; | ||
} | ||
} | ||
|
||
class I | ||
{ | ||
public int $a; | ||
|
||
public int $b { | ||
get => $this->a + 1; | ||
set(int $value) { | ||
$this->a = $value - 1; | ||
} | ||
} | ||
} | ||
|
||
|
||
$rcA = new ReflectionClass('A'); | ||
$rcB = new ReflectionClass('B'); | ||
$rcC = new ReflectionClass('C'); | ||
$rcD = new ReflectionClass('D'); | ||
$rcE = new ReflectionClass('E'); | ||
$rcF = new ReflectionClass('F'); | ||
$rcG = new ReflectionClass('G'); | ||
$rcH = new ReflectionClass('H'); | ||
$rcI = new ReflectionClass('I'); | ||
|
||
// assign bad data type to normal class | ||
try | ||
{ | ||
$rcA->newInstanceFromData(['a' => 'bad', 'b' => 123], ['foo', 1337]); | ||
echo "you should not see this\n"; | ||
} | ||
catch(Throwable $e) | ||
{ | ||
echo "Exception: " . $e->getMessage() . "\n"; | ||
} | ||
|
||
// normal class with constructor | ||
var_dump($rcA->newInstanceFromData(['a' => 123, 'b' => 'good'], ['foo', 1337])); | ||
|
||
// normal class with no constructor and a readonly property | ||
var_dump($rcB->newInstanceFromData(['a' => 123, 'b' => 'good'])); | ||
|
||
// trying to set dynamic properties on class without AllowDynamicProperties attribute | ||
var_dump($rcC->newInstanceFromData(['a' => 123, 'b' => 'good'])); // this should warn | ||
var_dump($rcC->newInstanceFromData([])); // this is fine | ||
|
||
// setting dynamic properties on a class with AllowDynamicProperties attribute | ||
var_dump($rcD->newInstanceFromData(['a' => 123, 'b' => 'good'])); | ||
|
||
// class with property promotion | ||
try | ||
{ | ||
$rcE->newInstanceFromData(['a' => 123, 'b' => 'good']); // no constructor args will fail | ||
echo "you should not see this\n"; | ||
} | ||
catch(Throwable $e) | ||
{ | ||
echo "Exception: " . $e->getMessage() . "\n"; | ||
} | ||
|
||
var_dump($rcE->newInstanceFromData(['a' => 123, 'b' => 'good'], [456, 'foo'])); // constructor args will override class props | ||
|
||
// class with readonly promoted property | ||
var_dump($rcF->newInstanceFromData(['a' => 123], ['b' => 'good'])); | ||
|
||
// readonly property set in the constructor | ||
try | ||
{ | ||
$rcG->newInstanceFromData(['a' => 123, 'b' => 'good']); // setting $b by data will conflict with constructor's set | ||
echo "you should not see this\n"; | ||
} | ||
catch(Throwable $e) | ||
{ | ||
echo "Exception: " . $e->getMessage() . "\n"; | ||
} | ||
|
||
// hooked set property | ||
var_dump($rcH->newInstanceFromData(['a' => 1])); | ||
|
||
// virtual property | ||
var_dump($rcI->newInstanceFromData(['a' => 1, 'b' => 2])); | ||
|
||
$instance = $rcI->newInstanceFromData(['a' => 1]); | ||
var_dump($instance); | ||
var_dump($instance->b); | ||
$instance->b = 3; | ||
var_dump($instance->b); | ||
|
||
?> | ||
--EXPECTF-- | ||
Exception: Cannot assign string to property A::$a of type int | ||
In constructor of class A | ||
object(A)#%d (2) { | ||
["a"]=> | ||
int(123) | ||
["b"]=> | ||
string(4) "good" | ||
} | ||
object(B)#%d (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)#%d (2) { | ||
["a"]=> | ||
int(123) | ||
["b"]=> | ||
string(4) "good" | ||
} | ||
object(C)#%d (0) { | ||
} | ||
object(D)#%d (2) { | ||
["a"]=> | ||
int(123) | ||
["b"]=> | ||
string(4) "good" | ||
} | ||
Exception: Too few arguments to function E::__construct(), 0 passed and exactly 2 expected | ||
object(E)#%d (2) { | ||
["a"]=> | ||
int(456) | ||
["b"]=> | ||
string(3) "foo" | ||
} | ||
object(F)#%d (2) { | ||
["a"]=> | ||
int(123) | ||
["b"]=> | ||
string(4) "good" | ||
} | ||
Exception: Cannot modify readonly property G::$b | ||
object(H)#%d (1) { | ||
["a"]=> | ||
int(2) | ||
} | ||
object(I)#%d (1) { | ||
["a"]=> | ||
int(1) | ||
} | ||
object(I)#%d (1) { | ||
["a"]=> | ||
int(1) | ||
} | ||
int(2) | ||
int(3) |
Uh oh!
There was an error while loading. Please reload this page.