I research about class compilation, it's sequence, and logic.
If I declare a class before a simple parent:
class First extends Second{}
class Second{}
This will work OK. See live example across PHP versions.
But if the parent class also has some not-yet-declared parents (extends or implements), as in this example:
class First extends Second{}
class Second extends Third{}
class Third{}
I will have an error:
Fatal error: Class 'Second' not found ...
See live example across PHP versions.
So, why in the second example it can't find Second
class?
Maybe php can't compile this class because it need also to compile Third
class, or what?
I am trying to find out why in first example, PHP compile class Second, but if it will have some parent classes, it won't. I researched a lot, but nothing exactly.
- I'm not trying to write code in this way, but in this example I try to understand how compilation and its sequence works.
So, PHP uses something called "late binding". Basically, inheritance and class definition doesn't happen until the end of the file's compilation.
There are a number of reasons for this. The first is the example you showed (
first extends second {}
working). The second reason is opcache.In order for compilation to work correctly in the realm of opcache, the compilation must occur with no state from other compiled files. This means that while it's compiling a file, the class symbol table is emptied.
Then, the result of that compilation is cached. Then at runtime, when the compiled file is loaded from memory, opcache runs the late binding which then does the inheritance and actually declares the classes.
When that class is seen, it's immediately added to the symbol table. No matter where it is in the file. Because there's no need for late binding anything, it's already fully defined. This technique is called early binding and is what allows you to use a class or function prior to its declaration.
When that's seen, it's compiled, but not actually declared. Instead, it's added to a "late binding" list.
When this is finally seen, it's compiled as well, and not actually declared. It's added to the late binding list, but after
Third
.So now, when the late binding process occurs, it goes through the list of "late bound" classes one by one. The first one it sees is
Third
. It then tries to find theSecond
class, but can't (since it's not actually declared yet). So the error is thrown.If you re-arrange the classes:
Then you'll see it works fine.
Why do this at all???
Well, PHP is funny. Let's imagine a series of files:
Now, which end
Foo
instance you get will depend on which b file you loaded. If you requiredb2.php
you'll getFoo extends Bar (impl2)
. If you requiredb1.php
, you'll getFoo extends Bar (impl1)
.Normally we don't write code this way, but there are a few cases where it may happen.
In a normal PHP request, this is trivial to deal with. The reason is that we can know about
Bar
while we are compilingFoo
. So we can adjust our compilation process accordingly.But when we bring an opcode cache into the mix, things get much more complicated. If we compiled
Foo
with the global state ofb1.php
, then later (in a different request) switched tob2.php
, things would break in weird ways.So instead, opcode caches null out the global state prior to compiling a file. So
a.php
would be compiled as if it was the only file in the application.After compilation is done, it's cached into memory (to be reused by later requests).
Then, after that point (or after it's loaded from memory in a future request), the "delayed" steps happen. This then couples the compiled file to the state of the request.
That way, opcache can more efficiently cache files as independent entities, since the binding to global state occurs after the cache is read from.
The source code.
To see why, let's look at the source code.
In Zend/zend_compile.c we can see the function that compiles the class:
zend_compile_class_decl()
. About half way down you'll see the following code:So it initially emits an opcode to declare the inherited class. Then, after compilation occurs, a function called
zend_do_early_binding()
is called. This pre-declares functions and classes in a file (so they are available at the top). For normal classes and functions, it simply adds them to the symbol table (declares them).The interesting bit is in the inherited case:
The outer if basically tries to fetch the class from the symbol table and checks if it doesn't exist. The second if checks to see if we're using delayed binding (opcache is enabled).
Then, it copies the opcode for declaring the class into the delayed early binding array.
Finally, the function
zend_do_delayed_early_binding()
is called (usually by an opcache), which loops through the list and actually binds the inherited classes:TL;DR
Order doesn't matter for classes which don't extend another class.
Any class that is being extended must be defined prior to the point it's implemented (or an autoloader must be used).