Google Groups no longer supports new Usenet posts or subscriptions. Historical content remains viewable.
Dismiss

Order of class definitions within the same file

6 views
Skip to first unread message

Thomas Mlynarczyk

unread,
Jul 24, 2016, 5:23:41 AM7/24/16
to
Consider the following code:

class C extends B {}
class B extends A {}
class A {}

This will cause an error:

Fatal error: Class 'B' not found in [...] on line 1

However, when switching lines 2 and 3, the error goes away:

class C extends B {}
class A {}
class B extends A {}

Note that the error had occurred at line 1 where class C was defined as
an extension of class B, and line 1 was not changed here! I tested this
with PHP 5.4, 5.6 and 7.0. In fact, *any* order of the three class
definitions will work *except* C-B-A. The PHP manual says
(<http://php.net/extends>):

| Note: Classes must be defined before they are used! If you want the
| class Named_Cart to extend the class Cart, you will have to define
| the class Cart first. If you want to create another class called
| Yellow_named_cart based on the class Named_Cart you have to define
| Named_Cart first. To make it short: the order in which the classes
| are defined is important.

Which contradicts the result of the experiment above. I would have
expected either A-B-C to be the *only* order that works, or *any* order
to work (including, of course C-B-A). So what's going on here? Google
was not too helpful -- I only found
<http://stackoverflow.com/q/27627045>, which does not really give a
satisfactory answer.

Greetings,
Thomas

--
Ce n'est pas parce qu'ils sont nombreux à avoir tort qu'ils ont raison!
(Coluche)

Christoph M. Becker

unread,
Jul 26, 2016, 6:45:13 PM7/26/16
to
On 24.07.2016 at 11:23, Thomas Mlynarczyk:

> Consider the following code:
>
> class C extends B {}
> class B extends A {}
> class A {}
>
> This will cause an error:
>
> Fatal error: Class 'B' not found in [...] on line 1
>
> However, when switching lines 2 and 3, the error goes away:
>
> [snip]
>
> […] I would have
> expected either A-B-C to be the *only* order that works, or *any* order
> to work (including, of course C-B-A). So what's going on here? […]

Interesting question! A debug session might be helpful to *surmise*
what's going on. At least in PHP 5.6 the compilation of a file ends
with a call to zend_do_end_compilation(), so set a breakpoint there.
Then view CG(class_table) (which is the class table of the compiler).
The original code shows the following relevant keys:

"\0c/vagrant/thomas.php0x7ffff7ff2019"
"\0b/vagrant/thomas.php0x7ffff7ff202e"
"a\0"

Apparently, class C and B are marked as being somehow incomplete. Note
that the order of the entries correlates to the definition of the
classes. That makes sense, as the (1-pass) compiler can't know the
details of the superclasses, which are defined only later.

Compare that to the relevant keys after having switched line 2 and 3:

"\0c/vagrant/thomas.php0x7ffff7ff2019"
"a\0"
"b\0"

Appears to be consistent.

Later on the executor tries to look up class B (`extends B`) what will
succeed in the second case, but fails in the first one, and so the error
is thrown.

It seems to me that the PHP manual (<http://php.net/extends>) is not
completely correct wrt. the implementation, but the fact that the second
script is accepted might be regarded as implementation detail (an
implementation which is currently slightly less restricting).

For me, this answer seems rather unsatisfactory, but I don't know
better, and there's not much documentation about the Zend engine, and
what is there seems to be pretty much scattered, unfortunately.

--
Christoph M. Becker

Jerry Stuckle

unread,
Jul 26, 2016, 11:15:11 PM7/26/16
to
Christopher, I'm not so sure the documentation (what little there is) is
incorrect in the way it is intended to work. Rather, I suspect it's not
working as intended.

I'm with you - I would expect the only sequence to work would be A-B-C
and anything else get compile error. Perhaps its an unanticipated side
effect from another change. But until the doc changes to match it, I
wouldn't depend on it. It might easily go away in the next version.

--
==================
Remove the "x" from my email address
Jerry Stuckle
jstu...@attglobal.net
==================

Thomas Mlynarczyk

unread,
Jul 27, 2016, 10:57:05 AM7/27/16
to
[Sorry, I had sent this to the poster instead of the group by mistake --
Thunderbird's Reply button vs. Followup... Grrr...]

On 27/07/16 00:45, Christoph M. Becker wrote:

> Then view CG(class_table) (which is the class table of the compiler).
> The original code shows the following relevant keys:
>
> "\0c/vagrant/thomas.php0x7ffff7ff2019"
> "\0b/vagrant/thomas.php0x7ffff7ff202e"
> "a\0"
>
> Apparently, class C and B are marked as being somehow incomplete. Note
> that the order of the entries correlates to the definition of the
> classes. That makes sense, as the (1-pass) compiler can't know the
> details of the superclasses, which are defined only later.
>
> Compare that to the relevant keys after having switched line 2 and 3:
>
> "\0c/vagrant/thomas.php0x7ffff7ff2019"
> "a\0"
> "b\0"
>
> Appears to be consistent.
>
> Later on the executor tries to look up class B (`extends B`) what will
> succeed in the second case, but fails in the first one, and so the error
> is thrown.

I still don't quite understand it. In the second example (C-A-B), the
executer notices that C is marked as incomplete, because of the parent
B. So it looks for B and finds it. In the first case (C-B-A), it's the
same, but B is marked as incomplete as well -- so why does the executor
give up instead of simply doing another "level" of look-up? This must
have been a deliberate decision made by the programmers.

> It seems to me that the PHP manual (<http://php.net/extends>) is not
> completely correct wrt. the implementation, but the fact that the second
> script is accepted might be regarded as implementation detail (an
> implementation which is currently slightly less restricting).

Indeed, as Jerry remarked, according to the manual, A-B-C is the only
valid order and whether any other order works or not should be regarded
as undocumented behaviour and hence not be relied upon.

But it would be nice if any order worked. Good programming practise
requires you to put each class definition in a separate file. For
distributing code, all files are sometimes combined into one. This is
easily achieved by concatenating every file in the source directory.
However, the order in which the files are concatenated, cannot be
controlled as easily, so one might end up with any order of the three
classes A, B and C. And that's where it would come in really handy if
any order worked. I know: PHAR exists, but still...

Anyway, thank you for your analysis.

Jerry Stuckle

unread,
Jul 27, 2016, 12:00:05 PM7/27/16
to
The problem with any order working is requiring additional passes to
parse the code. For instance, if it were C-B-A, the parser would have
to go through three times. The first would mark C and B as incomplete,
and A as complete. Then it would have to parse C and B again; this time
it would mark C as incomplete and B as complete. Finally, a third pass
would mark C as complete. Of course, this means parsing (and everything
else) takes longer.

Maybe they actually use a two pass parser - which would account for the
behavior. I haven't looked at the source code in depth, so I don't know.

Christoph M. Becker

unread,
Jul 27, 2016, 12:06:39 PM7/27/16
to
On 27.07.2016 at 16:57, Thomas Mlynarczyk wrote:

> I still don't quite understand it. In the second example (C-A-B), the
> executer notices that C is marked as incomplete, because of the parent
> B. So it looks for B and finds it. In the first case (C-B-A), it's the
> same, but B is marked as incomplete as well -- so why does the executor
> give up instead of simply doing another "level" of look-up? This must
> have been a deliberate decision made by the programmers.

There are cases which can't be resolved, at least mutual and circular
inheritance (A extends B, B extends A). Not doing further lookup might
just be a simplification to avoid tracking such cycles.

Anyhow, I found that your second example already works as of PHP 4.3 at
least, see <https://3v4l.org/2DJJh>. However, the respective manual
section has only been added in 2010[1], to resolve
<https://bugs.php.net/bug.php?id=51381>, which also notes that there are
exceptions from the rule "declare parent before child", which have not
been addressed. Interestingly, there is another bug report with regard
to this very issue: <https://bugs.php.net/bug.php?id=70800>.

I suppose we'd need to ask Zend engine developers about the details.
inte...@lists.php.net might be a good place to reach these.

> Indeed, as Jerry remarked, according to the manual, A-B-C is the only
> valid order and whether any other order works or not should be regarded
> as undocumented behaviour and hence not be relied upon.

ACK.

> […] Good programming practise
> requires you to put each class definition in a separate file. For
> distributing code, all files are sometimes combined into one. […]

Well, nowadays we may rely on OPcache (or another bytecode cache) to be
available, which solves the issue. But I agree that combining in a
single file might still make sense.

[1] <http://svn.php.net/viewvc/?view=revision&amp;revision=304561>

--
Christoph M. Becker

Thomas Mlynarczyk

unread,
Jul 27, 2016, 2:36:08 PM7/27/16
to
On 27/07/16 18:00, Jerry Stuckle wrote:
> The problem with any order working is requiring additional passes to
> parse the code. For instance, if it were C-B-A, the parser would have
> to go through three times. The first would mark C and B as incomplete,
> and A as complete. Then it would have to parse C and B again; this time
> it would mark C as incomplete and B as complete. Finally, a third pass
> would mark C as complete. Of course, this means parsing (and everything
> else) takes longer.

I think I get it now. There *are* actually two "passes": the first one
when the script is compiled and the second one when the script is
executed. And the latter expects all class definitions to be either
complete or available via autoloading but it will not bother with
multiple look-ups.

Thomas Mlynarczyk

unread,
Jul 27, 2016, 2:51:17 PM7/27/16
to
On 27/07/16 18:06, Christoph M. Becker wrote:

> Anyhow, I found that your second example already works as of PHP 4.3 at
> least, see <https://3v4l.org/2DJJh>. However, the respective manual
> section has only been added in 2010[1], to resolve
> <https://bugs.php.net/bug.php?id=51381>, which also notes that there are
> exceptions from the rule "declare parent before child", which have not
> been addressed. Interestingly, there is another bug report with regard
> to this very issue: <https://bugs.php.net/bug.php?id=70800>.

Interesting. I had not found these bug reports via Google.

> Well, nowadays we may rely on OPcache (or another bytecode cache) to be
> available, which solves the issue. But I agree that combining in a
> single file might still make sense.

Yes, with opcode caches and/or PHAR archives any overhead from including
multiple files is probably reduced to a negligible minimum. Still,
whenever combining several files into a single one is done, a way to
ensure the correct order would be to sort the file names by length --
the idea behind this being that derived classes should have longer names
than their parent class, like

class Thing {}
class FooThing extends Thing {}
class GreenFooThing extends FooThing {}

which would be reflected in the respective file names. Of course, this
requires discipline when naming classes and still feels like a hack.

Christoph M. Becker

unread,
Aug 29, 2016, 6:07:44 AM8/29/16
to
On 27.07.2016 at 20:35, Thomas Mlynarczyk wrote:

> On 27/07/16 18:00, Jerry Stuckle wrote:
>
>> The problem with any order working is requiring additional passes to
>> parse the code. For instance, if it were C-B-A, the parser would have
>> to go through three times. The first would mark C and B as incomplete,
>> and A as complete. Then it would have to parse C and B again; this time
>> it would mark C as incomplete and B as complete. Finally, a third pass
>> would mark C as complete. Of course, this means parsing (and everything
>> else) takes longer.
>
> I think I get it now. There *are* actually two "passes": the first one
> when the script is compiled and the second one when the script is
> executed. And the latter expects all class definitions to be either
> complete or available via autoloading but it will not bother with
> multiple look-ups.

Indeed. I just found a more detailed explanantion by one of the PHP
developers:
<http://jpauli.github.io/2015/03/24/zoom-on-php-objects.html#class-binding>.
(At the end of the section there's even the example that started this
thread.)

--
Christoph M. Becker

0 new messages