Thursday, August 25, 2011

Prototype-based programming

class UndefinedException extends Exception {
    public function __construct($msg, $file, $line) {
        $this->message = $msg;
        $this->file = $file;
        $this->line = $line;
    }
}

class Backtrace {
    /**
     * Return caller information from the backtrace
     */
    static public function caller() {
        $bt = debug_backtrace(0);
        $i = count($bt) - 1;
        return $bt[$i];
    }
}

class Prototype {
    public $prototype;

    public function __construct($parent = null) {
        $this->prototype = $parent;
    }

    public function __call($name, $args) {
        // Check closure
        $func = isset($this->$name) ? $this->$name : null;
        if (is_callable($func)) {
            return call_user_func_array($func, $args);
        }
        // Check prototype
        if ($this->prototype) {
            return call_user_func_array(array($this->prototype, $name), $args);
        }        
        $caller = Backtrace::caller();
        throw new UndefinedException('Undefined method: ' . $name, $caller['file'], $caller['line']);
    }

    public function __get($name) {
        if ($this->prototype) {
            return $this->prototype->$name;
        }
        $caller = Backtrace::caller();
        throw new UndefinedException('Undefined property: ' . $name, $caller['file'], $caller['line']);
    }

    final public function begetObject() {
        return new Prototype($this);
    }
}
Here is an example of using it:
$a = new Prototype();
$a->greet = function() {
    echo "Hello World!\n";
};
$b = $a->begetObject();
$b->greet(); // Prints: Hello World!

Tuesday, July 6, 2010

Proxy Class

/**
 * Wrapper around Zend_Db connection that performs logging
 */
class Zend_Db_LogWrapper {
    private $conn;
    private $logger;

    public function __construct($conn, $logFilename) {
        $this->conn = $conn;
        $this->logger = new Logger($logFilename);
    }

    public function insert($table, array $bind) {
        $this->logger->logDb($table, 'insert', $bind);
        return $this->conn->insert($table, $bind);
    }

    public function update($table, array $bind, $where = '') {
        $this->logger->logDb($table, 'update', $bind, $where);
        return $this->conn->update($table, $bind, $where);
    }

    public function delete($table, $where = '') {
        $this->logger->logDb($table, 'delete', '', $where);
        return $this->conn->delete($table, $where);
    }

    /**
     * Redirect all other calls to the wrapped connection
     */
    public function __call($name, $arguments) {
        return call_user_func_array(array($this->conn, $name), $arguments);
    }
}

Wednesday, September 10, 2008

Beware of References

I got tripped up by the following code:
// This data actually comes from csv file
$data = array(
    array('Area1', null, null),
    array(null, 'Section1', null),
    array(null, null, 'Location1'),
    array('Area2', null, null),
    array(null, 'Section2', null),
    array(null, null, 'Location2')
);
$root = array();
foreach ($data as $row) {
    if ($row[0]) { // Is Area
        $area = array();
        $root[$row[0]] =& $area;
    } elseif ($row[1]) { // Is Section
        $section = array();
        $area[$row[1]] =& $section;
    } elseif ($row[2]) { // Is Location
        $section[] = $row[2];
    }
}
print_r($root); 
Expected result:
Array(
[Area1] => Array(                     
        [Section1] => Array(
                [0] => Location1
            )               
    )
[Area2] => Array(       
        [Section2] => Array(          
                [0] => Location2
            )               
    )
)
Actual result:
Array(
[Area1] => Array(                     
        [Section2] => Array(
                [0] => Location2
            )               
    )
[Area2] => Array(       
        [Section2] => Array(          
                [0] => Location2
            )               
    )
)
So what did I do wrong? To answer this lets look at a simpler example:
$a = array();
$b =& $a;
$a[] = 'hello';
echo implode(' ', $b); // Outputs 'hello'
$a = array('world');
echo implode(' ', $b); // Outputs 'world' 
See I was expecting the last line to output 'hello' because I was thinking references were like C pointers. That is:
void* a = array();
void* b = *a;
So looking up the PHP manual it says:
They are not like C pointers; instead, they are symbol table aliases... References can be thought of as hardlinking in Unix filesystem.

Monday, September 1, 2008

Generate random password

/**
 * Generate a random character array from the list of $possible characters
 *
 * @param array  $possible Array of possible characters
 * @param int    $length   Length of string to generate
 * @return array An array of $length random characters
 */
