Skip to content

PHPC-2083: Allow enums to be instantiated during BSON decoding #1317

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

Merged
merged 5 commits into from
Aug 26, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions config.m4
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ if test "$PHP_MONGODB" != "no"; then
src/BSON/ObjectId.c \
src/BSON/ObjectIdInterface.c \
src/BSON/Persistable.c \
src/BSON/PersistableEnum.c \
src/BSON/Regex.c \
src/BSON/RegexInterface.c \
src/BSON/Serializable.c \
Expand Down
2 changes: 1 addition & 1 deletion config.w32
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ if (PHP_MONGODB != "no") {

EXTENSION("mongodb", "php_phongo.c", null, PHP_MONGODB_CFLAGS);
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");
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");
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");
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");
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");
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");
Expand Down
1 change: 1 addition & 0 deletions php_phongo.c
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,7 @@ PHP_MINIT_FUNCTION(mongodb) /* {{{ */
php_phongo_minkey_init_ce(INIT_FUNC_ARGS_PASSTHRU);
php_phongo_objectid_init_ce(INIT_FUNC_ARGS_PASSTHRU);
php_phongo_persistable_init_ce(INIT_FUNC_ARGS_PASSTHRU);
php_phongo_persistableenum_init_ce(INIT_FUNC_ARGS_PASSTHRU);
php_phongo_regex_init_ce(INIT_FUNC_ARGS_PASSTHRU);
php_phongo_symbol_init_ce(INIT_FUNC_ARGS_PASSTHRU);
php_phongo_timestamp_init_ce(INIT_FUNC_ARGS_PASSTHRU);
Expand Down
49 changes: 49 additions & 0 deletions src/BSON/PersistableEnum.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* Copyright 2014-present MongoDB, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

#include <php.h>

#include "php_phongo.h"
#include "phongo_error.h"
#include "PersistableEnum_arginfo.h"

zend_class_entry* php_phongo_persistableenum_ce;

PHP_METHOD(MongoDB_BSON_PersistableEnum, bsonSerialize)
{
PHONGO_PARSE_PARAMETERS_NONE();

RETVAL_ZVAL(getThis(), 1, 0);
convert_to_array(return_value);

return;
}

PHP_METHOD(MongoDB_BSON_PersistableEnum, bsonUnserialize)
{
zval* data;

PHONGO_PARSE_PARAMETERS_START(1, 1)
Z_PARAM_ARRAY(data)
PHONGO_PARSE_PARAMETERS_END();

return;
} /* }}} */

void php_phongo_persistableenum_init_ce(INIT_FUNC_ARGS) /* {{{ */
{
php_phongo_persistableenum_ce = register_class_MongoDB_BSON_PersistableEnum();
} /* }}} */
15 changes: 15 additions & 0 deletions src/BSON/PersistableEnum.stub.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

/**
* @generate-class-entries static
* @generate-function-entries
*/

namespace MongoDB\BSON;

trait PersistableEnum
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seeing these changes makes me appreciate the work people put into gen_stub.php for PHP core - it's just so much easier to define classes now than it was before when everything had to be done manually...

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

{
public final function bsonSerialize(): array {}

public final function bsonUnserialize(array $data): void {}
}
31 changes: 31 additions & 0 deletions src/BSON/PersistableEnum_arginfo.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/* This is a generated file, edit the .stub.php file instead.
* Stub hash: db87873800da8dfaf4b3235eed97254593c9b83d */

ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_MongoDB_BSON_PersistableEnum_bsonSerialize, 0, 0, IS_ARRAY, 0)
ZEND_END_ARG_INFO()

ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_MongoDB_BSON_PersistableEnum_bsonUnserialize, 0, 1, IS_VOID, 0)
ZEND_ARG_TYPE_INFO(0, data, IS_ARRAY, 0)
ZEND_END_ARG_INFO()


ZEND_METHOD(MongoDB_BSON_PersistableEnum, bsonSerialize);
ZEND_METHOD(MongoDB_BSON_PersistableEnum, bsonUnserialize);


static const zend_function_entry class_MongoDB_BSON_PersistableEnum_methods[] = {
ZEND_ME(MongoDB_BSON_PersistableEnum, bsonSerialize, arginfo_class_MongoDB_BSON_PersistableEnum_bsonSerialize, ZEND_ACC_PUBLIC|ZEND_ACC_FINAL)
ZEND_ME(MongoDB_BSON_PersistableEnum, bsonUnserialize, arginfo_class_MongoDB_BSON_PersistableEnum_bsonUnserialize, ZEND_ACC_PUBLIC|ZEND_ACC_FINAL)
ZEND_FE_END
};

