Class Constant Visibility Modifiers #1494

Closed
wants to merge 1 commit into
from

Projects

None yet
@Sean-Der
Contributor

@reeze created an RFC here and I emailed internals here and didn't get any responses positive/negative.

This is a WIP and has test fails, and the API has not been finalized. Maybe it should be changed to mirror properties more, but this would cause more breakage.

@marcioAlmada
Contributor

@reeze created an RFC here and I emailed internals here and didn't get any responses positive/negative.

Hi, I'm sorry that your request for comments had no replies. Anyway, you should also add your RFC to this list, some people discover new proposals through it.

@Sean-Der
Contributor

@marcioAlmada thanks for linking that!

When I hear back from @reeze and hear if he is ok with me adopting/working with him on the RFC I will add it to that page (or I will have to start my own)

@reeze
Contributor
reeze commented Aug 30, 2015

Hi @Sean-Der Thanks for your work. I didn't finish my patch but start to working on other things. You are welcome to working on the RFC.

@laruence laruence added the RFC label Aug 30, 2015
@Sean-Der
Contributor

Thanks for the quick response @reeze are you interested in working with me on it? I totally understand if you are busy/not interested anymore, I just want to make sure I am not stomping on your work/ideas.

thanks!

@reeze
Contributor
reeze commented Aug 31, 2015

Hi @Sean-Der Go ahead ;-) I will test your patch and try to contribute to your branch once I have time.

@c9s
Contributor
c9s commented Sep 24, 2015

One question for the RFC: why the syntax used in interface is different in class? could we make it consistent?

I saw the constant defined in interface use protected:

interface ICache {
    protected const PROTECTED = 0;
}

but the constants defined in class are just:

class Foo {
    protected FOO = 0;
    public BAR = 10;
}
@staabm
Contributor
staabm commented Sep 24, 2015

@c9s your 2nd example is not valid syntax, see https://3v4l.org/ST9Gp

@c9s
Contributor
c9s commented Sep 24, 2015

@staabm The code was copied from RFC and it's only in RFC. (this one https://wiki.php.net/rfc/class_const_visibility )

@c9s
Contributor
c9s commented Sep 24, 2015

I think changing the code below (The RFC one):

class Token {
    // Constants default to public
    const PUBLIC_CONST = 0;

        // Constants then also can have a defined visibility
        private PRIVATE_CONST = 0;
        protected PROTECTED_CONST = 0;
        public PUBLIC_CONST_TWO = 0;
}

to this one is better:

class Token {
    // Constants default to public
    const PUBLIC_CONST = 0;

        // Constants then also can have a defined visibility
        private const  PRIVATE_CONST = 0;
        protected const  PROTECTED_CONST = 0;
        public const  PUBLIC_CONST_TWO = 0;
}
@HallofFamer

I dont think its necessary. Constants are supposed to be public anyway, unless you mean read-only fields, which can be private or protected.

@c9s
Contributor
c9s commented Sep 24, 2015

So, is this going to be merged or not?

One more question:

If a constant is defined as protected constant in one interface, is it available to classes implemented with the interface?

@marcioAlmada
Contributor

@Sean-Der @reeze

I believe the inconsistent examples in the RFC are just an oversight. It indeed should be:

class { private const  PRIVATE_CONST = 0; } // valid
class { private PRIVATE_CONST = 0; } // invalid!

@c9s

So, is this going to be merged or not?

For major language changes we have a voting phase. You can watch the votings through http://php-rfc-watch.beberlei.de

@Sean-Der
Contributor

Hey @marcioAlmada @c9s yep you caught a typo! Pretty embarrassing for such a light RFC, thank you 👍

@HallofFamer In PHP I don't think you can have read-only fields (properties?) now. There are a couple of use cases for a readonly member of a class constant, check out the conversation so far about this here

@Furgas
Contributor
Furgas commented Oct 8, 2015

Please consider adding getDocComment method to ReflectionClassConstant.

@marcioAlmada
Contributor

@Furgas usually RFCs are hard work and authors tend to add reflection API after the discussion or voting phase, when they already know it's worth committing more time to it.

@Sean-Der
Contributor

Thanks for sticking up for me @marcioAlmada appreciate it, OpenSource is usually less than welcoming.

Thanks @Furgas for being interested in this RFC! getDocComment is now implemented by 47bdc4a

@marcioAlmada
Contributor

What will be your stance regarding ReflectionClass and it's public API? Currently:

  • ReflectionClass::getConstants returns an array of constants [name => value]
  • ReflectionClass::getConstant returns the value of the constant

You're changing this behavior, don't you? The "RFC impact" section must be explicit about it :)

@Sean-Der
Contributor

@marcioAlmada Yep those should be changed as well!

