get_defined_vars()
prints all variables in the "symbol table" of the scope where it is called. When you try to wrap it as debugAllVars
, you introduce a new scope, which has a new symbol table.
For a standalone function like this, the symbol table consists of:
- the function's parameters
- any global variables imported into the current scope using the
global
keyword
- any static variables declared in the current scope with the
static
keyword (even if not assigned a value)
- any variables implicitly declared by assigning a value to them
- any variables implicitly declared by taking a reference to them (e.g.
$foo = &$bar
would implicitly declare $bar
if not already defined; $foo = $bar
would not)
Notably, this list does not include the superglobals, such as $_GET
, $_POST
, and $GLOBALS
. If you run get_defined_vars()
in global scope (i.e. outside any function), you will see that these are present in the symbol table there, which is also what the magic variable $GLOBALS
points to. So, why are they not present in every scope, and how can we use them if they're not?
For this, we need to dig into the internals of the implementation, where these variables are referred to as "auto-globals" rather than "superglobals".
The answer to why is performance: the naive implementation of an "auto-global" would be one that acted as though every function automatically had a line at the top reading global $_GET, $_POST, ...;
. However, this would mean copying all those variables into the symbol table before every function was run, even if they weren't used.
So instead, these variables are special-cased in the compiler, while converting your PHP code into the internal "opcodes" used by the VM which executes the code.
Using a source code browser, we can see how this works.
The key function is zend_is_auto_global
in zend_compile.c
(taken from current master
, effectively PHP 7.2):
zend_bool zend_is_auto_global(zend_string *name) /* {{{ */
{
zend_auto_global *auto_global;
if ((auto_global = zend_hash_find_ptr(CG(auto_globals), name)) != NULL) {
if (auto_global->armed) {
auto_global->armed = auto_global->auto_global_callback(auto_global->name);
}
return 1;
}
return 0;
}
Here, name
is the name of a variable, and CG
means "compiler globals", so the main job of this function is to say "if the variable name given is in a compiler-global hash called auto_globals
, return 1". The additional call to auto_global_callback
allows the variable to be "lazy loaded", and only populated when it is first referenced.
The main usage of that function appears to be this conditional, in zend_compile_simple_var_no_cv
:
if (name_node.op_type == IS_CONST &&
zend_is_auto_global(Z_STR(name_node.u.constant))) {
opline->extended_value = ZEND_FETCH_GLOBAL;
} else {
opline->extended_value = ZEND_FETCH_LOCAL;
}
In other words, if the variable name you referenced is in the list of superglobals, the compiler switches the opcode into a different mode, so that when it is executed, it looks up the variable globally rather than locally.