Skip to content

Commit de5f1e5

Browse files
jmikolasgolemon
andauthored
PHPC-2083: Allow enums to be instantiated during BSON decoding (#1317)
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. This introduces a PersistableEnum trait, which can be used for both Unserializable or Persistable implementations. Co-authored-by: Sara Golemon <pollita@php.net>
1 parent 28b4d46 commit de5f1e5

15 files changed

+830
-5
lines changed

config.m4

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ if test "$PHP_MONGODB" != "no"; then
125125
src/BSON/ObjectId.c \
126126
src/BSON/ObjectIdInterface.c \
127127
src/BSON/Persistable.c \
128+
src/BSON/PersistableEnum.c \
128129
src/BSON/Regex.c \
129130
src/BSON/RegexInterface.c \
130131
src/BSON/Serializable.c \

config.w32

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ if (PHP_MONGODB != "no") {
121121

122122
EXTENSION("mongodb", "php_phongo.c", null, PHP_MONGODB_CFLAGS);
123123
MONGODB_ADD_SOURCES("/src", "phongo_apm.c phongo_bson.c phongo_bson_encode.c phongo_client.c phongo_compat.c phongo_error.c phongo_execute.c phongo_ini.c phongo_util.c");
124-
MONGODB_ADD_SOURCES("/src/BSON", "Binary.c BinaryInterface.c DBPointer.c Decimal128.c Decimal128Interface.c Int64.c Javascript.c JavascriptInterface.c MaxKey.c MaxKeyInterface.c MinKey.c MinKeyInterface.c ObjectId.c ObjectIdInterface.c Persistable.c Regex.c RegexInterface.c Serializable.c Symbol.c Timestamp.c TimestampInterface.c Type.c Undefined.c Unserializable.c UTCDateTime.c UTCDateTimeInterface.c functions.c");
124+
MONGODB_ADD_SOURCES("/src/BSON", "Binary.c BinaryInterface.c DBPointer.c Decimal128.c Decimal128Interface.c Int64.c Javascript.c JavascriptInterface.c MaxKey.c MaxKeyInterface.c MinKey.c MinKeyInterface.c ObjectId.c ObjectIdInterface.c Persistable.c PersistableEnum.c Regex.c RegexInterface.c Serializable.c Symbol.c Timestamp.c TimestampInterface.c Type.c Undefined.c Unserializable.c UTCDateTime.c UTCDateTimeInterface.c functions.c");
125125
MONGODB_ADD_SOURCES("/src/MongoDB", "BulkWrite.c ClientEncryption.c Command.c Cursor.c CursorId.c CursorInterface.c Manager.c Query.c ReadConcern.c ReadPreference.c Server.c ServerApi.c ServerDescription.c Session.c TopologyDescription.c WriteConcern.c WriteConcernError.c WriteError.c WriteResult.c");
126126
MONGODB_ADD_SOURCES("/src/MongoDB/Exception", "AuthenticationException.c BulkWriteException.c CommandException.c ConnectionException.c ConnectionTimeoutException.c EncryptionException.c Exception.c ExecutionTimeoutException.c InvalidArgumentException.c LogicException.c RuntimeException.c ServerException.c SSLConnectionException.c UnexpectedValueException.c WriteException.c");
127127
MONGODB_ADD_SOURCES("/src/MongoDB/Monitoring", "CommandFailedEvent.c CommandStartedEvent.c CommandSubscriber.c CommandSucceededEvent.c SDAMSubscriber.c Subscriber.c ServerChangedEvent.c ServerClosedEvent.c ServerHeartbeatFailedEvent.c ServerHeartbeatStartedEvent.c ServerHeartbeatSucceededEvent.c ServerOpeningEvent.c TopologyChangedEvent.c TopologyClosedEvent.c TopologyOpeningEvent.c functions.c");

php_phongo.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,7 @@ PHP_MINIT_FUNCTION(mongodb) /* {{{ */
217217
php_phongo_minkey_init_ce(INIT_FUNC_ARGS_PASSTHRU);
218218
php_phongo_objectid_init_ce(INIT_FUNC_ARGS_PASSTHRU);
219219
php_phongo_persistable_init_ce(INIT_FUNC_ARGS_PASSTHRU);
220+
php_phongo_persistableenum_init_ce(INIT_FUNC_ARGS_PASSTHRU);
220221
php_phongo_regex_init_ce(INIT_FUNC_ARGS_PASSTHRU);
221222
php_phongo_symbol_init_ce(INIT_FUNC_ARGS_PASSTHRU);
222223
php_phongo_timestamp_init_ce(INIT_FUNC_ARGS_PASSTHRU);

src/BSON/PersistableEnum.c

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/*
2+
* Copyright 2014-present MongoDB, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
#include <php.h>
18+
19+
#include "php_phongo.h"
20+
#include "phongo_error.h"
21+
#include "PersistableEnum_arginfo.h"
22+
23+
zend_class_entry* php_phongo_persistableenum_ce;
24+
25+
PHP_METHOD(MongoDB_BSON_PersistableEnum, bsonSerialize)
26+
{
27+
PHONGO_PARSE_PARAMETERS_NONE();
28+
29+
RETVAL_ZVAL(getThis(), 1, 0);
30+
convert_to_array(return_value);
31+
32+
return;
33+
}
34+
35+
PHP_METHOD(MongoDB_BSON_PersistableEnum, bsonUnserialize)
36+
{
37+
zval* data;
38+
39+
PHONGO_PARSE_PARAMETERS_START(1, 1)
40+
Z_PARAM_ARRAY(data)
41+
PHONGO_PARSE_PARAMETERS_END();
42+
43+
return;
44+
} /* }}} */
45+
46+
void php_phongo_persistableenum_init_ce(INIT_FUNC_ARGS) /* {{{ */
47+
{
48+
php_phongo_persistableenum_ce = register_class_MongoDB_BSON_PersistableEnum();
49+
} /* }}} */

src/BSON/PersistableEnum.stub.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?php
2+
3+
/**
4+
* @generate-class-entries static
5+
* @generate-function-entries
6+
*/
7+
8+
namespace MongoDB\BSON;
9+
10+
trait PersistableEnum
11+
{
12+
public final function bsonSerialize(): array {}
13+
14+
public final function bsonUnserialize(array $data): void {}
15+
}

src/BSON/PersistableEnum_arginfo.h

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/* This is a generated file, edit the .stub.php file instead.
2+
* Stub hash: db87873800da8dfaf4b3235eed97254593c9b83d */
3+
4+
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_MongoDB_BSON_PersistableEnum_bsonSerialize, 0, 0, IS_ARRAY, 0)
5+
ZEND_END_ARG_INFO()
6+
7+
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_MongoDB_BSON_PersistableEnum_bsonUnserialize, 0, 1, IS_VOID, 0)
8+
ZEND_ARG_TYPE_INFO(0, data, IS_ARRAY, 0)
9+
ZEND_END_ARG_INFO()
10+
11+
12+
ZEND_METHOD(MongoDB_BSON_PersistableEnum, bsonSerialize);
13+
ZEND_METHOD(MongoDB_BSON_PersistableEnum, bsonUnserialize);
14+
15+
16+
static const zend_function_entry class_MongoDB_BSON_PersistableEnum_methods[] = {
17+
ZEND_ME(MongoDB_BSON_PersistableEnum, bsonSerialize, arginfo_class_MongoDB_BSON_PersistableEnum_bsonSerialize, ZEND_ACC_PUBLIC|ZEND_ACC_FINAL)
18+
ZEND_ME(MongoDB_BSON_PersistableEnum, bsonUnserialize, arginfo_class_MongoDB_BSON_PersistableEnum_bsonUnserialize, ZEND_ACC_PUBLIC|ZEND_ACC_FINAL)
19+
ZEND_FE_END
20+
};
21+
22+
static zend_class_entry *register_class_MongoDB_BSON_PersistableEnum(void)
23+
{
24+
zend_class_entry ce, *class_entry;
25+
26+
INIT_NS_CLASS_ENTRY(ce, "MongoDB\\BSON", "PersistableEnum", class_MongoDB_BSON_PersistableEnum_methods);
27+
class_entry = zend_register_internal_class_ex(&ce, NULL);
28+
class_entry->ce_flags |= ZEND_ACC_TRAIT;
29+
30+
return class_entry;
31+
}

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 = NULL;
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(&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(&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);

src/phongo_classes.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,7 @@ extern zend_class_entry* php_phongo_bulkwriteexception_ce;
322322

323323
extern zend_class_entry* php_phongo_type_ce;
324324
extern zend_class_entry* php_phongo_persistable_ce;
325+
extern zend_class_entry* php_phongo_persistableenum_ce;
325326
extern zend_class_entry* php_phongo_unserializable_ce;
326327
extern zend_class_entry* php_phongo_serializable_ce;
327328
extern zend_class_entry* php_phongo_binary_ce;
@@ -373,6 +374,7 @@ extern void php_phongo_maxkey_init_ce(INIT_FUNC_ARGS);
373374
extern void php_phongo_minkey_init_ce(INIT_FUNC_ARGS);
374375
extern void php_phongo_objectid_init_ce(INIT_FUNC_ARGS);
375376
extern void php_phongo_persistable_init_ce(INIT_FUNC_ARGS);
377+
extern void php_phongo_persistableenum_init_ce(INIT_FUNC_ARGS);
376378
extern void php_phongo_regex_init_ce(INIT_FUNC_ARGS);
377379
extern void php_phongo_serializable_init_ce(INIT_FUNC_ARGS);
378380
extern void php_phongo_symbol_init_ce(INIT_FUNC_ARGS);

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===

0 commit comments

Comments
 (0)