This is a stupid experiment about making a PHP source code preprocessor in PHP, and also try to implement some closure support in PHP 5.2, which I’m still using. Here’s how it works:

  • Create a custom stream filter (thanks to this blog post) that process the source code using a function.
  • Create a function that transforms anonymous functions into create_function calls, and also make it support the “use” keyword.
  • Then include file like this: include ('php://filter/read=preprocess/resource=(filename)');
  • The transform function also adds 'php://filter/read=preprocess/resource=' . after each occurence of include/include_once/require/require_once, this way all included files are proprocessed too!

After some hours of experimenting, I came up with this code. It reads in realindex.php that contains PHP code with closures and transform the source:

<?php

// index.php
// Closure Preprocessor by the DtTvB

$closure_preprocessor_data = array();
$closure_preprocessor_id = 0;

function closure_preprocessor__quote($x) {
    return '\'' . strtr($x, array('\\' => '\\\\', '\'' => '\\\'')) . '\'';
}

function closure_preprocessor__create($args, $code, $imports) {
    global $closure_preprocessor_id, $closure_preprocessor_data;
    $id = $closure_preprocessor_id ++;
    $closure_preprocessor_data[$id] = array();
    $importcode = '';
    $j = 0;
    foreach ($imports as $k => $v) {
        $closure_preprocessor_data[$id][$j] = &$imports[$k];
        $importcode .= '$' . $k . ' = &$GLOBALS[\'closure_preprocessor_data\'][' . $id . '][' . $j . '];';
        $j ++;
    }
    return create_function($args, $importcode . $code);
}

function closure_preprocessor__function($data) {
    preg_match ('~function\s*([^\(]+)?\(((?:\'(?:\\\\.|[^\'])*\'|"(?:\\\\.|[^"])*"|.)*?)\)\s*(use\s*\(([^)]*)\)\s*)?\{~si', $data, $head);
    // print_r ($head);
    $body = substr(substr($data, strlen($head[0])), 0, -1);
    $body = substr(substr(closure_preprocessor('<?php ' . $body . ' ?>'), 5), 0, -3);
    if (!empty($head[1])) { // Has name. Leave unchanged.
        return $head[0] . $body . '}';
    }
    if (!empty($head[4])) {
        preg_match_all ('~&?\$([^,\s]*)~', $head[4], $imports, PREG_SET_ORDER);
        $importcode = array();
        foreach ($imports as $v) {
            $importcode[] = closure_preprocessor__quote($v[1]) . ' => ' . $v[0];
        }
        return 'closure_preprocessor__create(' . closure_preprocessor__quote($head[2]) . ', ' . closure_preprocessor__quote($body) . ', array(' . implode(',', $importcode) . '))';
    } else {
        return 'create_function(' . closure_preprocessor__quote($head[2]) . ', ' . closure_preprocessor__quote($body) . ')';
    }
}

function closure_preprocessor($data) {
    $tokens = token_get_all($data);
    $out = '';
    $buffer = '';
    $level = 0;
    foreach ($tokens as $v) {
        if ($level > 0) {
            if (is_string($v) && $v == '{') {
                $buffer .= $v;
                $level ++;
            } else if (is_string($v) && $v == '}') {
                $level --;
                $buffer .= $v;
                if ($level == 1) {
                    $level --;
                    $out .= closure_preprocessor__function($buffer);
                }
            } else {
                $buffer .= is_array($v) ? $v[1] : $v;
            }
        } else {
            if (is_array($v) && $v[0] == T_FUNCTION) {
                $buffer = $v[1];
                $level ++;
            } else if (is_array($v) && ($v[0] == T_INCLUDE || $v[0] == T_INCLUDE_ONCE || $v[0] == T_REQUIRE || $v[0] == T_REQUIRE_ONCE)) {
                $out .= $v[1] . ' \'php://filter/read=preprocess/resource=\' . ';
            } else {
                $out .= is_array($v) ? $v[1] : $v;
            }
        }
    }
    // echo $out;
    return $out;
}

class preprocessor_filter extends php_user_filter {
    var $data = '';
    function filter($in, $out, &$consumed, $closing) {
        while($bucket = stream_bucket_make_writeable($in)) {
            $this->data .= $bucket->data;
            $this->bucket = $bucket;
            $consumed = 0;
        }
        if ($closing) {
            $consumed += strlen($this->data);
            $this->bucket->data = closure_preprocessor($this->data);
            //echo $this->bucket->data . "\n\n";
            $this->bucket->datalen = strlen($this->bucket->data);
            if (!empty($this->bucket->data))
                stream_bucket_append($out, $this->bucket);
            return PSFS_PASS_ON;
        }
        return PSFS_FEED_ME;
    }
}

stream_filter_register ('preprocess', 'preprocessor_filter');
include ('php://filter/read=preprocess/resource=realindex.php');

?>

And here is realindex.php

<?php

include 'testinclude.php';

$h1 = makeTag('h1');
$p = makeTag('p');

echo $h1($greet('Closures'));

$incrementA = makeIncrementer();
$incrementB = makeIncrementer(10);
$incrementC = makeIncrementer(20);

echo $p('Increment A: ' . $incrementA());
echo $p('Increment A: ' . $incrementA());
echo $p('Increment A: ' . $incrementA());
echo $p('Increment A: ' . $incrementA());
echo $p('Increment B: ' . $incrementB());
echo $p('Increment B: ' . $incrementB());
echo $p('Increment B: ' . $incrementB());
echo $p('Increment C: ' . $incrementC());
echo $p('Increment C: ' . $incrementC());
echo $p('Increment C: ' . $incrementC());
echo $p('Increment A: ' . $incrementA());
echo $p('Increment B: ' . $incrementB());
echo $p('Increment C: ' . $incrementC());

$a = 1; $b = 2; $c = 3;
$adderA = makeAdder($a);
$adderB = $adderA($b);
echo $p($a . ' + ' . $b . ' + ' . $c . ' = ' . $adderB($c));

echo $p(camelCase('hello-world'));

?>

And another file testinclude.php

<?php

function makeIncrementer($start = 0) {
    return function() use (&$start) {
        return ++$start;
    };
}

function makeAdder($a) {
    return function($b) use ($a) {
        return function($c) use ($a, $b) {
            return $a + $b + $c;
        };
    };
}

function makeTag($tag) {
    return function($content) use ($tag) {
        return '<' . $tag . '>' . $content . '</' . $tag . '>';
    };
}

function camelCase($x) {
    return preg_replace_callback('~-([a-z])~', function ($match) {
        return strtoupper($match[1]);
    }, $x);
}

$greet = function($name) {
    return sprintf("Hello %s!", $name);
};

?>

Running the code, it said:

Hello Closures!

Increment A: 1

Increment A: 2

Increment A: 3

Increment A: 4

Increment B: 11

Increment B: 12

Increment B: 13

Increment C: 21

Increment C: 22

Increment C: 23

Increment A: 5

Increment B: 14

Increment C: 24

1 + 2 + 3 = 6

helloWorld

The limitations is that it somehow does not work very well in some classes. For example, it can’t access private class members (not sure what it’s called, I don’t do much OOP).

And this code probably has bugs. If you found any bugs in this code and you have a fix please email me so I can update the code. Lol