Skip to content

Auto-implement Stringable for string backed enums #8825

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

Closed
wants to merge 3 commits into from
Closed
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
17 changes: 17 additions & 0 deletions Zend/tests/enum/__toString.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
--TEST--
Enum __toString
--FILE--
<?php

enum Foo {
case Bar;

public function __toString()
{
return '__toString';
}
}

?>
--EXPECTF--
Fatal error: Enum may not include __toString in %s on line %d
17 changes: 17 additions & 0 deletions Zend/tests/enum/backed-int-__toString.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
--TEST--
Backed int enum __toString is forbidden
--FILE--
<?php

enum Foo: int {
case Bar = 0;

public function __toString()
{
return '__toString';
}
}

?>
--EXPECTF--
Fatal error: Enum may not include __toString in %s on line %d
17 changes: 17 additions & 0 deletions Zend/tests/enum/backed-string-__toString.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
--TEST--
Backed string enum __toString
--FILE--
<?php

enum Foo: string {
case Bar = 'bar';

public function __toString()
{
return '__toString';
}
}

?>
--EXPECTF--
Fatal error: Cannot redeclare Foo::__toString() in %s on line %d
26 changes: 26 additions & 0 deletions Zend/tests/enum/backed-stringable.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
--TEST--
Backed string enum auto-implement Stringable
--FILE--
<?php

enum Foo: string {
case Bar = 'Baz';
}

function print_(string $value) {
echo $value, "\n";
}

echo (string) Foo::Bar, "\n";
echo Foo::Bar, "\n";
echo Foo::Bar . "\n";
print_(Foo::Bar);
var_dump(Foo::Bar instanceof Stringable);

?>
--EXPECT--
Baz
Baz
Baz
Baz
bool(true)
47 changes: 46 additions & 1 deletion Zend/zend_enum.c
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
#include "zend_API.h"
#include "zend_compile.h"
#include "zend_enum_arginfo.h"
#include "zend_interfaces_arginfo.h"
Copy link
Member Author

Choose a reason for hiding this comment

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

@kocsismate I'm importing this to have access to arginfo_class_Stringable___toString but this generates a bunch of "defined but not used" warnings. How would you fix those? Split the stubs?

Copy link
Member

Choose a reason for hiding this comment

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

Yes, arginfo.h files should only be imported once, so splitting the required parts would make sense.

Copy link
Member Author

Choose a reason for hiding this comment

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

@kocsismate Note: The stringable stubs would still be imported twice, but there wouldn't be any other symbols included twice and not used in this case. Is that ok? The problem is, I can't declare the function in an enum interface since __toString is only generated for string enums. And I want to avoid creating another interface.

#include "zend_interfaces.h"
#include "zend_enum.h"

Expand Down Expand Up @@ -81,11 +82,14 @@ static void zend_verify_enum_magic_methods(zend_class_entry *ce)
ZEND_ENUM_DISALLOW_MAGIC_METHOD(__set, "__set");
ZEND_ENUM_DISALLOW_MAGIC_METHOD(__unset, "__unset");
ZEND_ENUM_DISALLOW_MAGIC_METHOD(__isset, "__isset");
ZEND_ENUM_DISALLOW_MAGIC_METHOD(__tostring, "__toString");
ZEND_ENUM_DISALLOW_MAGIC_METHOD(__debugInfo, "__debugInfo");
ZEND_ENUM_DISALLOW_MAGIC_METHOD(__serialize, "__serialize");
ZEND_ENUM_DISALLOW_MAGIC_METHOD(__unserialize, "__unserialize");

if (ce->enum_backing_type != IS_STRING) {
ZEND_ENUM_DISALLOW_MAGIC_METHOD(__tostring, "__toString");
}