I will make those changes and update the RFC, have plenty of failing tests also. Trying to hit the bare minimum to just get the RFC approved. Thanks for looking it all over!

thanks!

@nikic nikic and 1 other commented on an outdated diff Oct 21, 2015
Zend/zend_API.c
+ const_info = pemalloc(sizeof(zend_class_constant_info), 1);
+ } else {
+ const_info = zend_arena_alloc(&CG(arena), sizeof(zend_class_constant_info));
+ }
+
+ const_info->offset = ce->constants_count++;
+ const_info->name = zend_string_copy(name);
+ const_info->name = zend_new_interned_string(const_info->name);
+ const_info->flags = access_type;
+ const_info->doc_comment = doc_comment;
+ const_info->ce = ce;
+
+ ce->constants_table = perealloc(ce->constants_table, sizeof(zval) * ce->constants_count, ce->type == ZEND_INTERNAL_CLASS);
+ zend_hash_add_ptr(&ce->constants_info, name, const_info);
+ } else {
+ i_zval_ptr_dtor(&ce->constants_table[const_info->offset] ZEND_FILE_LINE_CC);
@nikic
nikic Oct 21, 2015 Contributor

Shouldn't this error instead?

@Sean-Der
Sean-Der Oct 21, 2015 Contributor

The old behavior was to just update the value of the constant if it already existed, and some extensions in tree now depend on it (Date being one of them)

I thought better to keep the existing behavior, because maybe extensions outside php-src depend on this?

@nikic nikic commented on an outdated diff Oct 21, 2015
Zend/zend_compile.c
- if (CG(compiler_options) & ZEND_COMPILE_NO_PERSISTENT_CONSTANT_SUBSTITUTION) {
- return 0;
- }
+ if (!zend_verify_const_access(const_info, EG(current_execute_data) ? EG(scope) : CG(active_class_entry))) {
@nikic
nikic Oct 21, 2015 Contributor

Given this code is in zend_compile, should this always use CG(active_class_entry)?

@nikic nikic and 1 other commented on an outdated diff Oct 21, 2015
Zend/zend_API.c
{
+ zend_class_constant_info *const_info;
+
+ if (zend_string_equals_literal_ci(name, "class")) {
+ zend_error(E_CORE_ERROR,
+ "A class constant must not be called 'class'; it is reserved for class name fetching");
@nikic
nikic Oct 21, 2015 Contributor

Note: Moving the check in here changes the error from E_COMPILE_ERROR to E_CORE_ERROR. I'd probably choose between these to depending on whether it's an internal class or not.

@Sean-Der
Sean-Der Oct 21, 2015 Contributor

Fixed by 544ba66

@nikic nikic commented on the diff Oct 21, 2015
Zend/zend_compile.h
@@ -296,6 +296,18 @@ typedef struct _zend_property_info {
#define OBJ_PROP_TO_NUM(offset) \
((offset - OBJ_PROP_TO_OFFSET(0)) / sizeof(zval))
+typedef struct _zend_class_constant_info {
+ uint32_t offset; /* offset for values in (zend_class_entry*)->constants_table */
+ uint32_t flags;
+ zend_string *name;
+ zend_string *doc_comment;
+ zend_class_entry *ce;
+} zend_class_constant_info;
+
+
+#define OBJ_CONST_NUM(obj, num) \
+ (&(obj)->constants_table[(num)])
@nikic
nikic Oct 21, 2015 Contributor

As this isn't working on an object, OBJ_ is a bit of a misnomer. As this doesn't need any special treatment I'd just inline the macro.

@Sean-Der
Sean-Der Oct 23, 2015 Contributor

This is just copied from OBJ_PROP_NUM in the same file. Should the other macro be inlined as well (maybe not on this branch, but just want to understand the difference?)

@nikic nikic commented on an outdated diff Oct 21, 2015
Zend/zend_constants.c
@@ -330,6 +343,14 @@ ZEND_API zval *zend_get_constant_ex(zend_string *cname, zend_class_entry *scope,
}
}
+ if (!current_scope) {
+ if (EG(current_execute_data)) {
@nikic
nikic Oct 21, 2015 Contributor

Not sure, but probably current_execute_data will be set during compilation as well. Better check in_compilation?

@bwoebi bwoebi commented on the diff Oct 21, 2015
Zend/zend_API.c
@@ -3723,58 +3724,151 @@ ZEND_API int zend_declare_property_stringl(zend_class_entry *ce, const char *nam
}
/* }}} */
-ZEND_API int zend_declare_class_constant(zend_class_entry *ce, const char *name, size_t name_length, zval *value) /* {{{ */
+ZEND_API int zend_declare_class_constant_ex(zend_class_entry *ce, zend_string *name, zval *value, int access_type, zend_string *doc_comment) /* {{{ */
@bwoebi
bwoebi Oct 21, 2015 Contributor

The RFC says we are mirroring method visibilities in interfaces. As far as I see, this method does not prohibit private and protected visibilities for class constants in interfaces… I guess the check was just missed … copied from zend_begin_method_decl():

    if (ce->ce_flags & ZEND_ACC_INTERFACE) {
        if (access_type != ZEND_ACC_PUBLIC) {
            zend_error_noreturn(E_COMPILE_ERROR, "Access type for interface constant %s::%s must be omitted", ZSTR_VAL(ce->name), ZSTR_VAL(name));
        }
    }
@Danack
Danack Oct 21, 2015 Contributor

The RFC specifically says that protected and public constants in an interface are allowed.

//Interfaces only support protected/public const, and a compile time error will be throw for privates
interface ICache {
        public const PUBLIC = 0;
    protected const PROTECTED = 0;

I've asked on list for it to be clarified. Most people seem to be against the idea of protected constants. So it would be better to explicitly exclude that from the RFC, to avoid someone adding it as a 'bug-fix' later.

@Sean-Der
Sean-Der Oct 21, 2015 Contributor

@bwoebi @Danack

This is fixed in the RFC! I will be pushing more commits to fix the (many) issues people have found so far. Adding the ZEND_ACC_PUBLIC check for interfaces.

@Sean-Der
Sean-Der Oct 21, 2015 Contributor

Fixed by 4c98124

@nikic nikic commented on the diff Oct 21, 2015
Zend/zend_inheritance.c
- zval *old_constant;
-
- if ((old_constant = zend_hash_find(child_constants_table, name)) != NULL) {
- if (!Z_ISREF_P(old_constant) ||
- !Z_ISREF_P(parent_constant) ||
- Z_REFVAL_P(old_constant) != Z_REFVAL_P(parent_constant)) {
- zend_error_noreturn(E_COMPILE_ERROR, "Cannot inherit previously-inherited or override constant %s from interface %s", ZSTR_VAL(name), ZSTR_VAL(iface->name));
- }
+ zend_class_constant_info *old_const_info;
+
+ if ((old_const_info = zend_hash_find_ptr(child_constants_table, name)) != NULL) {
+ if ((old_const_info != parent_constant) &&
+ (OBJ_CONST_NUM(old_const_info->ce, old_const_info->offset) !=
+ OBJ_CONST_NUM(parent_constant->ce, parent_constant->offset))) {
+ zend_error_noreturn(E_COMPILE_ERROR, "Cannot inherit previously-inherited or override constant %s from interface %s", ZSTR_VAL(name), ZSTR_VAL(iface->name));
+ }
@nikic
nikic Oct 21, 2015 Contributor

Do we need a check to forbid constants from going public -> protected during inheritance?

Also, I feel like something is missing in the inheritance code to manage constant_table. Where is it resized for more constants?

@nikic
nikic Oct 21, 2015 Contributor

Also, won't private constants be inherited as well? Even if I'm wrong, a testcase like this would be nice:

<?php

class A {
    private const X = 1;
    public static function getX() { return B::X; }
}
class B extends A {}
A::getX();

This should throw an error.

@Sean-Der
Sean-Der Oct 21, 2015 Contributor

Adding that test cases + fixing, thanks for catching both issue!

Both should be fixed by 0c250e2

@nikic
nikic Oct 21, 2015 Contributor

Allowing a public -> protected change violates LSP. We have this check in place for methods: https://3v4l.org/5adLa

@nikic nikic and 1 other commented on an outdated diff Oct 21, 2015
Zend/zend_vm_execute.h
@@ -5935,7 +5936,13 @@ static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_FETCH_CONSTANT_SPEC_CONST_CONS
}
}
- if (EXPECTED((value = zend_hash_find(&ce->constants_table, Z_STR_P(EX_CONSTANT(opline->op2)))) != NULL)) {
@nikic
nikic Oct 21, 2015 Contributor

You're directly modifying zend_vm_execute here. You should change zend_vm_def instead and regenerate using zend_vm_gen.php

@Sean-Der
Sean-Der Oct 21, 2015 Contributor

Fixed by ef9ad8f

@Majkl578
Contributor

Hi,
although I like the idea to have private/protected constants, I think the RFC should be rejected in current state, because the changes to Reflection API are a very serious BC break that will break a lot of user-land code. 😟

Also, there seems to be a conflict in trait resolution for constants and methods with same name (based on an example from http://news.php.net/php.internals/88906). What will this code do?

trait TestTrait {
    const foo = 'const';
    function foo() { echo 'method'; }
}

class TestClass {
    use TestTrait { foo as bar; }
}
@Sean-Der
Contributor

@Majkl578 that example was wrong (it was mine) sorry please ignore the trait stuff.

I am sorry if the Reflection changes cause any issues for you! However, if the change was not made we would be missing out on important information (flags + docComments) It will be a while before this code ends up on production machines though (7.1 is a long time away)

@marcioAlmada
Contributor

Hi!

@Majkl578

Also, there seems to be a conflict in trait resolution for constants and methods with same name (based on an example from http://news.php.net/php.internals/88906). What will this code do?

First of all: currently, traits cannot have constants so the problem doesn't even exists https://3v4l.org/gCvYH.

Even if we add constants to traits one day (and I hope this day never comes), we could have:

trait TestTrait {
    const foo = 'const';
    function foo() { echo 'method'; }
}

class TestClass {
    use TestTrait {
        const foo as bar;
        foo as bar;
    }
}

I don't see any issue here 😉

Regarding the Reflection API break, @nikic proposed an alternative. Instead of changing the return values of existing methods, we could just include ReflectionClass::getReflectionConstants() and ReflectionClass::getReflectionConstant($constant_name).

Seems reasonable, even though I prefer the BC break.

@Sean-Der
Contributor

@marcioAlmada I prefer the BC break as well.

My rationale is

  • getConstant/getConstants is not something that is going to be very useful to applications (Reflection itself always for me has been great for testing/introspection not making stuff happen at runtime)
  • The fix it easy, just call getValue() and you are done
  • That is some pretty annoying baggage to take on

But always happy to take to a vote :)

@Sean-Der Sean-Der closed this Oct 23, 2015
@Sean-Der Sean-Der reopened this Oct 23, 2015
@laruence
Member

I think it's better if you can squash all you changes into one commit... , there are lots of commits now

@Sean-Der
Contributor

@laruence done!

Are you able to reproduce those test failures on a ZTS build?

My CLI SAPI returns

PHP 7.1.0-dev (cli) (built: Oct 23 2015 00:20:56) ( ZTS DEBUG )
Copyright (c) 1997-2015 The PHP Group 
Zend Engine v3.1.0-dev, Copyright (c) 1998-2015 Zend Technologies

But make test doesn't get any of the same segfaults, and valgrind shows no invalid frees/reads?

@nikic
Contributor
nikic commented Oct 23, 2015

@Sean-Der The debug build on Travis also enabled opcache. That's likely where the issue is.

@Majkl578
Contributor

Thanks for the clarification about traits, that's not an issue for now.

I understand your reasons for breaking compatibility in relfection, but this is an excellent example of a change that becomes a nighmare for projects that need to keep compatibility with multiple versions of PHP.

And since this RFC targets 7.1, it should also be noted that it violates PHP's release process, which prohibits userland API BC breaks in x.y+1.z versions.

@Sean-Der
Contributor
Sean-Der commented Nov 1, 2015

@Majkl578 That is a great point, what do you think of not breaking the old API at all and doing @marcioAlmada's suggestion?

Adding two new subroutines getReflectionConstant and getReflectionConstants and keeping getConstant and getConstants unchanged.

@Sean-Der
Contributor
Sean-Der commented Nov 2, 2015

And done!

I implemented @marcioAlmada's suggestion, and no more BC in this commit anymore.

AFAIK this is ready for review (and hopefully merge) if anyone has any suggestions/guidance on better tests or who to bother to get the merge process going would love to hear!

@Sean-Der
Contributor
Sean-Der commented Dec 3, 2015

@dstogov Hey! Would you mind reviewing the opcache changes, the entire test suite does pass though.

thanks

@dstogov dstogov commented on the diff Dec 3, 2015
Zend/zend.h
HashTable function_table;
HashTable properties_info;
- HashTable constants_table;
+ HashTable constants_info;
@dstogov
dstogov Dec 3, 2015 Contributor

I think, it's going to be more efficient to introduce zend_class_constant structure and keep such structures in HashTable constants_table. We don't need separate "info" and "table" because, in opposite to properties, constants can't be changed in run-time.

@Sean-Der
Sean-Der Dec 3, 2015 Contributor

Lucky to get a review out of you! Thanks for taking a careful look at this, and keeping php-src in top shape :)

If there is anything I can do code wise, just tell me. Would love to stay involved, but it is more efficient for you to work alone.

thanks

@dstogov
Contributor
dstogov commented Dec 4, 2015

Please review another PR based on this one #1662

@laruence
Member
laruence commented Dec 5, 2015

close this, focus on #1662 instead

@laruence laruence closed this Dec 5, 2015
@Sean-Der Sean-Der deleted the Sean-Der:bug-69980-class-constants branch Jan 2, 2016
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment