I think this will make sense to you when you see the Zend Engine opcodes for each example (this is all the same to me).
Example 1:
compiled vars: !0 = $t line # * op fetch ext return operands --------------------------------------------------------------------------------- 4 0 > EXT_STMT 1 ZEND_FETCH_CLASS :0 'Test' 2 EXT_FCALL_BEGIN 3 NEW $1 :0 4 DO_FCALL_BY_NAME 0 5 EXT_FCALL_END 6 ASSIGN !0, $1 6 7 EXT_STMT 8 ZEND_INIT_METHOD_CALL !0, 'foo' 9 EXT_FCALL_BEGIN 10 DO_FCALL_BY_NAME 0 11 EXT_FCALL_END 7 12 EXT_STMT 13 ZEND_FETCH_CLASS :6 'FakeInvalidClass' 14 ZEND_DECLARE_INHERITED_CLASS $7 '%00test%2Fhome%2Fflacroix%2Ftest.php0x7f756fea4055', 'test' 12 15 > RETURN 1
As you can see, # 1 gets the Test class, which in turn goes to # 13 to get a FakeInvalidClass (see return :6 there). Since the latter is undefined, # 13 fails and returns to # 1, which also fails, because Test remains undefined.
Both of them (# 13 and # 1) will zend_error (as shown in the PHP source), but zend_error has a global state (that is: errors are not stacked), so any subsequent call will overwrite the error message with a new one. So in the pseudo code:
ZEND_FETCH_CLASS('Test') ZEND_FETCH_CLASS('FakeInvalidClass') zend_error('Class FakeInvalidClass not found') return zend_error('Class Test not found') return
Example 2:
compiled vars: !0 = $t line # * op fetch ext return operands --------------------------------------------------------------------------------- 4 0 > EXT_STMT 1 ZEND_FETCH_CLASS :0 'FakeInvalidClass' 2 ZEND_DECLARE_INHERITED_CLASS $1 '%00test%2Fhome%2Fflacroix%2Ftest2.php0x7fe2c1461038', 'test' 10 3 EXT_STMT 4 ZEND_FETCH_CLASS :2 'Test' 5 EXT_FCALL_BEGIN 6 NEW $3 :2 7 DO_FCALL_BY_NAME 0 8 EXT_FCALL_END 9 ASSIGN !0, $3 12 10 EXT_STMT 11 ZEND_INIT_METHOD_CALL !0, 'foo' 12 EXT_FCALL_BEGIN 13 DO_FCALL_BY_NAME 0 14 EXT_FCALL_END 13 15 > RETURN 1
Here # 1 is the code ZEND_FETCH_CLASS 'FakeInvalidClass' , but the class does not exist, so it returns the message FakeInvalidClass not found , as it should be.
Example 3:
compiled vars: !0 = $t line
Zend receives the code ZEND_FETCH_CLASS 'Test' and runs normally.
This is because PHP will analyze the first-level classes that it encounters in the code before it executes it. When you create a class definition that extends another class or instantiates an object, the operation code ZEND_FETCH_CLASS will be inserted for that class at this point in the code. This is actually lazy initialization.
This is also evidenced by the fact that this works great:
<?php exit; class Test extends FakeInvalidClass { public function foo(){ echo "arrived in foo."; } }
Output:
Different error messages are explained by different parameters of the operation code ZEND_FETCH_CLASS .
Now, if you are wondering why ZE generates such operation codes, this is probably the design choice, it is probably easier to maintain. But honestly, I have no idea.