Derived class defined later in the same file "does not exist"?
Asked Answered
L

2

18

Let’s suppose we’ve got two php files, a.php and b.php Here’s content of file a.php:

<?php // content of a.php
class A {
}

And here’s the content of file b.php

<?php  // content of b.php
include dirname(__FILE__) . "/a.php";
echo "A: ", class_exists("A") ? "exists" : "doesn’t exist", "\n";
echo "B: ", class_exists("B") ? "exists" : "doesn’t exist", "\n";
echo "BA (before): ", class_exists("BA") ? "exists" : "doesn’t exist", "\n";
echo "BB: ", class_exists("BB") ? "exists" : "doesn’t exist", "\n";
class B {
}
class BA extends A {
}
class BB extends B {
}
echo "BA (after): ", class_exists("BA") ? "exists" : "doesn’t exist", "\n";

If you launch the b.php script you have this output:

A: exists
B: exists
BA (before): doesn’t exist
BB: exists
BA (after): exists

Why does the BA class exist only after the class definition? And why does the other classes exist even before their definition? Which is the difference? I’d expect to have a common behavior in both cases... Is there a way I could use the BA class even before its definition?

Thank you

Michele

Leela answered 27/9, 2012 at 8:32 Comment(7)
What version of PHP are you using ?? Am not able to replicate this errorCrosseyed
See the classes abstraction here php.net/manual/en/language.oop5.abstract.phpRiggins
Very good question! Perhaps you should add that if class A is defined in the same file then BA (before) exists as well.Atreus
@Baba: My php version in 5.3.15Leela
@Jon: this is the reason for which I added the BB classLeela
@bodi0: I can't see anything related to my question in php.net/manual/en/language.oop5.abstract.phpLeela
@MicheleLocati: I just finished investigating and added an answer. Thank you very much for asking such an interesting question!Atreus
A
7

Disclaimer: I don't claim to understand the inner workings of Zend. The following is my interpretation of the PHP source, fueled in great part by educated guesses. Even though I am fully confident in the conclusion, the terminology or details might be off. I 'd love to hear from anyone with experience in Zend internals on the matter.

The investigation

From the PHP parser we can see that when a class declaration is encountered the zend_do_early_binding function is called. Here is the code that handles the declaration of derived classes:

case ZEND_DECLARE_INHERITED_CLASS:
{
    zend_op *fetch_class_opline = opline-1;
    zval *parent_name;
    zend_class_entry **pce;

    parent_name = &CONSTANT(fetch_class_opline->op2.constant);
    if ((zend_lookup_class(Z_STRVAL_P(parent_name), Z_STRLEN_P(parent_name), &pce TSRMLS_CC) == FAILURE) ||
        ((CG(compiler_options) & ZEND_COMPILE_IGNORE_INTERNAL_CLASSES) &&
         ((*pce)->type == ZEND_INTERNAL_CLASS))) {
        if (CG(compiler_options) & ZEND_COMPILE_DELAYED_BINDING) {
            zend_uint *opline_num = &CG(active_op_array)->early_binding;

            while (*opline_num != -1) {
                opline_num = &CG(active_op_array)->opcodes[*opline_num].result.opline_num;
            }
            *opline_num = opline - CG(active_op_array)->opcodes;
            opline->opcode = ZEND_DECLARE_INHERITED_CLASS_DELAYED;
            opline->result_type = IS_UNUSED;
            opline->result.opline_num = -1;
        }
        return;
    }
    if (do_bind_inherited_class(CG(active_op_array), opline, CG(class_table), *pce, 1 TSRMLS_CC) == NULL) {
        return;
    }
    /* clear unnecessary ZEND_FETCH_CLASS opcode */
    zend_del_literal(CG(active_op_array), fetch_class_opline->op2.constant);
    MAKE_NOP(fetch_class_opline);

    table = CG(class_table);
    break;
}

This code immediately calls zend_lookup_class to see if the parent class exists in the symbol table... and then diverges depending on whether the parent is found or not.

Let's first see what it does if the parent class is found:

if (do_bind_inherited_class(CG(active_op_array), opline, CG(class_table), *pce, 1 TSRMLS_CC) == NULL) {
    return;
}

Going over to do_bind_inherited_class, we see that the last argument (which in this call is 1) is called compile_time. This sounds interesting. What does it do with this argument?

if (compile_time) {
    op1 = &CONSTANT_EX(op_array, opline->op1.constant);
    op2 = &CONSTANT_EX(op_array, opline->op2.constant);
} else {
    op1 = opline->op1.zv;
    op2 = opline->op2.zv;
}

found_ce = zend_hash_quick_find(class_table, Z_STRVAL_P(op1), Z_STRLEN_P(op1), Z_HASH_P(op1), (void **) &pce);

if (found_ce == FAILURE) {
    if (!compile_time) {
        /* If we're in compile time, in practice, it's quite possible
         * that we'll never reach this class declaration at runtime,
         * so we shut up about it.  This allows the if (!defined('FOO')) { return; }
         * approach to work.
         */
        zend_error(E_COMPILE_ERROR, "Cannot redeclare class %s", Z_STRVAL_P(op2));
    }
    return NULL;
} else {
    ce = *pce;
}

