Sunday 5 October 2014

PHP: include paths are relative to the current working directory

G'day:
This one had me confused for a day or so last week. It seems the relative paths in include / require calls in PHP are relative to the current working directory, not the file the include statement is actually in. I'm not sure I agree with this.



I was knocking together some unit tests with PHPUnit, just to test how its mocking working (this will be another blog lamentation during the week, once I finalise my findings), and was getting some very odd behaviour when running my tests from the command line. I had this sort of file structure:

[base directory]
    classes/
        Some.class.php
    tests/
        SomeTest.class.php

In SomeTest.class.php, I had this:

require "../classes/Some.class.php";

I opened my command prompt and drilled down to the tests dir, and ran the tests just fine. Later I had a different command prompt open, and it was sitting in [base directory], as I was doing a few things at once, so I just ran phpunit tests, and it errored, because PHP could not find ../classes/Some.class.php. The file was still there.

Whilst hunting around for it, I ended up with my command prompt back in the tests directory, and I ran the tests again - this time with an unqualified call to just phpunit, and the tests ran. I did a cd .., ran them as phpunit tests, and they failed again. WTF?

After much  Googling and StackOverflowing, I found the answer.

Relative file paths in include / require calls are not relative to the code making them, they're relative to the current working directory! So if I call my code from one directory, the include is looking for the file in one place; if I call it from another directory with a relative path to the PHP file, the include is looking for the file in a different place.

This strikes me as absolute lunacy.

I hasten to add that I get why PHP scripts need to be aware of their working directory, because one might use PHP to write shell scripts, and those scripts will need to be working-directory savvy. However I don't see the sense in this being applied to PHP's own include / require operations. Or at least not by default.

However I'm clearly missing something, because I create some like-for-like tests on Ruby and Python, and they behave the same way.

Here's an example for PHP. Note: the directory structure for these files is as follows:

include/    
    working/
        actual/
            callMe.php
            inc.php
        inc.php


<?php
// working/actual/callMe.php
printf("Working directory: %s\n", getcwd());
include "inc.php";


<?php
// working/actual/inc.php
echo "included: working/actual/inc.php\n\n";


<?php
// working/inc.php
echo "included: working/inc.php\n\n";

Obviously (?) I expect the inc.php in the actual directory to be included. However this is only the case if I call callMe.php from the actual directory. If I call it from the working directory, as php actual/callMe.php, then it's the working/inc.php file that's actually included. I cannot see how this is sensible. Here's the test runs to demonstrate this:

C:\include\working>php actual\callMe.php
Working directory: C:\include\working
included: working/inc.php

C:\include\working>cd actual

C:\include\working\actual>php callMe.php
Working directory: C:\include\working\actual
included: working/actual/inc.php

C:\include\working\actual>

See how it's including different files, depending on from where I ran the initial script?!

As I said I have Ruby and Python (my first go at Python!) variations too:

include/    
    working/
        actual/
            callMe.py
            callMe.rb
            inc.py
            inc.rb
        inc.py
        inc.rb

(the links go to the code on GitHub), and they behave the same. If it was PHP behaving this way in isolation, I'd just assume this is PHP being thick (it generally seems to be a safe bet). However given the other two behave the same way, I guess my expectations are off.

Basically this means that if you're gonna be calling PHP code from the command prompt, for it to work reliably you need to give the include / require paths as fully-qualified absolute paths, eg:

require dirname(__FILE__) . "\..\classes\Some.class.php";

That way it'll always find the file you mean (relative to the current file). That, I'm afraid, seems a bit shit to me.

--
Adam