function randomStringArray($possible, $length) {
    $stringArray = array();
    $possibleCount = count($possible);

    $i = 0;
    while ($i < $length) {
        // pick a random character from the possible ones
        $char = $possible[mt_rand(0, $possibleCount - 1)];

        // we don't want this character if it's already in the password
        if (!in_array($char, $stringArray)) {
            $stringArray[] = $char;
            $i++;
        }
    }

    return $stringArray;
}

/**
 * Generate password that contains 6 letters and 2 numbers
 */
function generatePassword() {
    $letters = array_merge(range('a', 'z'), range('A', 'Z'));
    $numbers = range('0', '9');
    $chars = array_merge(
        randomStringArray($letters, 6), randomStringArray($numbers, 2));
    shuffle($chars);
    return implode('', $chars);
}

Tuesday, August 26, 2008

Beware of Relative Includes

Say you have an application with an index.php that contains:
require_once 'include/config.php';
require_once '/path_to_lib/util/all.php';
The util library also has an include/config.php file. In util/all.php it has:
require_once 'include/config.php';
Which config file will util/all.php include? Relative paths are converted to absolute pathnames with something like:
function relativeToAbsolute($relativePath) {
    return realpath(dirname($_SERVER['SCRIPT_FILENAME']) . '/' . $relativePath);
}
Since index.php is the script that is being executed then util/all.php will include the config file in the application folder instead of the one from the util library folder. The solution is instead of relative includes use:
require_once dirname(__FILE__) . '/include/config.php';

Tuesday, July 15, 2008

Domain Model Validation

Using some magic methods can integrate validation without having to implement get and set methods everywhere.
class ValidationException extends Exception {
}

abstract class DomainModel {
    private $validationErrors = array();

    final public function __get($field) {
        $getMethodName = 'get' . ucfirst($field);
        if (method_exists($this, $getMethodName)) {
            return call_user_func(array($this, $getMethodName));
        } elseif (isset($this->$field)) {
            return $this->$field;
        }
    }

    final public function __set($field, $value) {
        $setMethodName = 'set' . ucfirst($field);
        if (array_key_exists($field, $this->validationErrors)) {
            unset($this->validationErrors[$field]);
        }
        if (method_exists($this, $setMethodName)) {
            try {
                call_user_func(array($this, $setMethodName), $value);
            } catch (ValidationException $e) {
                $this->validationErrors[$field] = $e->getMessage();
            }
        } elseif (array_key_exists($field, get_object_vars($this))) {
            $this->$field = $value;
        }
    }

    final public function isValid() {
        return count($this->validationErrors) == 0;
    }

    final public function getValidationErrors() {
        return array_values($this->validationErrors);
    }

    final public function setData($data) {
        foreach ($data as $field => $value) {
            $this->__set($field, $value);
        }
    }

    final public function getData() {
        return get_object_vars($this);
    }
}

class Test extends DomainModel {
    protected $message;

    public function setMessage($msg) {
        if ($msg == null || strlen(trim($msg)) == 0) {
            throw new ValidationException("Message must not be empty");
        }
        $this->message = $msg;
    }
}

$obj = new Test();
$obj->message = "Hello!";
echo $obj->message . "\n";
$obj->message = " ";
if (!$obj->isValid()) {
    foreach ($obj->getValidationErrors() as $error) {
        echo $error . "\n";
    }
}

Wednesday, June 18, 2008

Workaround to CSS and JS cache woes

Here is a method I found of making it so the browser receives new CSS and Javascript files when they are changed on the server. It works by appending a timestamp parameter of when the file was last modified.
function urlmtime($url) {
   $parsed_url = parse_url($url);
   $path = $parsed_url['path'];
   if ($path[0] == "/") {
       $filename = $_SERVER['DOCUMENT_ROOT'] . "/" . $path;
   } else {
       $filename = $path;
   }
   if (!file_exists($filename)) {
       // If not a file then use the current time
       $lastModified = date('YmdHis');
   } else {
       $lastModified = date('YmdHis', filemtime($filename));
   }
   if (strpos($url, '?') === false) {
       $url .= '?ts=' . $lastModified;
   } else {
       $url .= '&ts=' . $lastModified;
   }
   return $url;
}

function include_css($css_url, $media='all') {
   // According to Yahoo, using link allows for progressive 
   // rendering in IE where as @import url($css_url) does not
   echo '<link rel="stylesheet" type="text/css" media="' .
      $media . '" href="' .
      urlmtime($css_url) . '">'."\n";
}

function include_javascript($javascript_url) {
   echo '<script type="text/javascript" src="' .
     urlmtime($javascript_url) .
     '"></script>'."\n";
}