The second parameter of php define is used

  • 2020-11-03 22:03:06
  • OfStack

The manual says that the constants defined by define are allowed only:
Only scalars and null are allowed. The types of scalars are integer, float, string, or boolean. It is also possible to define a constant value of type resource, but it is not recommended to do so, which can lead to unknown situations.
Reading the source code of php today, I found that the second parameter of define could also be an object.
First paste 1 example:

class A {
    public function __toString() {
        return 'bar';
    }
}

$a = new A();
define('foo', $a);
echo foo;
//  The output bar

Here's how define in php works:

ZEND_FUNCTION(define)
{
    char *name;
    int name_len;
    zval *val;
    zval *val_free = NULL;
    zend_bool non_cs = 0;
    int case_sensitive = CONST_CS;
    zend_constant c;

    //  receive 3 The parameters, string . zval . bool
    if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "sz|b", &name, &name_len, &val, &non_cs) == FAILURE) {
        return;
    }

    //  Case sensitive or not 
    if(non_cs) {
        case_sensitive = 0;
    }

    //  if define A class constant , Is an error 
    if (zend_memnstr(name, "::", sizeof("::") - 1, name + name_len)) {
        zend_error(E_WARNING, "Class constants cannot be defined or redefined");
        RETURN_FALSE;
    }

    //  To get the real value, use val save 
repeat:
    switch (Z_TYPE_P(val)) {
        case IS_LONG:
        case IS_DOUBLE:
        case IS_STRING:
        case IS_BOOL:
        case IS_RESOURCE:
        case IS_NULL:
            break;
        case IS_OBJECT:
            if (!val_free) {
                if (Z_OBJ_HT_P(val)->get) {
                    val_free = val = Z_OBJ_HT_P(val)->get(val TSRMLS_CC);
                    goto repeat;
                } else if (Z_OBJ_HT_P(val)->cast_object) {
                    ALLOC_INIT_ZVAL(val_free);
                    if (Z_OBJ_HT_P(val)->cast_object(val, val_free, IS_STRING TSRMLS_CC) == SUCCESS) {
                        val = val_free;
                        break;
                    }
                }
            }
            /* no break */
        default:
            zend_error(E_WARNING,"Constants may only evaluate to scalar values");
            if (val_free) {
                zval_ptr_dtor(&val_free);
            }
            RETURN_FALSE;
    }

    //  To build a constant 
    c.value = *val;
    zval_copy_ctor(&c.value);
    if (val_free) {
        zval_ptr_dtor(&val_free);
    }
    c.flags = case_sensitive; /* non persistent */
    c.name = zend_strndup(name, name_len);
    c.name_len = name_len+1;
    c.module_number = PHP_USER_CONSTANT;

    //  Registered constants 
    if (zend_register_constant(&c TSRMLS_CC) == SUCCESS) {
        RETURN_TRUE;
    } else {
        RETURN_FALSE;
    }
}

Note that the 1-paragraph loop that starts with repeat also USES the goto statement T_T
The function of this code is:
For int, float, string, bool, resource, null, the actual defined constants use these values directly
For object, you need to convert object to one of the above six types (if it is still object after the transformation, continue the transformation)
How do you break object into one of the six types? From a code perspective, there are two approaches:

if (Z_OBJ_HT_P(val)->get) {
    val_free = val = Z_OBJ_HT_P(val)->get(val TSRMLS_CC);
    goto repeat;
}
// __toString() Methods in cast_object In the called 
else if (Z_OBJ_HT_P(val)->cast_object) {
    ALLOC_INIT_ZVAL(val_free);
    if (Z_OBJ_HT_P(val)->cast_object(val, val_free, IS_STRING TSRMLS_CC) == SUCCESS)
    {
        val = val_free;
        break;
    }
}

1, Z_OBJ_HT_P (val) - > get, after macro expansion is (*val).value.obj.handlers - > get
2, Z_OBJ_HT_P (val) - > cast_object, after macro expansion is (*val).value.obj.handlers - > cast_object
handlers is a struct containing many function Pointers, as defined in _zend_object_handlers. The function Pointers in the structure are used to manipulate object, such as reading/modifying object properties, getting/calling object methods, and so on... get and cast_object are also 1 of them.
For 1-like objects, php provides the standard cast_object function zend_std_cast_object_tostring, code in ES87en-ES88en /zend/ zend-ES91en-ES92en.c:

ZEND_API int zend_std_cast_object_tostring(zval *readobj, zval *writeobj, int type TSRMLS_DC) /* {{{ */
{
    zval *retval;
    zend_class_entry *ce;

    switch (type) {
        case IS_STRING:
            ce = Z_OBJCE_P(readobj);

            //  If the user's class Defined in the __toString, Then try to call 
            if (ce->__tostring &&
                (zend_call_method_with_0_params(&readobj, ce, &ce->__tostring, "__tostring", &retval) || EG(exception))) {
                 ... 

            }
            return FAILURE;
         ... 
    }
    return FAILURE;
}

With the concrete implementation described above, the default cast_object is to find the method in class with SUCCtostring and call...
Back to the original example, define(' foo', $a). Since $a is an instance of A and each succtoString is defined in class A, the foo constant is essentially the return value of toString, bar.

Related articles: