Skip to content

Commit a902f10

Browse files
committed
PHPC-2083: Allow enums to be instantiated during BSON decoding
Enums serialize like PHP objects with a "name" property (BackedEnum instance will also have a "value") and thus become BSON documents. In order for a document to unserialize back to an enum, it must implement either Unserializable or Persistable. The bsonUnserialize() method serves no purpose for initialization, but it will still be invoked.
1 parent c99a856 commit a902f10

File tree

6 files changed

+574
-4
lines changed

6 files changed

+574
-4
lines changed

src/phongo_bson.c

Lines changed: 130 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -768,6 +768,55 @@ static void php_phongo_handle_field_path_entry_for_compound_type(php_phongo_bson
768768
}
769769
}
770770

771+
#if PHP_VERSION_ID >= 80100
772+
/* Resolves an enum class and case name to a zval. On error, an exception will
773+
* have been thrown and NULL will be returned.
774+
*
775+
* This function is modeled after php_var_unserialize_internal in php-src. */
776+
static zval* resolve_enum_case(zend_class_entry* ce, const char* case_name)
777+
{
778+
zval* return_value = NULL;
779+
zend_string* c_str;
780+
zend_class_constant* c;
781+
782+
if (!(ce->ce_flags & ZEND_ACC_ENUM)) {
783+
phongo_throw_exception(PHONGO_ERROR_UNEXPECTED_VALUE, "Class '%s' is not an enum", ZSTR_VAL(ce->name));
784+
goto cleanup;
785+
}
786+
787+
c_str = zend_string_init(case_name, strlen(case_name), 0);
788+
c = zend_hash_find_ptr(CE_CONSTANTS_TABLE(ce), c_str);
789+
790+
if (!c) {
791+
phongo_throw_exception(PHONGO_ERROR_UNEXPECTED_VALUE, "Undefined constant %s::%s", ZSTR_VAL(ce->name), case_name);
792+
goto cleanup;
793+
}
794+
795+
if (!(ZEND_CLASS_CONST_FLAGS(c) & ZEND_CLASS_CONST_IS_CASE)) {
796+
phongo_throw_exception(PHONGO_ERROR_UNEXPECTED_VALUE, "%s::%s is not an enum case", ZSTR_VAL(ce->name), case_name);
797+
goto cleanup;
798+
}
799+
800+
if (Z_TYPE(c->value) == IS_CONSTANT_AST && zval_update_constant_ex(&c->value, ce) == FAILURE) {
801+
phongo_throw_exception(PHONGO_ERROR_UNEXPECTED_VALUE, "Failed to evaluate constant expression AST for %s::%s", ZSTR_VAL(ce->name), case_name);
802+
goto cleanup;
803+
}
804+
805+
if (Z_TYPE(c->value) != IS_OBJECT) {
806+
phongo_throw_exception(PHONGO_ERROR_UNEXPECTED_VALUE, "Expected %s::%s to be an object, but it is: %s", ZSTR_VAL(ce->name), case_name, PHONGO_ZVAL_CLASS_OR_TYPE_NAME_P(&c->value));
807+
}
808+
809+
return_value = &c->value;
810+
811+
cleanup:
812+
if (c_str) {
813+
zend_string_release_ex(c_str, 0);
814+
}
815+
816+
return return_value;
817+
}
818+
#endif /* PHP_VERSION_ID >= 80100 */
819+
771820
static bool php_phongo_bson_visit_document(const bson_iter_t* iter ARG_UNUSED, const char* key, const bson_t* v_document, void* data) /* {{{ */
772821
{
773822
zval* retval = PHONGO_BSON_STATE_ZCHILD(data);
@@ -805,9 +854,51 @@ static bool php_phongo_bson_visit_document(const bson_iter_t* iter ARG_UNUSED, c
805854
break;
806855

807856
case PHONGO_TYPEMAP_CLASS: {
808-
zval obj;
857+
zval obj;
858+
zend_class_entry* obj_ce = state.odm ? state.odm : state.map.document;
859+
860+
#if PHP_VERSION_ID >= 80100
861+
/* Enums require special handling for instantiation */
862+
if (obj_ce->ce_flags & ZEND_ACC_ENUM) {
863+
int plen;
864+
zend_bool pfree;
865+
char* case_name;
866+
zval* enum_case;
867+
868+
case_name = php_array_fetchc_string(&state.zchild, "name", &plen, &pfree);
869+
870+
if (!case_name) {
871+
phongo_throw_exception(PHONGO_ERROR_UNEXPECTED_VALUE, "Missing 'name' field to infer enum case for %s", ZSTR_VAL(obj_ce->name));
872+
873+
/* Clean up and return true to stop iteration for
874+
* our parent context. */
875+
zval_ptr_dtor(&state.zchild);
876+
php_phongo_bson_state_dtor(&state);
877+
return true;
878+
}
879+
880+
enum_case = resolve_enum_case(obj_ce, case_name);
881+
882+
if (pfree) {
883+
efree(case_name);
884+
}
885+
886+
if (!enum_case) {
887+
/* Exception already thrown. Clean up and return
888+
* true to stop iteration for our parent context. */
889+
zval_ptr_dtor(&state.zchild);
890+
php_phongo_bson_state_dtor(&state);
891+
return true;
892+
}
893+
894+
ZVAL_COPY_VALUE(&obj, enum_case);
895+
} else {
896+
object_init_ex(&obj, obj_ce);
897+
}
898+
#else /* PHP_VERSION_ID < 80100 */
899+
object_init_ex(&obj, obj_ce);
900+
#endif /* PHP_VERSION_ID */
809901

810-
object_init_ex(&obj, state.odm ? state.odm : state.map.document);
811902
zend_call_method_with_1_params(PHONGO_COMPAT_OBJ_P(&obj), NULL, NULL, BSON_UNSERIALIZE_FUNC_NAME, NULL, &state.zchild);
812903
if (((php_phongo_bson_state*) data)->is_visiting_array) {
813904
add_next_index_zval(retval, &obj);
@@ -1035,9 +1126,44 @@ bool php_phongo_bson_to_zval_ex(const unsigned char* data, int data_len, php_pho
10351126
break;
10361127

10371128
case PHONGO_TYPEMAP_CLASS: {
1038-
zval obj;
1129+
zval obj;
1130+
zend_class_entry* obj_ce = state->odm ? state->odm : state->map.root;
1131+
1132+
#if PHP_VERSION_ID >= 80100
1133+
/* Enums require special handling for instantiation */
1134+
if (obj_ce->ce_flags & ZEND_ACC_ENUM) {
1135+
int plen;
1136+
zend_bool pfree;
1137+
char* case_name;
1138+
zval* enum_case;
1139+
1140+
case_name = php_array_fetchc_string(&state->zchild, "name", &plen, &pfree);
1141+
1142+
if (!case_name) {
1143+
phongo_throw_exception(PHONGO_ERROR_UNEXPECTED_VALUE, "Missing 'name' field to infer enum case for %s", ZSTR_VAL(obj_ce->name));
1144+
1145+
goto cleanup;
1146+
}
1147+
1148+
enum_case = resolve_enum_case(obj_ce, case_name);
1149+
1150+
if (pfree) {
1151+
efree(case_name);
1152+
}
1153+
1154+
if (!enum_case) {
1155+
/* Exception already thrown */
1156+
goto cleanup;
1157+
}
1158+
1159+
ZVAL_COPY_VALUE(&obj, enum_case);
1160+
} else {
1161+
object_init_ex(&obj, obj_ce);
1162+
}
1163+
#else /* PHP_VERSION_ID < 80100 */
1164+
object_init_ex(&obj, obj_ce);
1165+
#endif /* PHP_VERSION_ID */
10391166

1040-
object_init_ex(&obj, state->odm ? state->odm : state->map.root);
10411167
zend_call_method_with_1_params(PHONGO_COMPAT_OBJ_P(&obj), NULL, NULL, BSON_UNSERIALIZE_FUNC_NAME, NULL, &state->zchild);
10421168
zval_ptr_dtor(&state->zchild);
10431169
ZVAL_COPY_VALUE(&state->zchild, &obj);

tests/bson/bson-enum-001.phpt

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
--TEST--
2+
Enums serialize as documents and are not unserialized by default
3+
--SKIPIF--
4+
<?php require __DIR__ . "/../utils/basic-skipif.inc"; ?>
5+
<?php skip_if_php_version('<', '8.1.0'); ?>
6+
--FILE--
7+
<?php
8+
9+
require_once __DIR__ . '/../utils/basic.inc';
10+
11+
enum MyEnum
12+
{
13+
case foo;
14+
}
15+
16+
enum MyBackedEnum: string
17+
{
18+
case foo = 'bar';
19+
}
20+
21+
$tests = [
22+
MyEnum::foo,
23+
MyBackedEnum::foo,
24+
['myEnum' => MyEnum::foo],
25+
['myBackedEnum' => MyBackedEnum::foo],
26+
];
27+
28+
foreach ($tests as $document) {
29+
$bson = fromPHP($document);
30+
echo "Test ", toJSON($bson), "\n";
31+
hex_dump($bson);
32+
var_dump(toPHP($bson));
33+
echo "\n";
34+
}
35+
36+
?>
37+
===DONE===
38+
<?php exit(0); ?>
39+
--EXPECTF--
40+
Test { "name" : "foo" }
41+
0 : 13 00 00 00 02 6e 61 6d 65 00 04 00 00 00 66 6f [.....name.....fo]
42+
10 : 6f 00 00 [o..]
43+
object(stdClass)#%d (%d) {
44+
["name"]=>
45+
string(3) "foo"
46+
}
47+
48+
Test { "name" : "foo", "value" : "bar" }
49+
0 : 22 00 00 00 02 6e 61 6d 65 00 04 00 00 00 66 6f ["....name.....fo]
50+
10 : 6f 00 02 76 61 6c 75 65 00 04 00 00 00 62 61 72 [o..value.....bar]
51+
20 : 00 00 [..]
52+
object(stdClass)#%d (%d) {
53+
["name"]=>
54+
string(3) "foo"
55+
["value"]=>
56+
string(3) "bar"
57+
}
58+
59+
Test { "myEnum" : { "name" : "foo" } }
60+
0 : 20 00 00 00 03 6d 79 45 6e 75 6d 00 13 00 00 00 [ ....myEnum.....]
61+
10 : 02 6e 61 6d 65 00 04 00 00 00 66 6f 6f 00 00 00 [.name.....foo...]
62+
object(stdClass)#%d (%d) {
63+
["myEnum"]=>
64+
object(stdClass)#%d (%d) {
65+
["name"]=>
66+
string(3) "foo"
67+
}
68+
}
69+
70+
Test { "myBackedEnum" : { "name" : "foo", "value" : "bar" } }
71+
0 : 35 00 00 00 03 6d 79 42 61 63 6b 65 64 45 6e 75 [5....myBackedEnu]
72+
10 : 6d 00 22 00 00 00 02 6e 61 6d 65 00 04 00 00 00 [m."....name.....]
73+
20 : 66 6f 6f 00 02 76 61 6c 75 65 00 04 00 00 00 62 [foo..value.....b]
74+
30 : 61 72 00 00 00 [ar...]
75+
object(stdClass)#%d (%d) {
76+
["myBackedEnum"]=>
77+
object(stdClass)#%d (%d) {
78+
["name"]=>
79+
string(3) "foo"
80+
["value"]=>
81+
string(3) "bar"
82+
}
83+
}
84+
85+
===DONE===

tests/bson/bson-enum-002.phpt

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
--TEST--
2+
Enums implementing Unserializable
3+
--SKIPIF--
4+
<?php require __DIR__ . "/../utils/basic-skipif.inc"; ?>
5+
<?php skip_if_php_version('<', '8.1.0'); ?>
6+
--FILE--
7+
<?php
8+
9+
require_once __DIR__ . '/../utils/basic.inc';
10+
11+
enum MyEnum implements MongoDB\BSON\Unserializable
12+
{
13+
case foo;
14+
15+
public function bsonUnserialize(array $data): void
16+
{
17+
/* Enums do not maintain state, so this method serves no practical
18+
* purpose other than being required by the interface. Since an
19+
* implementation is required, we will log a message to assert that BSON
20+
* decoding always invokes this method. */
21+
printf("%s called with: %s\n", __METHOD__, json_encode($data));
22+
}
23+
}
24+
25+
enum MyBackedEnum: string implements MongoDB\BSON\Unserializable
26+
{
27+
case foo = 'bar';
28+
29+
public function bsonUnserialize(array $data): void
30+
{
31+
printf("%s called with: %s\n", __METHOD__, json_encode($data));
32+
}
33+
}
34+
35+
$tests = [
36+
/* There is no practical reason to use an enum as a root document, since it
37+
* cannot have an "_id" field, but we'll test this anyway since we're only
38+
* using BSON functions and not round-tripping data through the server. */
39+
[
40+
MyEnum::foo,
41+
['root' => MyEnum::class],
42+
],
43+
[
44+
MyBackedEnum::foo,
45+
['root' => MyBackedEnum::class],
46+
],
47+
[
48+
['myEnum' => MyEnum::foo],
49+
['fieldPaths' => ['myEnum' => MyEnum::class]],
50+
],
51+
[
52+
['myBackedEnum' => MyBackedEnum::foo],
53+
['fieldPaths' => ['myBackedEnum' => MyBackedEnum::class]],
54+
],
55+
];
56+
57+
foreach ($tests as $test) {
58+
[$document, $typeMap] = $test;
59+
60+
$bson = fromPHP($document);
61+
echo "Test ", toJSON($bson), "\n";
62+
hex_dump($bson);
63+
var_dump(toPHP($bson, $typeMap));
64+
echo "\n";
65+
}
66+
67+
?>
68+
===DONE===
69+
<?php exit(0); ?>
70+
--EXPECTF--
71+
Test { "name" : "foo" }
72+
0 : 13 00 00 00 02 6e 61 6d 65 00 04 00 00 00 66 6f [.....name.....fo]
73+
10 : 6f 00 00 [o..]
74+
MyEnum::bsonUnserialize called with: {"name":"foo"}
75+
enum(MyEnum::foo)
76+
77+
Test { "name" : "foo", "value" : "bar" }
78+
0 : 22 00 00 00 02 6e 61 6d 65 00 04 00 00 00 66 6f ["....name.....fo]
79+
10 : 6f 00 02 76 61 6c 75 65 00 04 00 00 00 62 61 72 [o..value.....bar]
80+
20 : 00 00 [..]
81+
MyBackedEnum::bsonUnserialize called with: {"name":"foo","value":"bar"}
82+
enum(MyBackedEnum::foo)
83+
84+
Test { "myEnum" : { "name" : "foo" } }
85+
0 : 20 00 00 00 03 6d 79 45 6e 75 6d 00 13 00 00 00 [ ....myEnum.....]
86+
10 : 02 6e 61 6d 65 00 04 00 00 00 66 6f 6f 00 00 00 [.name.....foo...]
87+
MyEnum::bsonUnserialize called with: {"name":"foo"}
88+
object(stdClass)#%d (%d) {
89+
["myEnum"]=>
90+
enum(MyEnum::foo)
91+
}
92+
93+
Test { "myBackedEnum" : { "name" : "foo", "value" : "bar" } }
94+
0 : 35 00 00 00 03 6d 79 42 61 63 6b 65 64 45 6e 75 [5....myBackedEnu]
95+
10 : 6d 00 22 00 00 00 02 6e 61 6d 65 00 04 00 00 00 [m."....name.....]
96+
20 : 66 6f 6f 00 02 76 61 6c 75 65 00 04 00 00 00 62 [foo..value.....b]
97+
30 : 61 72 00 00 00 [ar...]
98+
MyBackedEnum::bsonUnserialize called with: {"name":"foo","value":"bar"}
99+
object(stdClass)#%d (%d) {
100+
["myBackedEnum"]=>
101+
enum(MyBackedEnum::foo)
102+
}
103+
104+
===DONE===

0 commit comments

Comments
 (0)