Okay... so it reads the parent and derived class names either from a static (from the PHP user's perspective) or dynamic context, depending on the compile_time status. It then tries to find the class entry ("ce") in the class table, and if it is not found then... it returns without doing anything in compile time, but emits a fatal error at runtime.

This sounds enormously important. Let's go back to zend_do_early_binding. What does it do if the parent class is not found?

if (CG(compiler_options) & ZEND_COMPILE_DELAYED_BINDING) {
    zend_uint *opline_num = &CG(active_op_array)->early_binding;

    while (*opline_num != -1) {
        opline_num = &CG(active_op_array)->opcodes[*opline_num].result.opline_num;
    }
    *opline_num = opline - CG(active_op_array)->opcodes;
    opline->opcode = ZEND_DECLARE_INHERITED_CLASS_DELAYED;
    opline->result_type = IS_UNUSED;
    opline->result.opline_num = -1;
}
return;

It seems that it is generating opcodes that will trigger a call to do_bind_inherited_class again -- but this time, the value of compile_time will be 0 (false).

Finally, what about the implementation of the class_exists PHP function? Looking at the source shows this snippet:

found = zend_hash_find(EG(class_table), name, len+1, (void **) &ce);

Great! This class_table variable is the same class_table that gets involved in the do_bind_inherited_class call we saw earlier! So the return value of class_exists depends on whether an entry for the class has already been inserted into class_table by do_bind_inherited_class.

The conclusions

The Zend compiler does not act on include directives at compile time (even if the filename is hardcoded).

If it did, then there would be no reason to emit a class redeclaration fatal error based on the compile_time flag not being set; the error could be emitted unconditionally.

When the compiler encounters a derived class declaration where the base class has not been declared in the same script file, it pushes the act of registering the class in its internal data structures to runtime.

This is evident from the last code snippet above, which sets up a ZEND_DECLARE_INHERITED_CLASS_DELAYED opcode to register the class when the script is executed. At that point the compile_time flag will be false and behavior will be subtly different.

The return value of class_exists depends on whether the class has already been registered.

Since this happens in different ways at compile time and at run time, the behavior of class_exists is also different:

  • classes whose ancestors are all included in the same source file are registered at compile time; they exist and can be instantiated at any point in that script
  • classes which have an ancestor defined in another source file are registered at runtime; before the VM executes the opcodes that correspond to the class definition in the source these classes do not exist for all practical purposes (class_exists returns false, instantiating gives a fatal error)
Atreus answered 27/9, 2012 at 9:37 Comment(4)
Many thanks for your deep exploration. Anyway, when you say "The Zend compiler does not act on include directives at compile time", I asked myself why. Can we consider this a bug?Leela
@MicheleLocati: Not really IMHO -- the filename may be a variable, in which case it is not possible to include the file at compile time. So you 'd need two separate paths and you 'd have inconsistent behavior, not to mention the bugs that could arise from this change in behavior.Atreus
Yes, you're right. Not a bug... But the behavior for "final users" is still somehow silly. BTW, many thanks!Leela
With autoloading get_class() and instanciation will first trigger this one, before failing. Usually the autoloader should be able to load to class and both should show their expected behaviour even if the mssing classes are not included yet (because they included now). This works for classes/interfaces, that appear in implements- or extends-statements too. However, I like your explanation :)Balkan
C
1

This simply as to do with PHP handle class in included files include dirname(__FILE__) . "/a.php";

BB exists because it extends B which was define in the same file.

BA does not exist because PHP did not parse A online it is called

Both would work return the same result

Using class BA extends B

include dirname(__FILE__) . "/a.php";
echo "BA (before): ", class_exists("BA") ? "exists" : "doesn’t exist", "\n";
echo "BB: ", class_exists("BB") ? "exists" : "doesn’t exist", "\n";
class B {
}
class BA extends B {
}
class BB extends B {
}
echo "BA (after): ", class_exists("BA") ? "exists" : "doesn’t exist", "\n";

Or Defining class A and using class BA extends A

class A {
}
echo "<pre>";
echo "BA (before): ", class_exists("BA") ? "exists" : "doesn’t exist", "\n";
echo "BB: ", class_exists("BB") ? "exists" : "doesn’t exist", "\n";
class B {
}
class BA extends A {
}
class BB extends B {
}
echo "BA (after): ", class_exists("BA") ? "exists" : "doesn’t exist", "\n";

Output

BA (before): exists
BB: exists
BA (after): exists

Conclusion

FORM PHP DOC

When a file is included, the code it contains inherits the variable scope of the line on which the include occurs. Any variables available at that line in the calling file will be available within the called file, from that point forward. However, all functions and classes defined in the included file have the global scope.

I think extended classes are covered in what PHP doc says, this can be treated as BUG that needs to be corrected but for the main time include your class before you call or use them

Crosseyed answered 27/9, 2012 at 9:17 Comment(1)
This "BUG" is now 7 years old ;) I think we can safely say... classes with extends are never going to be free of the issue.Hazlett

© 2022 - 2024 — McMap. All rights reserved.