06 Feb

Some interesting stuff about include and functions in PHP

Let mi tell you a fun scenario. You are a web developer and you release your product on a third-party environment. Not only one product, but two or even three with a really similar code base. Then you find out that someone (or rather a group of people) tried to use all your products at once. On the same platform. And because you didn’t worry about that before, you find out on a nice and quiet morning, that there is some incompatibility which triggers an error. And not just any error, a fatal error which affects the whole platform. Of course, I am talking about PHP and WordPress.

Each plugin gets injected into a WordPress blog with include_once statements. There are four methods to include some external PHP code (like a plugin) in your file:

  • require
  • include
  • require_once
  • include_once

The basic difference between require and include is that if the file is missing or couldn’t be included because of an error, the require triggers a fatal error and include a warning. The _once postfix just means that the external file can only be included once and if the code tries to do it again, nothing happens (meaning that the code doesn’t get included again).

These were just basics. Now we are going to move to the interesting stuff.

 The power

The variable scope in the included PHP file is inherited from the line where it was called. This is great, because we can do something like this:

external-example1.php:

<?php
print $variable;
php &gt; $variable = 'Hello world 1!';
php &gt; include_once 'external-example1.php';
Hello world 1!
php &gt; $variable = 'Hello world 2!';
php &gt; include 'external-example1.php';
Hello world 2!
php &gt; $variable = 'Hello world 3!';
php &gt; include_once 'external-example1.php';
php &gt;

And the chaos

But seriously, we are talking about PHP here. Let me introduce you to my  today’s Wat. We have some example files, which I will call from PHP’s interactive shell. Note, I always closed the shell and reopened it again.

external-example2.php

<?php
if (!function_exists('test_fn')) {
        function test_fn() {
                print "Hello, test_fn!";
        }
}
else {
        print "test_fn already exists!\n";
}
php &gt; include 'external-example2.php';
php &gt; include 'external-example2.php';
test_fn already exists!
php &gt;

This is still pretty much expected. In the first run the function gets defined and in the second id doesn’t get redefined again. So now we can be sure that the declared functions belong to global scope.

external-example3.php

<?php
if (function_exists('test_fn')) {
        print "Function exists, do not redeclare it!\n";
        return;
}
else {
        function test_fn() {
                print "Hello, test_fn!";
        }
        print "test_fn declared.\n";
}
print "This line should always get printed except if the if-statement is true.\n";
php &gt; include 'external-example3.php';
test_fn declared.
This line should always get printed except if the if-statement is true.
php &gt; include 'external-example3.php';
Function exists, do not redeclare it!
php &gt;

It is only natural that we can put declarations in brackets and declare them when needed. But why the else statement? When the return is triggered, the script will stop anyway. So let’s rewrite external-example3.php to something prettier.

external-example4.php

<?php

if (function_exists('test_fn')) {
        print "Function exists, do not redeclare it!\n";
        return;
}

function test_fn() {
        print "Hello, test_fn!";
}
print "test_fn declared.\n";

print "This line should always get printed except if the if-statement is true.\n";
php &gt; include 'external-example4.php';
Function exists, do not redeclare it!
php &gt; include 'external-example4.php';
PHP Fatal error:  Cannot redeclare test_fn() (previously declared in /home/sigi/tmp/php/external-example4.php:9) in /home/sigi/tmp/php/external-example4.php on line 10

Fatal error: Cannot redeclare test_fn() (previously declared in /home/sigi/tmp/php/external-example4.php:9) in /home/sigi/tmp/php/external-example4.php on line 10
php &gt;

I hope you agree that this is really disturbing. I didn’t know about this and when I was debugging the products I was sure that WordPress had some business with it. I went trough WordPress documentation and code, trying to figure out the cause. My first idea was that WordPress includes the files more than once but that wasn’t the case. The second (probably stupid) idea was that WordPress does some recompiling on its own but I also didn’t find any proof. The last thing I did (which should have been the first, noted!) helped me understand what was going on. Or rather understand what wasn’t going on. I did some examples of my one. The first explanation I got is that PHP does some kind of sanity check before the file is included. I am extremely fond of PHP documentation but this isn’t mentioned anywhere in includerequireinclude_once and require_once pages. Then I did some googling and found this: User-defined functions.

My colleague mentioned that this looks like hoisting but this is not the case here. Well, not entirely. The “hoisting” happens on the script’s global scope but not internally for functions. The next example proves that.

<?php

print no_hoisting('wat') . "\n";

function no_hoisting($x) {
        return $x . $x . $x;
}

function test() {
        print test2() . "\n";
        function test2() {
                return 'This doesn\'t work';
        }
}

test();
php &gt; include 'external-example5.php';
watwatwat
PHP Fatal error:  Call to undefined function test2() in /home/sigi/tmp/php/external-example5.php on line 10

Fatal error: Call to undefined function test2() in /home/sigi/tmp/php/external-example5.php on line 10
php &gt;

And then I realized that I already knew that. I knew that the function declaration order isn’t important. I really disliked it because it can lead to many misunderstandings (like the one above), so I never put much thought into it.

So, the logical consequence of unimportant function order is that the functions get declared at the beginning of the script. This explains the above behavior and errors.

Point being, never intentionally ignore things you don’t like about a programming language. Never. Because they tend to stab you in the back if you give them an opportunity. And I gave them. Point taken. And kids, don’t do drugs, do PHP. It’s certainly not that dangerous but it can f**k your life just as well.

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>