Fun with ‘anonymous’ functions in PHP

Just some fun with anonymous functions in PHP (which are surprisingly rare due to silly syntax, and a buggy implementation)

<?php

    class LambdaException extends Exception {}
    class Lambda
    {
        private static $cache;

        public static function Create($functionString)
        {
            if(!isset(self::$cache[$functionString]))
            {
                list($args, $body) = self::BreakupFunctionString($functionString);
                self::$cache[$functionString] = new self($args, $body);
            }

            return self::$cache[$functionString];
        }

        private static function BreakupFunctionString($functionString)
        {
            $errorMessage = "Unable to understand the function: '$functionString'." .
                            "Format should follow function (\$arg1, \$arg2) { return rand(\$arg1, \$arg2); }";

            $regex = "^function\s*\(([^\)]*)\)\s*\{(.*?)\}$";
            if(preg_match("#$regex#si", trim($functionString), $matches) !== 1)
            {
                throw new LambdaException($errorMessage);
            }

            list(, $args, $body) = $matches;

            $args = explode(',', $args);
            $args = array_map('trim', $args);
            $args = array_diff($args, array(''));

            return array($args, $body);
        }

        //--------------------------------------------------------------------


        private $args;
        private $body;
        private $function;

        private function __construct($args, $body)
        {
            assert('is_array($args)');
            assert('is_string($body)');

            $this->args = $args;
            $this->body = $body;
            $this->function = create_function(implode(',', $this->args), $this->body);
        }

        public function map($array)
        {
            //  We could actually test if the number of args is two
            //  and do a array_map with keys as well
            assert('count($this->args) == 1');
            return array_map($this->function, $array);
        }

        public function filter($array)
        {
            assert('count($this->args) == 1');
            return array_filter($array, $this->function);
        }

        public function sort($array)
        {
            assert('count($this->args) == 2');
            usort($array, $this->function);
            return $array;
        }

        public function call()
        {
            $args = func_get_args();

            return call_user_func_array($this->function, $args);
        }

        public function getFunction()
        {
            return $this->function;
        }

        public function __toString()
        {
            return $this->function;
        }
    }

    $extractRowId = Lambda::Create('function ($row) { return $row["id"]; }');
    print_r($extractRowId->map(array(array('id' => 'a'), array('id' => 'b'))));

    $randomise = Lambda::Create('function ($v, $v) { return rand(0,1) == 0 ? -1 : 1; }');
    print_r($randomise->sort(array(1, 2, 3, 4, 5, 6, 7, 8, 9)));

    $greaterThanFive = Lambda::Create('function ($var) { return $var > 5; }');
    print_r(array_filter(array(1,2,3,4,5,6,7,8,9), $greaterThanFive->getFunction()));
    print_r(array_filter(array(1,2,3,4,5,6,7,8,9), (string)$greaterThanFive));

    $printString = Lambda::Create('function($str) { echo $str . "\n"; }');
    $printString->call("Hello World");


    function formatString($s)
    {
        $args = func_get_args();

        for($i = 1; $i < count($args); $i++)
        {
            $s = $args[$i]->call($s);
        }

        return $s;
    }


    $italic = Lambda::Create('function($s) { return "<i>" . $s . "</i>"; }');
    $bold   = Lambda::Create('function($s) { return "<bold>" . $s . "</bold>"; }');

    $tag        = Lambda::Create('function($t) { return Lambda::Create("function(\$s) { return \"<$t>\" . \$s . \"</$t>\"; }"); }');
    $heading    = $tag->call('h1');
    $para       = $tag->call('p');

    echo formatString('Weird Formatting', $italic, $bold, $heading) . "\n";


    $content    = Lambda::Create('function($s) { return "Hello Lambda!" . $s; }');
    $newLine    = Lambda::Create('function($s) { return $s . "<br/>"; }');

    echo formatString('', $content, $bold, $italic, $newLine, $para) . "\n";

?>

Output

Array
(
    [0] => a
    [1] => b
)
Array
(
    [0] => 7
    [1] => 1
    [2] => 5
    [3] => 4
    [4] => 3
    [5] => 6
    [6] => 2
    [7] => 8
    [8] => 9
)
Array
(
    [5] => 6
    [6] => 7
    [7] => 8
    [8] => 9
)
Array
(
    [5] => 6
    [6] => 7
    [7] => 8
    [8] => 9
)
Hello World
<h1><bold><i>Weird Formatting</i></bold></h1>
<p><i><bold>Hello Lambda!</bold></i><br/></p>

