Fixing the $_FILES Superglobal

December is just around the corner and with it this year’s 24 ways to impress your friends. When asked, I jumped at the opportunity to resume my role as impress-ario. I must have been a tad stingy with the content on my own site this year because I had a number of ideas for articles. What follows is one of the topics that didn’t make the cut (not that it wasn’t impressive, just too language-specific).

In PHP, when a user submits a form with a <input type="file" /> the $_FILES superglobal is populated with useful information about the uploaded file. Inside you’ll find each uploaded file’s original name, content-type, temporary upload location on the server, an error code, and the size in bytes. Let’s see what that looks like (using print_r($_FILES);):

Array
(
    [download_zip] => Array
        (
            [name] => dummy.txt
            [type] => text/plain
            [tmp_name] => /Applications/MAMP/tmp/php/php5TBPsw
            [error] => 0
            [size] => 1
        )

    [download_screenshot] => Array
        (
            [name] => dummy.txt
            [type] => text/plain
            [tmp_name] => /Applications/MAMP/tmp/php/phpTncd39
            [error] => 0
            [size] => 1
        )
)

Handy. But what if you’re of the OO-persuasion and use square brackets in your input name attributes to group the properties of your object? Something like this:

<label>Zip File: <input type="file" name="download[zip]" /></label>
<label>Screenshot: <input type="file" name="download[screenshot]" /></label>

Based on the behavior of the $_GET and $_POST superglobals one would assume something along the lines of:

Array
(
    [download] => Array
        (
            [zip] => Array
                (
                    [name] => dummy.txt
                    [type] => text/plain
                    [tmp_name] => /Applications/MAMP/tmp/php/phpIPmbBR
                    [error] => 0
                    [size] => 1
                )

            [screenshot] => Array
                (
                    [name] => dummy.txt
                    [type] => text/plain
                    [tmp_name] => /Applications/MAMP/tmp/php/phpsAUsX1
                    [error] => 0
                    [size] => 1
                )
        )
)

But what have we learned about assumption? Suddenly, $_FILES is a tangled mess. The properties of your uploaded files become properties of your named object and the properties you were expecting become properties of the uploaded file properties! Zah?

Array
(
    [download] => Array
        (
            [name] => Array
                (
                    [zip] => dummy.txt
                    [screenshot] => dummy.txt
                )

            [type] => Array
                (
                    [zip] => text/plain
                    [screenshot] => text/plain
                )

            [tmp_name] => Array
                (
                    [zip] => /Applications/MAMP/tmp/php/phpIPmbBR
                    [screenshot] => /Applications/MAMP/tmp/php/phpsAUsX1
                )

            [error] => Array
                (
                    [zip] => 0
                    [screenshot] => 0
                )

            [size] => Array
                (
                    [zip] => 1
                    [screenshot] => 1
                )
        )
)

The confusion grows when you add in additional square brackets to group related files. So how do we fix this mess? We could loop through the nested properties and rebuild the array but the number of nested loops would change based on the number of nested properties.

I find it easiest to think about multi-dimensional arrays as paths to the content. The above tangle would look like this:

/download/name/zip = dummy.txt
/download/name/screenshot = dummy.txt
/download/type/zip = text/plain
/download/type/screenshot = text/plain
/download/tmp_name/zip = /Applications/MAMP/tmp/php/php7FMngA
/download/tmp_name/screenshot = /Applications/MAMP/tmp/php/phpUqQNhx
/download/error/zip = 0
/download/error/screenshot = 0
/download/size/zip = 1
/download/size/screenshot = 1

Looking at the array like this it’s clear that all we need to do is move the property (eg. name, type, etc) from the middle of the path to the end.

/download/zip/name = dummy.txt
/download/zip/type = text/plain
/download/zip/tmp_name = /Applications/MAMP/tmp/php/php3U8leL
/download/zip/error = 0
/download/zip/size = 1
/download/screenshot/name = dummy.txt
/download/screenshot/type = text/plain
/download/screenshot/tmp_name = /Applications/MAMP/tmp/php/phpdvGbJp
/download/screenshot/error = 0
/download/screenshot/size = 1

It just so happens that I have two functions in my library.php that convert arrays to and from these path representations. Once the array is flattened into a string using array_to_paths() we can use a regular expression to move the bits around and rebuild the superglobal using paths_to_array().

function fix_files_superglobal()
{
    $flat_files     = array_to_paths($_FILES);
    $fixed_files    = preg_replace('#^(/[^/]+)(/name|/type|/tmp_name|/error|/size)([^s]*)( = [^n]*)#m', '\\1\\3\\2\\4', $flat_files);
    $_FILES         = paths_to_array($fixed_files);
}