static zend_class_entry *register_class_MongoDB_BSON_PersistableEnum(void)
{
zend_class_entry ce, *class_entry;

INIT_NS_CLASS_ENTRY(ce, "MongoDB\\BSON", "PersistableEnum", class_MongoDB_BSON_PersistableEnum_methods);
class_entry = zend_register_internal_class_ex(&ce, NULL);
class_entry->ce_flags |= ZEND_ACC_TRAIT;

return class_entry;
}
134 changes: 130 additions & 4 deletions src/phongo_bson.c
Original file line number Diff line number Diff line change
Expand Up @@ -768,6 +768,55 @@ static void php_phongo_handle_field_path_entry_for_compound_type(php_phongo_bson
}
}

#if PHP_VERSION_ID >= 80100
/* Resolves an enum class and case name to a zval. On error, an exception will
* have been thrown and NULL will be returned.
*
* This function is modeled after php_var_unserialize_internal in php-src. */
static zval* resolve_enum_case(zend_class_entry* ce, const char* case_name)
{
zval* return_value = NULL;
zend_string* c_str = NULL;
zend_class_constant* c;

if (!(ce->ce_flags & ZEND_ACC_ENUM)) {
phongo_throw_exception(PHONGO_ERROR_UNEXPECTED_VALUE, "Class '%s' is not an enum", ZSTR_VAL(ce->name));
goto cleanup;
}

c_str = zend_string_init(case_name, strlen(case_name), 0);
c = zend_hash_find_ptr(CE_CONSTANTS_TABLE(ce), c_str);

if (!c) {
phongo_throw_exception(PHONGO_ERROR_UNEXPECTED_VALUE, "Undefined constant %s::%s", ZSTR_VAL(ce->name), case_name);
goto cleanup;
}

if (!(ZEND_CLASS_CONST_FLAGS(c) & ZEND_CLASS_CONST_IS_CASE)) {
phongo_throw_exception(PHONGO_ERROR_UNEXPECTED_VALUE, "%s::%s is not an enum case", ZSTR_VAL(ce->name), case_name);
goto cleanup;
}

if (Z_TYPE(c->value) == IS_CONSTANT_AST && zval_update_constant_ex(&c->value, ce) == FAILURE) {
phongo_throw_exception(PHONGO_ERROR_UNEXPECTED_VALUE, "Failed to evaluate constant expression AST for %s::%s", ZSTR_VAL(ce->name), case_name);
goto cleanup;
}

if (Z_TYPE(c->value) != IS_OBJECT) {
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));
}

return_value = &c->value;

cleanup:
if (c_str) {
zend_string_release_ex(c_str, 0);
}

return return_value;
}
#endif /* PHP_VERSION_ID >= 80100 */