Note:Please don’t actually use this code for anything important :-)

Posted on

explodeTree

While reading a blog post by Kevin Van Zonneveld’s on converting arrays to trees in PHP, I was shocked to see his approach to the problem. To paraphrase a popular quote:

Some people, when confronted with a problem, think
“I know, I’ll use eval.” Now they have two problems.

Below is the same function, but using a reference rather than eval to keep track of the current branch. (After reading the comments on the blog, my code is surprisingly similar to lachlan’s. Great minds, or something like that I guess :))

function explodeTree($array, $delim = '/')
{
    $tree = array();

    foreach($array as $elem)
    {
        //  Split our string up, and remove any blank items
        $items = explode($delim, $elem);
        $items = array_diff($items, array(''));

        //  current holds the current position in the tree
        $current = &$tree;

        foreach($items as $item)
        {
            //  If we've not created this branch before, or there is
            //  a leaf with the same name, then turn it into a branch
            if(!isset($current[$item]) || !is_array($current[$item]))
            {
                $current[$item] = array();
            }

            //  Update our current position to the branch we entered
            //  (or created, depending on the above if statement)
            $current = &$current[$item];
        }

        //  If the last value in this row is an array with 0 elements
        //  then (for now) we will consider it a leaf node, and set it
        //  to be equal to the string representation that got us here.
        if(count($current) == 0)
        {
            $current = $elem;
        }
    }

    return $tree;
}

Update: To quickly clarify, I didn’’’t mean the post to come over as arrogant, I think using eval() like that is ingenious. It’s just (IMHO) not the best way as it suffers greatly with readability, and have to be extra careful using eval especially with user data.

Posted on

qpsmon v0.1

This is a simple script that will print the average qps (using a specified period) for one or more MySQL databases. If doesn’t have many practical uses, but it’s fun to watch when running large imports or exports.