const char *forbidden_methods[] = {
"__sleep",
"__wakeup",
Expand Down Expand Up @@ -169,6 +173,10 @@ void zend_enum_add_interfaces(zend_class_entry *ce)
ce->num_interfaces++;
if (ce->enum_backing_type != IS_UNDEF) {
ce->num_interfaces++;

if (ce->enum_backing_type == IS_STRING) {
ce->num_interfaces++;
}
}

ZEND_ASSERT(!(ce->ce_flags & ZEND_ACC_RESOLVED_INTERFACES));
Expand All @@ -181,6 +189,11 @@ void zend_enum_add_interfaces(zend_class_entry *ce)
if (ce->enum_backing_type != IS_UNDEF) {
ce->interface_names[num_interfaces_before + 1].name = zend_string_copy(zend_ce_backed_enum->name);
ce->interface_names[num_interfaces_before + 1].lc_name = zend_string_init("backedenum", sizeof("backedenum") - 1, 0);

if (ce->enum_backing_type == IS_STRING) {
ce->interface_names[num_interfaces_before + 2].name = zend_string_copy(zend_ce_stringable->name);
ce->interface_names[num_interfaces_before + 2].lc_name = zend_string_init("stringable", sizeof("stringable") - 1, 0);
}
}
}

Expand Down Expand Up @@ -394,6 +407,16 @@ static ZEND_NAMED_FUNCTION(zend_enum_try_from_func)
zend_enum_from_base(INTERNAL_FUNCTION_PARAM_PASSTHRU, 1);
}

static ZEND_NAMED_FUNCTION(zend_enum_tostring_func)
{
ZEND_PARSE_PARAMETERS_NONE();

zval *value = zend_enum_fetch_case_value(Z_OBJ_P(ZEND_THIS));
ZEND_ASSERT(Z_TYPE_P(value) == IS_STRING);

RETURN_COPY(value);
}

void zend_enum_register_funcs(zend_class_entry *ce)
{
const uint32_t fn_flags =
Expand Down Expand Up @@ -447,6 +470,28 @@ void zend_enum_register_funcs(zend_class_entry *ce)
zend_error_noreturn(E_COMPILE_ERROR,
"Cannot redeclare %s::tryFrom()", ZSTR_VAL(ce->name));
}

if (ce->enum_backing_type == IS_STRING) {
zend_internal_function *tostring_function =
zend_arena_alloc(&CG(arena), sizeof(zend_internal_function));
memset(tostring_function, 0, sizeof(zend_internal_function));
tostring_function->type = ZEND_INTERNAL_FUNCTION;
tostring_function->module = EG(current_module);
tostring_function->handler = zend_enum_tostring_func;
tostring_function->function_name = ZSTR_KNOWN(ZEND_STR_TOSTRING);
tostring_function->scope = ce;
tostring_function->fn_flags = ZEND_ACC_PUBLIC|ZEND_ACC_HAS_RETURN_TYPE|ZEND_ACC_ARENA_ALLOCATED;
tostring_function->num_args = 0;
tostring_function->required_num_args = 0;
tostring_function->arg_info = (zend_internal_arg_info *) (arginfo_class_Stringable___toString + 1);
if (!zend_hash_add_ptr(
&ce->function_table, ZSTR_KNOWN(ZEND_STR_TOSTRING_LOWERCASE), tostring_function)) {
zend_error_noreturn(E_COMPILE_ERROR,
"Cannot redeclare %s::__toString()", ZSTR_VAL(ce->name));
}

ce->__tostring = (zend_function *) tostring_function;
}
}
}

Expand Down
2 changes: 2 additions & 0 deletions Zend/zend_string.h
Original file line number Diff line number Diff line change
Expand Up @@ -592,6 +592,8 @@ EMPTY_SWITCH_DEFAULT_CASE()
_(ZEND_STR_AUTOGLOBAL_ENV, "_ENV") \
_(ZEND_STR_AUTOGLOBAL_REQUEST, "_REQUEST") \
_(ZEND_STR_COUNT, "count") \
_(ZEND_STR_TOSTRING, "__toString") \
_(ZEND_STR_TOSTRING_LOWERCASE, "__tostring") \


typedef enum _zend_known_string_id {
Expand Down