This function should be called once any time before you access the $_FILES superglobal. I’ve packaged up the three functions in an example page containing four forms. Running on your server or local development environment, this page allows you to see how PHP populates the $_FILES superglobal and what it looks like after fix_files_superglobal() tidies up (be sure to use the included 1 byte dummy.txt as your upload file or you’ll need to increase the hidden max_file_size in each form).

Previous
Quicktime Check
Next
Sharp (Partial Demo)
Author
Shaun Inman
Posted
November 30th, 2006 at 11:46 am
Categories
PHP
Comments
012 (Now closed)

012 Comments

001

Never used square brackets as name on input[type=file] elements, but always go for camelcasing or underscores. Yet now that I see it, it might in fact be handy is some situations. Good thing I won’t have to worry about re-ordering the array thanks to the inventive coding :)

Thanks Shaun!

Author
Bramus!
Posted
Nov 30th, 2006 1:20 pm
002

Yes, 100% agree.

Every time I use the $_FILES global, I always get it wrong (right?).

Thanks for the functions!

Author
Wil Alambre
Posted
Nov 30th, 2006 2:30 pm
003

Wait ‘til the ladies find out I know about this! Instant sex.

Author
Mike D.
Posted
Nov 30th, 2006 3:31 pm
004

Instant sex, indeed, Mike D!

Thanks for the code, Shaun. Since I’ve moved to ruby on rails, form/file hassle has been (mostly) a distant memory, but I’ll definitely be using your code for the odd PHP project that comes across my door.

Author
Jacob
Posted
Nov 30th, 2006 4:55 pm
005

seems like a lot of work to correct a problem that is so arbitrary.

Why use square brackets as a delimiter for properties when you could easily use ‘::’ (which even seems natural, seeing how you call non-instantiated class methods with that operator), or ‘||’ or anything at all really?

Is this property grouping with square brackets a part of some pre-built component you use, or is it completely custom?

Author
steve
Posted
Nov 30th, 2006 6:22 pm
006

Steve, square brackets are traditionally used to let the language on the receiving end of a form submission know to group the submitted data as an associative array.

A form containing the following:

<input type="text" name="user[email]" />
<input type="password" name="user[password]" />

Would result in the creation of a user array in the $_POST (or $_GET—take your pick) superglobal with two properties (or indexes since we’re dealing with an array and not a proper instance of an object), email and password. The $_FILES superglobal behaves the same way except it inserts its own properties in an unexpected (and inconsistent depending on your input naming) place. Hence the fix.

Author
Shaun Inman
Posted
Nov 30th, 2006 7:02 pm
007

I think I read about that square brackets when I first got started with PHP and since then have forgotten about them. Good article — I hate the $_FILES stuff in PHP :\

Author
shorty114
Posted
Nov 30th, 2006 11:41 pm
008

It’s stuff like this that makes me really glad I started working in Ruby on Rails a few months ago.

Unfortunately I still have some projects in PHP, and this is actually one of the many problems I ran into.

I have already fixed the problem in a different way (I honestly would never use regular expressions in a case like this, they just feel to hacky), but it’s refreshing to see some other solutions.

Author
Tom-Eric Gerritsen
Posted
Dec 1st, 2006 3:20 am
009

Your knowledge on regex stun me, Shaun. Great fix. But cake does this kind of things for me.

Author
Chris Hoeppner
Posted
Dec 2nd, 2006 11:06 am
010

I’m perplexed. Why all the swap-alicious mumbo jumbo when you can just access the properties in that order while dealing with the uploads? It makes sense to me to just ask for $_FILES[‘download’][‘error’][$current_item], etc..

Or:

foreach($_FILES['download'] as $property => $arr)
{
    foreach($arr as $item => $value)
    {
        $result[$item][$property] = $value;
    }
}
Author
Eli Huebert
Posted
Dec 2nd, 2006 7:51 pm
011

Eli, you can still do it that way but I prefer consistency in my programming interfaces. The other superglobals set the precedent, this function just helps $_FILES follow through.

Author
Shaun Inman
Posted
Dec 2nd, 2006 8:50 pm
012

I understand. Just thinking out loud, thus the included code snippet as I realized what you were getting at. I’m a fan of tricky regular expressions, but that seems a bit clumsy to me. I guess I don’t have your other two functions lying around either.

Anyway, any article that has visions of code snippets floating through my head as I read is enjoyable. Thanks!

My(spur-of-the-moment) function:

function fix_files_superglobal()
{
    function rearrange($group)
    {
        foreach($group as $property => $arr)
        {
            foreach($arr as $item => $value)
            {
                $result[$item][$property] = $value;
            }
        }
    return $result;
    }
    $_FILES = array_map('rearrange', $_FILES);
}
Author
Eli Huebert
Posted
Dec 2nd, 2006 9:59 pm