It may not be the best practice, but in PHP we are allowed to define classes at the bottom of files However,
$t = new Test();
$t->foo();
class Test extends FakeInvalidClass {
public function foo(){
echo "arrived in foo.";
}
}
Produces error message:
Fatal error: Class 'Test' not found in /mysite/test.php on line 4
That's odd... class 'Test' is defined at the bottom of the file, PHP should have failed because FakeInvalidClass
was not found, not Test
<?php
// test.php
class Test extends FakeInvalidClass {
public function foo(){
echo "arrived in foo.";
}
}
$t = new Test();
$t->foo();
Produces a more human readable error
Fatal error: Class 'FakeInvalidClass' not found in /mysite/test.php on line 4
For reference, this works just fine:
<?php
// test.php
$t = new Test();
$t->foo();
class Test {
public function foo(){
echo "arrived in foo.";
}
}
I think it'll all make sense to you when you see the opcodes Zend Engine generates for each example (it does for me anyway).
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 the FakeInvalidClass
(see the return :6
there). Since the latter is not defined, #13 fails and return to #1, which also fails because Test
is left undefined.
Both of them (#13 and #1) will call zend_error
(as evidenced in the PHP source), but zend_error
has global state (i.e.: errors are not stacked) so any subsequent call will overwrite the error message with the new one. So, in 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 a ZEND_FETCH_CLASS 'FakeInvalidClass'
code, but the class doesn't exist, so it returns a FakeInvalidClass not found
message, as it should.
Example 3:
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
8 12 EXT_STMT
13 NOP
13 14 > RETURN 1
Zend gets a ZEND_FETCH_CLASS 'Test'
code and execute normally.
This is explained by the fact that PHP will parse first-level classes it encounters in the code before it executes it. When you create a class definition that extends another class or instantiate an object, a ZEND_FETCH_CLASS
opcode will be inserted for that class at that point in the code. It's actually lazy-initialization.
This is also evidenced by the fact that this works perfectly:
<?php
exit;
class Test extends FakeInvalidClass {
public function foo(){
echo "arrived in foo.";
}
}
Conclusion:
The different error messages are explained by the different parameters to the ZEND_FETCH_CLASS
opcode.
Now, if you wonder why ZE generates opcodes like that, it's probably a design choice, it's probably easier to maintain. But to be honest, I have no idea.
The logical conclusion is that if you try to create an object of a class, it will continue to scan the rest of the file for this class declaration. If it fails to 'parse' the class (due to the fact that you extend an unexisting class), it will return to the line where you tried to create the object and tell you that the class doesn't exist.
So why not giving you an error when not finding the class that you extend?
Well, PHP doesn't know if you later on will include a file which would contain FakeInvalidClass
, and it would be wrong to say at line 4 that Fatal error: Class 'FakeInvalidClass' not found
.
Edit:
However, you can't include FakeInvalidClass
later on since Test
will already have been parsed. So, it still remains that PHP will give you the error on the line it fails to execute. But a more informative error message would have been to stack them:
Class 'Test' not found (Class 'FakeInvalidClass' not found in /mysite/test.php on line 8) in /mysite/test.php on line 4
But PHP doesn't stack errors.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With