<?php
/*

    qpsmon.php  v0.1

    This is an (extremely) simple script that will get the average
    number of queries/second for several MySQL databases.


    Example Output:

        qpsmon v0.1
        Sample size 120 seconds.  [60 times every 2 seconds]

          Margaret   176 qps
             Yarra    11 qps
            Murray  1066 qps

*/


    error_reporting(E_ALL | E_STRICT);


    define('NUMBER_SAMPLES', 60);   //  Number of samples to keep
    define('PEROID_LENGTH', 2);     //  How often (in seconds) to take a simple
    define('AUTO_CLEAR', true);     //  Automatically clear the screen each refresh


    define('PROGRAM_NAME', 'qpsmon');
    define('PROGRAM_VERSION', '0.1');


    $databases = array
    (
        //  Define you MySQL databases here.  E.G.
        'My Server' => new MysqlConn('localhost', 'username', 'password', MysqlConn::MYSQL_VERSION_5),
    );


    class LimBuffer
    {
        private $maxSize;
        private $buffer = array();

        public function __construct($maxSize = 20)
        {
            $this->maxSize = $maxSize;
        }

        public function add($v)
        {
            $this->buffer[] = $v;

            if(count($this->buffer) > $this->maxSize)
            {
                $this->buffer = array_slice($this->buffer, -$this->maxSize);
            }
        }

        public function get()
        {
            return $this->buffer;
        }

        public function first()
        {
            return $this->buffer[0];
        }

        public function last()
        {
            return $this->buffer[count($this->buffer) - 1];
        }
    }

    class MysqlConn
    {
        const MYSQL_VERSION_4 = 1010;
        const MYSQL_VERSION_5 = 2020;

        private $host;
        private $username;
        private $password;
        private $mysqlVersion;

        private $conn = false;

        public function __construct($host, $username, $password, $mysqlVersion = self::MYSQL_VERSION_5)
        {
            if(!in_array($mysqlVersion, array(self::MYSQL_VERSION_4, self::MYSQL_VERSION_5)))
            {
                die("Invalid mysql version specified\n");
            }

            $this->host         = $host;
            $this->username     = $username;
            $this->password     = $password;
            $this->mysqlVersion = $mysqlVersion;
        }

        public function getQueryCount()
        {
            if(!$this->conn)
            {
                $this->connect();
            }

            $sql    = ($this->mysqlVersion == self::MYSQL_VERSION_4) ? ' SHOW STATUS' : 'SHOW GLOBAL STATUS';

            $query  = mysql_query($sql, $this->conn);

            while(($row = mysql_fetch_row($query)) !== false)
            {
                if($row[0] == 'Questions')
                {
                    return $row[1];
                }
            }

            return 0;
        }

        private function connect()
        {
            $this->conn = mysql_connect($this->host, $this->username, $this->password, true);
        }
    }

    function printHeader()
    {
        printf("%s v%s\n", PROGRAM_NAME, PROGRAM_VERSION);
        printf("Sample size %d seconds.  [%d times every %d seconds]\n", NUMBER_SAMPLES * PEROID_LENGTH, NUMBER_SAMPLES, PEROID_LENGTH);
        printf("\n");
    }


    //  For nicer formatting, here we get the maximum length of the
    //  databases names.
    $maxDatabaseNameSize = 0;
    foreach($databases as $name => $connection)
    {
        $maxDatabaseNameSize = (strlen($name) > $maxDatabaseNameSize) ? strlen($name) : $maxDatabaseNameSize;
    }


    if(!AUTO_CLEAR)
    {
        printHeader();
    }


    //  Our main loop
    $stats = array();

    while(true)
    {
        foreach($databases as $name => $connection)
        {
            //  First time round we need to init our stats array with
            //  a new Limit buffer with the correct sample size
            if(!isset($stats[$name]))
            {
                $stats[$name] = new LimBuffer(NUMBER_SAMPLES);
            }

            //  Add a new item, with the current time, and the total number
            //  of queries
            $stats[$name]->add(array('time' => time(), 'count' => $connection->getQueryCount()));
        }

        //  No point doing anything on the first run through
        //  Abusing scope here: $name is the from the last loop for the for statement above
        if(count($stats[$name]->get()) == 1)
        {
            sleep(PEROID_LENGTH);
            continue;
        }

        if(AUTO_CLEAR)
        {
            system("clear");
            printHeader();
        }

        foreach($stats as $name => $stat)
        {
            $first  = $stat->first();
            $last   = $stat->last();

            $period     = $last['time'] - $first['time'];
            $queryCount = $last['count'] - $first['count'];
            $qps        = $queryCount / ($period == 0 ? 1 : $period);

            printf("  %{$maxDatabaseNameSize}s %5d qps\n", $name, $qps);
        }

        printf("\n");

        //  Warn the user that we have yet to fill up out buffer
        if(count($stats[$name]->get()) != NUMBER_SAMPLES)
        {
            printf("Warming up...\n");
            printf("\n");
        }

        sleep(PEROID_LENGTH);
    }

?>

Posted on

AYBABTU

#544203

<atrus> i worked on a project once where somebody named variables defined in
        various places explicity so on one line of code, it showed up as:
<atrus> function_name($all, $urBase, $rBelong, $toUs);
<atrus> closest i've ever come to manslaughter

Posted on

WoW Progress

First trip to Kara on the weekend, being mostly a PUG we started off really poorly. We eventually replaced our off tank (who was arms :/) with a well geared (epic) tank which helped substantially. Though, we still only managed to down Huntsman and Maiden of Virtue.

I have most of the standard instances down pretty well (ran SV the other night with a naked healer without any trouble :)). Though without a good group of people, heroics seem really hits and miss (so far, mostly miss) which is a little annoying as the progression seems pretty steep after the standard 5 mans. As I (slowly) improve I’m sure it will get better.

My new progression target is Exalted with Darnassus. Tonight I managed to pick up ~4000 rep simply by doing a few very low level quests, hopefully the next 15,000 well be just as easy :-)

Huntsman

Maiden

Murmur

Posted on

Random code: Comic Calendar

I wrote this the other night in response to a question on the OCAU forums, and though that perhaps someone else might benefit from it (though please take note of the below disclaimer). The script is a really basic image archive (in this case comics), showing (and linking) the when images were created (based upon the filename). I’m not very happy with the code, but it took just over an hour and was meant as a POC more than anything.

<?php

/*

    Note: Code was quickly hacked up, there are spelling mistakes, logic errors
    and performance issues.  Be careful :-)

    - Matt_D  <buzzard@project-2501.net>

*/