static bool php_phongo_bson_visit_document(const bson_iter_t* iter ARG_UNUSED, const char* key, const bson_t* v_document, void* data) /* {{{ */
{
zval* retval = PHONGO_BSON_STATE_ZCHILD(data);
Expand Down Expand Up @@ -805,9 +854,51 @@ static bool php_phongo_bson_visit_document(const bson_iter_t* iter ARG_UNUSED, c
break;

case PHONGO_TYPEMAP_CLASS: {
zval obj;
zval obj;
zend_class_entry* obj_ce = state.odm ? state.odm : state.map.document;

#if PHP_VERSION_ID >= 80100
/* Enums require special handling for instantiation */
if (obj_ce->ce_flags & ZEND_ACC_ENUM) {
int plen;
zend_bool pfree;
char* case_name;
zval* enum_case;

case_name = php_array_fetchc_string(&state.zchild, "name", &plen, &pfree);

if (!case_name) {
phongo_throw_exception(PHONGO_ERROR_UNEXPECTED_VALUE, "Missing 'name' field to infer enum case for %s", ZSTR_VAL(obj_ce->name));

/* Clean up and return true to stop iteration for
* our parent context. */
zval_ptr_dtor(&state.zchild);
php_phongo_bson_state_dtor(&state);
return true;
}

enum_case = resolve_enum_case(obj_ce, case_name);

if (pfree) {
efree(case_name);
}

if (!enum_case) {
/* Exception already thrown. Clean up and return
* true to stop iteration for our parent context. */
zval_ptr_dtor(&state.zchild);
php_phongo_bson_state_dtor(&state);
return true;
}

ZVAL_COPY(&obj, enum_case);
} else {
object_init_ex(&obj, obj_ce);
}
#else /* PHP_VERSION_ID < 80100 */
object_init_ex(&obj, obj_ce);
#endif /* PHP_VERSION_ID */

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

case PHONGO_TYPEMAP_CLASS: {
zval obj;
zval obj;
zend_class_entry* obj_ce = state->odm ? state->odm : state->map.root;

#if PHP_VERSION_ID >= 80100
/* Enums require special handling for instantiation */
if (obj_ce->ce_flags & ZEND_ACC_ENUM) {
int plen;
zend_bool pfree;
char* case_name;
zval* enum_case;

case_name = php_array_fetchc_string(&state->zchild, "name", &plen, &pfree);

if (!case_name) {
phongo_throw_exception(PHONGO_ERROR_UNEXPECTED_VALUE, "Missing 'name' field to infer enum case for %s", ZSTR_VAL(obj_ce->name));

goto cleanup;
}

enum_case = resolve_enum_case(obj_ce, case_name);

if (pfree) {
efree(case_name);
}

if (!enum_case) {
/* Exception already thrown */
goto cleanup;
}

ZVAL_COPY(&obj, enum_case);
} else {
object_init_ex(&obj, obj_ce);
}
#else /* PHP_VERSION_ID < 80100 */
object_init_ex(&obj, obj_ce);
#endif /* PHP_VERSION_ID */

object_init_ex(&obj, state->odm ? state->odm : state->map.root);
zend_call_method_with_1_params(PHONGO_COMPAT_OBJ_P(&obj), NULL, NULL, BSON_UNSERIALIZE_FUNC_NAME, NULL, &state->zchild);
zval_ptr_dtor(&state->zchild);
ZVAL_COPY_VALUE(&state->zchild, &obj);
Expand Down
2 changes: 2 additions & 0 deletions src/phongo_classes.h
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,7 @@ extern zend_class_entry* php_phongo_bulkwriteexception_ce;

extern zend_class_entry* php_phongo_type_ce;
extern zend_class_entry* php_phongo_persistable_ce;
extern zend_class_entry* php_phongo_persistableenum_ce;
extern zend_class_entry* php_phongo_unserializable_ce;
extern zend_class_entry* php_phongo_serializable_ce;
extern zend_class_entry* php_phongo_binary_ce;
Expand Down Expand Up @@ -373,6 +374,7 @@ extern void php_phongo_maxkey_init_ce(INIT_FUNC_ARGS);
extern void php_phongo_minkey_init_ce(INIT_FUNC_ARGS);
extern void php_phongo_objectid_init_ce(INIT_FUNC_ARGS);
extern void php_phongo_persistable_init_ce(INIT_FUNC_ARGS);
extern void php_phongo_persistableenum_init_ce(INIT_FUNC_ARGS);
extern void php_phongo_regex_init_ce(INIT_FUNC_ARGS);
extern void php_phongo_serializable_init_ce(INIT_FUNC_ARGS);
extern void php_phongo_symbol_init_ce(INIT_FUNC_ARGS);
Expand Down
85 changes: 85 additions & 0 deletions tests/bson/bson-enum-001.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
--TEST--
Enums serialize as documents and are not unserialized by default
--SKIPIF--
<?php require __DIR__ . "/../utils/basic-skipif.inc"; ?>
<?php skip_if_php_version('<', '8.1.0'); ?>
--FILE--
<?php

require_once __DIR__ . '/../utils/basic.inc';

enum MyEnum
{
case foo;
}

enum MyBackedEnum: string
{
case foo = 'bar';
}

$tests = [
MyEnum::foo,
MyBackedEnum::foo,
['myEnum' => MyEnum::foo],
['myBackedEnum' => MyBackedEnum::foo],
];

foreach ($tests as $document) {
$bson = fromPHP($document);
echo "Test ", toJSON($bson), "\n";
hex_dump($bson);
var_dump(toPHP($bson));
echo "\n";
}

?>
===DONE===
<?php exit(0); ?>
--EXPECTF--
Test { "name" : "foo" }
0 : 13 00 00 00 02 6e 61 6d 65 00 04 00 00 00 66 6f [.....name.....fo]
10 : 6f 00 00 [o..]
object(stdClass)#%d (%d) {
["name"]=>
string(3) "foo"
}

Test { "name" : "foo", "value" : "bar" }
0 : 22 00 00 00 02 6e 61 6d 65 00 04 00 00 00 66 6f ["....name.....fo]
10 : 6f 00 02 76 61 6c 75 65 00 04 00 00 00 62 61 72 [o..value.....bar]
20 : 00 00 [..]
object(stdClass)#%d (%d) {
["name"]=>
string(3) "foo"
["value"]=>
string(3) "bar"
}

Test { "myEnum" : { "name" : "foo" } }
0 : 20 00 00 00 03 6d 79 45 6e 75 6d 00 13 00 00 00 [ ....myEnum.....]
10 : 02 6e 61 6d 65 00 04 00 00 00 66 6f 6f 00 00 00 [.name.....foo...]
object(stdClass)#%d (%d) {
["myEnum"]=>
object(stdClass)#%d (%d) {
["name"]=>
string(3) "foo"
}
}

Test { "myBackedEnum" : { "name" : "foo", "value" : "bar" } }
0 : 35 00 00 00 03 6d 79 42 61 63 6b 65 64 45 6e 75 [5....myBackedEnu]
10 : 6d 00 22 00 00 00 02 6e 61 6d 65 00 04 00 00 00 [m."....name.....]
20 : 66 6f 6f 00 02 76 61 6c 75 65 00 04 00 00 00 62 [foo..value.....b]
30 : 61 72 00 00 00 [ar...]
object(stdClass)#%d (%d) {
["myBackedEnum"]=>
object(stdClass)#%d (%d) {
["name"]=>
string(3) "foo"
["value"]=>
string(3) "bar"
}
}

===DONE===
Loading