Now I remember why PHP is so easy to hate…

(aka “why do my include/require/include_once/require_once files not work / seem NOT to be included, even though they are?”)

PHP has a mechanism for including files inside each other. The architects of PHP didn’t really think much about what they were doing with a lot of the core language features (witness the foolishness over Register Globals), and file import/include/require is a classic example.

This is one of the most fundamental features of the language, and it’s screwed up. It “seems” to work, so long as you write simplistic enough / small enough apps. The bigger your app, the more likely it is you’ll discover how poor this part of the language is.

In most languages, you have a distinction between

  1. importing (IMP)
  2. including (INC)
  3. internally-evaluating (INT)
  4. externally-evaluating (EXT)

(not all languages have all of the above – most have 2 or 3 of them)

The first one runs at the language-level, and is surrounded by all sorts of careful compiletime/runtime code to manage exactly what happens. The second one runs at the source-file-level, and simply “dumps” the contents of another file inside the main file. The last two are like the second, except that they “executes” the other file, and whatever that “execution” process outputs is what gets embedded – rather than the contents of the file. They differ from each other in that one executes using all the currently-in-scope info, the other executes without any access to currently scoped data.

PHP says “**** it, I can’t be bothered with writing code properly, I”ll just pretend all four of those are identical, and I’ll name the functions as if I meant INC, even though I don’t”.

Net result: the include/require/include_once/require_once statements in PHP don’t really work properly, because they have to do the work BOTH of importing AND of including AND of evaluating … all in one go.

(by the way … this is subtly documented in the fourth paragraph of the docs for include(), but there’s no warning in the other statement/function docs)

Here’s the “compromises” that have been made (with the use-cases in brackets):

  • Files are treated as plain HTML (INC)
  • …but also, simultaneously, as “executable, may contain PHP tags” (EXT)
  • When a file is brought in, it is only executed when that line of code is executed (INC, INT)
  • The scope of anything executed is the scope of the currently-executing function (INT)
  • Any (de facto) closures (in the form of defined functions) found inside the brought-in file … are shunted into the current environment (INC, IMP)

Several of those are clearly mutually incompatible already. It’s simply not possible to put all three of those features onto a single language-statement. Unfortunately … PHP has.

That’s just irritating – it means that basic PHP apps suddenly break when you add a line of code, because e.g. you’ve been using include() to do EXT, or IMP, and that was fine … but you just added some code that expose it’s incompatible behaviour by showing off some of its INT functionality (or vice versa).

But far worse is what happens when you throw “_once()” into the mix. In practical terms, it’s actually surprisingly easy to write code that doesn’t run correctly at runtime (and this is undocumented, too). Because “_once()” is, essentially, an undefined language feature.

Don’t believe me? Go read the docs. Find for me the point where they state the definition of:

“if the code from a file has already been included”

(or … don’t bother. Take my word for it – it’s not defined).

If IMP, INC, and EVAL were *not* squashed into some ugly mess like they are in PHP, this *would not be a problem*, because people would just work on the “obvious” interpretation of “has already been included”, i.e.:

you called “include*()” or “require*()” on this filename already

you called “include*()” or “require*()” on this filename AT THIS SCOPE already

What? TWO definitions? Well, yes, dear reader, because most humans will pick the first definition. It appears correct. And for IMP and EXT – by definition – it is correct, since they ignore scope. However, sadly, INC and INT explicitly require scope to be obeyed, and they *require* the second definition to be used.

Guess which definition PHP uses? Bearing in mind that PHP *treats the include() file differently* depending upon which scope it’s imported at…

Did you guess the second option? Ha! Wrong!

And so, in PHP, it’s easy to write something like this:

file 1:
if( !isset( $A) )
   $A = "not blank"

file 2:
run();
function run()
{
   require_once 'file 1';
   require_once 'file 3';
}

file 3:
crashout();
function crashout()
{
  require_once 'file 1';
  if( isset($A) )
    echo "this will never happen!";
  else
    echo "PHP will claim that A is not set, even though"
      . " it is explicit set in the file that is explicitly included above!";
}

I bashed my head against a wall with this (kind of) problem until I realised what was going on: PHP has a very poor definition of “a file has already been included”.

And what the heck can you do to workaround this? Actually, I’m not sure yet. I’ve only recently worked out how and why the runtime does what it does. I’ve not yet worked out a simple approach to PHP programming that avoids the above problems, beyond “never use an include* statement from within a function – never ever, under any circumstances”. At least that forces the behaviour to be predictable (NB: as soon as you do an include* from within a function, each and every one of your PHP scripts will potentially cease to work). But it’s very annoying to be actively prevented from ever doing an INC/INT/EXT.

16 thoughts on “Now I remember why PHP is so easy to hate…”

Comments are closed.