?>
<html>
    <head>
        <title>Comic Archive Test</title>
        <style>
            .calendarTable
            {
                border-collapse: collapse;
                border: 1px solid gray;
                margin: 2em;
                float: left;

                /* To help the floats, float correctly :-/ */
                height: 240px;
            }

            .calendarTable .spacer
            {
                background-color: lightgray;
            }

            .calendarTable .hit
            {
                background-color: #efdfdf;
            }

            .calendarTable .hit a
            {
                text-decoration: none;
                font-weight: bold;
            }

            h2
            {
                text-align: center;
            }
        </style>
    </head>

    <body>
        <h1>Comic Archive Test</h1>

<?php

    error_reporting(E_ALL | E_STRICT);

    date_default_timezone_set("Australia/Melbourne");

    //    We find all the images matching a filemask, and turn them in a array
    //    of image deatils (i.e. extract the day/month/year)
    $imageList = getImageListDetails(glob("./images/*.*"));


    if(count($imageList) == 0)
    {
        die("No images found");
    }


    //    Display an entire years calender if there was one image made
    foreach(getYearsWithComics($imageList) as $year)
    {
        echo "<h2>Comics for $year</h2>";
        printCalenderForYear($year, $imageList);
        echo "<div style='clear: both'></div>";
    }


    //    Takes a list of filenames, and uses getDateFromImage() to get the
    //    year/month/day for each one.  Returns the a new list.
    function getImageListDetails($filenameList)
    {
        $newDetails = array();

        foreach($filenameList as $imageName)
        {
            try
            {
                $newDetails[] = getDateFromImage($imageName);
            }
            catch(Exception $e)
            {
                echo "\n<!-- Ignoring $imageName  -  " . $e->getMessage() . " -->\n";
            }
        }

        return $newDetails;
    }


    //    Loop through every image and return an array of years where atleast one
    //    image is present
    function getYearsWithComics($imageList)
    {
        $yearList = array();

        foreach($imageList as $imageDetails)
        {
            $yearList[] = $imageDetails['year'];
        }

        return array_unique($yearList);
    }


    //    Extract the ymd from a single image filename, returning an array with
    //    the keys: year, month, day, filename
    //    Throws an exception is the filename doesn't match the regex.
    function getDateFromImage($imageName)
    {
        if(preg_match("#img_(\d\d\d\d)(\d\d)(\d\d)\.#", $imageName, $matches) !== 1)
        {
            throw new Exception("$imageName doesn't not match expected format img_yyymmddd.");
        }

        return array('filename' => $imageName, 'year' => $matches[1], 'month' => $matches[2], 'day' => $matches[3]);
    }


    //    Given a year, and a list of image details.  Display some nice calender
    //    tables.
    function printCalenderForYear($year, $imageList)
    {
        $skipLookup = array
        (
            "Sun" => 0,
            "Mon" => 1,
            "Tue" => 2,
            "Wed" => 3,
            "Thu" => 4,
            "Fri" => 5,
            "Sat" => 6,
        );

        for($month = 1; $month <= 12; $month++)
        {
            $firstDay        = mktime(0, 0, 0, $month, 1, $year);

            $monthName        = date('F', $firstDay);

            $firstDayName    = date('D', $firstDay);

            $amountToSkip    = $skipLookup[$firstDayName];

            $daysInMonth    = cal_days_in_month(0, $month, $year);

            echo "<table class='calendarTable' border='1' cellpadding='4' cellspacing='0'>";
            echo "<tr><th colspan='7'>$monthName $year</th></tr>";
            echo "<tr><td>S</td><td>M</td><td>T</td><td>W</td><td>T</td><td>F</td><td>S</td></tr>";
            echo "<tr>";

            $dayOfWeek = 1;

            for($i = 0; $i < $amountToSkip; $i++)
            {
                echo "<td class='spacer'>&nbsp;</td>";    
            }

            $dayOfWeek += $amountToSkip;



            $currentDay = 1;

            while($currentDay <= $daysInMonth)
            {

                //    Check to see if there if there was an image made of this date
                $foundMatch = false;
                foreach($imageList as $imageDetails)
                {
                    if(($imageDetails['year'] == $year) && ($imageDetails['month'] == $month) && ($imageDetails['day'] == $currentDay))
                    {
                        $foundMatch = true;

                        echo "<td class='hit'><a href='{$imageDetails['filename']}'>$currentDay</a></td>";
                    }
                }


                //    If no image was found, then just display the day
                if(!$foundMatch)
                {
                    echo "<td>$currentDay</td>";
                }


                $currentDay++;
                $dayOfWeek++;

                if($dayOfWeek > 7)
                {
                    echo "</tr><tr>";
                    $dayOfWeek = 1;
                }
            }


            if($dayOfWeek != 1)
            {
                for($i = $dayOfWeek; $i <= 7; $i++)
                {
                    echo "<td class='spacer'>&nbsp;</td>";
                }
            }

            echo "</table>";
            echo "\n\n";
        }
    }

?>

    </body>
</html>

Posted on

Bad code I’m proud of

Some days I feel like I’m fighting with PHP every step (I’m sure most PHP programmers know what I mean). To stop it getting to me, I often try to come up creative ways to implement some code (often hacked up and unreadable) just to prove that I’m better than it is. :-)

The code below is used to transform rows from a database query, and has an ugly hack to work around a bug (atleast IMO) regarding the scope of functions created with `create_function`

$whoNeedsStructure = create_function('$r', 'global $__f; if(!isset($__f)) $__f = create_function(\'$m\', \'$r = DatabaseManager::get("bergamot")->query("SELECT * FROM record WHERE idDatabase IN (12,13,14) AND idPublic=?", array($m)); return $r[0]["id"];\'); return new Record580k($r["c"], $r["m"], $__f($r["m"]));');

// snip

return array_map($whoNeedsStructure, $rows)

Disclaimer: The script was a one off, and there is a comment that describes what the entire function is intended to do, so it’s not a complete maintenance nightmare.

Edit: Read post before pressing publish.

Posted on

Better anonymous functions in PHP5

I’m sure everyone knows that create_function is evil, while at the same time, so very appealing. The most significant problem are the memory leaks that occur every time `create_function()` is called (as it’s not really an anonymous function, just a randomly named function in the global scope). The below class sidesteps the problem by caching `create_function()` results, minimizing the actual number of functions that are created.

<?php

    /**
    * A create_function() wrapper to stop memory leaks when calling
    * create_function multiple times with the same arguments
    *
    * @author Matthew Davey
    */
    class AnonFunction
    {
        /**
        * 'Hash' to hold our functions.  The key is the function arguments
        * concatenated with the function body.
        *
        * @var $functions array
        * @private
        * @static
        */
        private static $functions = array();

        /**
        * Create a new function, or return a previous function
        *
        * @param string $arg function arguments
        * @param string $body function body
        * @return string name of function
        */
        public static function Create($arg, $body)
        {
            if(!isset(self::$functions[$arg . $body]))
            {
                self::$functions[$arg . $body] = create_function($arg, $body);
            }

            return self::$functions[$arg . $body];
        }
    }

?>

Example

//  New style
$f1 = AnonFunction::Create('', 'return "Hello World";');
$f2 = AnonFunction::Create('', 'return "Hello World";');

//  Pass
assert('$f1 === $f2');


//  Old style
$g1 = create_function('', 'return "Hello World";');
$g2 = create_function('', 'return "Hello World";');

//  Fail
assert('$g1 === $g2');

Posted on

WoW

Progress

Hmm, seems the the numbers I was reporting weren’t actually that useful thanks to arcane talents. After a respec back to a more conventional fire spec, I now have: +632 spell damage, +172 fire damage, and +26 more from set bonus. Giving a total (before talents) of: +830 fire damage, again, it all came at a cost of stam and int.

  1. Actually start running Karazhan… Guild doesn’t look to be able to do this any time soon, might need to swap again
  2. Enchant Enchant Weapon – Major Spellpower on my Greatsword of Horrid DreamsDone
  3. Craft Spellfire Robe to finish the Wrath of Spellfire setDone

Posted on

Fun with relative dates

$ date
Fri Sep 14 12:48:47 EST 2007

$ php -r "echo date('Y-m-d H:i:s', strtotime('Friday'));"
2007-09-14 00:00:00

$ php -r "echo date('Y-m-d H:i:s', strtotime('next Friday'));"
2007-09-21 00:00:00

$ date -d "Friday"
Fri Sep 14 00:00:00 EST 2007

$ date -d "next Friday"
Fri Sep 14 00:00:00 EST 2007

My first thought was that PHP was a POS, but according to the documentation on GNU.org (which is the same as ‘info date’) ‘date’ seems to be incorrect.

A number may precede a day of the week item to move forward supplementary weeks. It is best used in expression like `third monday’. In this context, `last day’ or `next day’ is also acceptable; they move one week before or after the day that day by itself would represent.

Posted on