The Source

<?php

    
/**
    * a cache for the POPO Classes
    *
    * we have to parse the doc-comments to extract the annotations 
    * to reduce the overhead we parse them only once per Class
    */
    
class POPOClassCache {
        protected 
$vars;

        protected 
$doccomment_mapping = array();
        protected 
$doccomment_vars = array();

        protected 
$classname;
        
        function 
__construct($class) {
            
$this->classname $class;
        }

        function 
setDoccommentVars($regexes) {
            
$this->doccomment_mapping array_merge($this->doccomment_mapping$regexes);
        }

        protected function 
parseDoccomment() {
            
$o = new ReflectionClass($this->classname);

            foreach (
$this->doccomment_mapping as $field => $v) {
                
$this->doccomment_vars[$field] = array();
            }

            foreach (
$o->getProperties() as $prop) {
                
$name $prop->getName();
            
                
$dc $prop->getDocComment();

                foreach (
$this->doccomment_mapping as $field => $regex) {
                    if (
preg_match($regex$dc$match)) {
                        
$this->doccomment_vars[$field][$name] = $match[1];
                    } else {
                        
$this->doccomment_vars[$field][$name] = NULL;
                    }
                }
            
                
$this->vars[$name] = $name;
            }
        }

        function 
hasProperty($prop) {
            if (!isset(
$this->vars)) $this->parseDoccomment();

            return isset(
$this->vars[$prop]);
        }

        function 
getDoccommentVar($var$prop) {
            if (!
$this->hasProperty($prop)) return NULL;
            if (!isset(
$this->doccomment_vars[$var])) return NULL;

            return 
$this->doccomment_vars[$var][$prop];
        }

        function 
getProperties() {
            return 
$this->vars;
        }
    }

    class 
POPOClassCacheFactory {
        static 
$instances/** array of */

        
static function getInstance($class) {
            if (!isset(
self::$instances)) {
                
self::$instances = array();
            }
            
            if (!isset(
self::$instances[$class])) {
                
self::$instances[$class] = new POPOClassCache($class);
            }

            return 
self::$instances[$class];
        }
    }

    
/* a plain-old-php-object */
    
class POPO {
        function 
__construct() {
            
POPOClassCacheFactory::getInstance(get_class($this))->setDoccommentVars(
                array(
                    
"var"       => "#@var\s+([_a-zA-Z][_0-9a-zA-Z]+(?:\[\])?)#",
                    )
                );
        }

        function 
hasProperty($k) {
            return 
POPOClassCacheFactory::getInstance(get_class($this))->hasProperty($k);
        }

        function 
getPropertyType($k) {
            return 
POPOClassCacheFactory::getInstance(get_class($this))->getDoccommentVar("var"$k);
        }

        function 
getProperties() {
            return 
POPOClassCacheFactory::getInstance(get_class($this))->getProperties();
        }
    }

    class 
POPOTypeSafe extends POPO {
        function 
__construct() {
            
parent::__construct();

            
POPOClassCacheFactory::getInstance(get_class($this))->setDoccommentVars(
                array(
                    
"length"    => "#@length\s+([0-9]+)#"
                    
"validate"  => "#@validate\s+(\S+)#"
                    )
                );
        }

        function 
getPropertyValidator($k) {
            return 
POPOClassCacheFactory::getInstance(get_class($this))->getDoccommentVar("validate"$k);
        }

        function 
getPropertyLength($k) {
            return 
POPOClassCacheFactory::getInstance(get_class($this))->getDoccommentVar("length"$k);
        }

        function 
__get($k) {
            if (!
$this->hasProperty($k)) {
                throw new 
Exception(sprintf("'%s' has no property '%s'"get_class($this), $k));
            }

            if (!isset(
$this->$k)) {
                return 
NULL;
            }

            return 
$this->$k;
        }

        function 
__set($k$v) {
            if (!
$this->hasProperty($k)) {
                throw new 
Exception(sprintf("'%s' has no property '%s'"get_class($this), $k));
            }

            if (!(
$type $this->getPropertyType($k))) {
                throw new 
Exception(sprintf("'%s'.'%s' has no type set"get_class($this), $k));
            }

            if (!
$this->isValid($k$v$type)) {
                throw new 
Exception(sprintf("'%s'.'%s' = %s is not valid for '%s'"get_class($this), $k$v$type));
            }

            
$this->$k $v;
        }

        protected function 
isValid($k$v$type) {
            if (
is_null($v)) return true;

            switch (
$type) {
            case 
"int":
            case 
"timestamp":
                return (
is_numeric($v));
            case 
"string":
                return 
true;
            default:
                if (
is_array($v)) {
                    if (
substr($type, -2) != "[]") {
                        throw new 
Exception();
                    }

                    foreach (
$v as $ks => $vs) {
                        if (!
$vs instanceof POPO) {
                            throw new 
Exception(sprintf("'%s'.'%s'[%s] has invalid type: '%s'"get_class($this), $k$ks$basetype));
                        }
                    }

                    return 
true;
                } else if (
class_exists($type) && $v instanceof POPO) {
                    return 
true;
                }
                throw new 
Exception(sprintf("'%s'.'%s' has invalid type: '%s'"get_class($this), $k$type));
            }
        }
    }

    class 
SerializeXML {
        static function 
toXML(POPO $oSimpleXMLElement $parent NULL) {
            if (
is_null($parent)) {
                
$parent = new SimpleXMLElement(sprintf("<?xml version=\"1.0\"?><%s/>"get_class($o)));
            }

            foreach (
$o->getProperties() as $k) {
                
$type $o->getPropertyType($k);

                
$v $o->$k;

                if (
is_null($v)) continue;

                switch (
$type) {
                case 
"int":
                    
$parent->addChild($k, (int)$v);
                    break;
                case 
"timestamp":
                    
$parent->addChild($kgmstrftime("%Y-%m-%dT%H:%M:%SZ"$v));
                    break;
                case 
"string":
                    
$parent->addChild($k$v);
                    break;
                default:
                    if (
is_array($v)) {
                        
$ar_parent $parent->addChild($k);
                        
                        
$basetype substr($type0, -2);

                        foreach (
$v as $obj) {
                            
self::toXML($obj$ar_parent->addChild($basetype));
                        }
                    } else if (
$v instanceof POPO) {
                        
self::toXML($v$parent->addChild($k));
                    } else {
                        throw new 
Exception(sprintf("unkown type '%s'"$type));
                    }
                    break;
                }
            }

            return 
$parent;
        }
    }

    class 
SerializeSQL {
        
/**
        * escape the field-names, table-names, ...
        *
        * MySQL likes backticks around field-names others prefer quotes
        */
        
static function escapePropertyName($k) {
            return 
$k;
        }

        
/**
        * create a INSERT statement from a POPO object
        */
        
static function toINSERT(POPO $o) {
            
$fields = array();
            
$values = array();
            
$sql "";
            
$table get_class($o);

            foreach (
$o->getProperties() as $k) {
                
$type $o->getPropertyType($k);

                
$v $o->$k;


                switch (
$type) {
                case 
"int":
                    
$fields[] = self::escapePropertyName($k);
                    
$values[] = is_null($v) ? "NULL" : (int)$v;
                    break;
                case 
"timestamp":
                    
$fields[] = self::escapePropertyName($k);
                    
$values[] = is_null($v) ? "NULL" gmstrftime('"%Y-%m-%d %H:%M:%S"'$v);
                    break;
                case 
"string":
                    
$fields[] = self::escapePropertyName($k);
                    
$values[] = is_null($v) ? "NULL" '"'.addslashes($v).'"';
                    break;
                default:
                    if (
is_null($v)) break;

                    if (
substr($type, -2) == "[]") {
                        
/* looks like a array of POPOs */

                        
if (!is_array($v)) {
                            throw new 
Exception(sprintf("'%s' should have been an array, is '%s'"$typevar_export($v1)));
                        }
                        
                        
$basetype substr($type0, -2);

                        foreach (
$v as $ar) {
                            if (
$ar instanceof POPO) {
                                
$sql .= self::toINSERT($ar);
                            } else {
                                throw new 
Exception(sprintf("unkown type '%s'"$basetype));
                            }
                        }
                    } else if (
$v instanceof POPO) {
                        
$sql .= self::toINSERT($vget_class($v));
                    } else {
                        throw new 
Exception(sprintf("unkown type '%s'"$type));
                    }
                    break;
                }
            }

            
$sql .= sprintf('INSERT INTO %s (%s) VALUES (%s);'."\n",
                
self::escapePropertyName($table),
                
join($fields", "),
                
join($values", ")
            );

            return 
$sql;
        }

        
/**
        * create a CREATE statement from a POPO object
        *
        * if the object contains a @length field we use it
        */
        
static function toCREATE(POPO $o) {
            
$fields = array();
            
$table get_class($o);
            
$sql "";

            foreach (
$o->getProperties() as $k) {
                
$type $o->getPropertyType($k);
                if (
$o instanceof POPOTypeSafe) {
                    
$length $o->getPropertyLength($k);
                } else {
                    
$length NULL;
                }

                
$v $o->$k;

                switch (
$type) {
                case 
"int":
                    
$fields[] = sprintf('%s INT%s'self::escapePropertyName($k), is_null($length) ? "" '('.$length.')');
                    break;
                case 
"timestamp":
                    
$fields[] = sprintf('%s TIMESTAMP'self::escapePropertyName($k));
                    break;
                case 
"string":
                    if (
is_null($length) || $length 64000) {
                        
$fields[] = sprintf('%s TEXT'self::escapePropertyName($k));
                    } else {
                        
$fields[] = sprintf('%s VARCHAR(%d)'self::escapePropertyName($k), $length);
                    }
                    break;
                default:
                    if (
substr($type, -2) == "[]") {
                        if (!
is_array($v) && !is_null($v)) {
                            throw new 
Exception();
                        }
                        if (
count($v) == || is_null($v)) break;

                        
$sql .= self::toCREATE($v[0]);
                    } else if (
$v instanceof POPO) {
                        
$sql .= self::toCREATE($v);
                    } else if (
is_null($v) && is_subclass_of($type"POPO")) {
                        
$sql .= self::toCREATE(new $type());
                    } else {
                        throw new 
Exception(sprintf("unkown type '%s'"$type));
                    }
                    break;
                }
            }

            
$sql .= sprintf("CREATE TABLE %s (\n  %s);\n",
                
self::escapePropertyName($table),
                
join($fields",\n  ")
            );

            return 
$sql;
        }
    }

    class 
Department extends POPOTypeSafe {
        
/**
        * @var int
        */
        
protected $department_id;

        
/**
        * @var string
        * @length 32
        */
        
protected $name;

        
/**
        * look like we have many Employees
        *
        * @var Employee[]
        */
        
protected $employees;
    }

    
/**
    * a small example 
    *
    * @has_one Department
    */
    
class Employee extends POPOTypeSafe 
        
/** 
        * @var      int
        * @length   10
        * @validate 1-
        * @is_required
        */
        
protected $employee_id;

        
/** 
        * @var string
        * @length 32
        * @is_required
        */
        
protected $name;

        
/** 
        * @var string
        * @length 32
        * @is_required
        */
        
protected $surname;

        
/** 
        * @var timestamp
        */
        
protected $since;

        
/**
        * every employee should belong to ONE department
        *
        * @var Department
        */
        
protected $department;
    }

    
$d = new Department();
    
$d->name        "Enterprise Tools";

    
$e = new Employee();

    
$e->name        "Jan"
    
$e->employee_id 123;
    
$e->since       mktime(000112005);
    
$e->department  $d;

    print 
"<h2>The Source</h2>";
    
highlight_file(__FILE__);

    print 
"<h2>One Employee with is in one Department</h2>";

    print 
"<pre>";
    print 
htmlentities(SerializeXML::toXML($e)->asXML()."\n");
    print 
SerializeSQL::toINSERT($e)."\n";
    print 
SerializeSQL::toCREATE($e)."\n";
    print 
"</pre>";

    
$e->department  NULL/* unset the department */

    
$d->employees = array($e);

    print 
"<h2>One Department with has multiple Employees</h2>";
    
    print 
"<pre>";
    print 
htmlentities(SerializeXML::toXML($d)->asXML()."\n");
    print 
SerializeSQL::toINSERT($d)."\n";
    print 
SerializeSQL::toCREATE($d)."\n";
    print 
"</pre>";



One Employee with is in one Department

<?xml version="1.0"?>
<Employee><employee_id>123</employee_id><name>Jan</name><since>2004-12-31T23:00:00Z</since><department><name>Enterprise Tools</name></department></Employee>

INSERT INTO Department (department_id, name) VALUES (NULL, "Enterprise Tools");
INSERT INTO Employee (employee_id, name, surname, since) VALUES (123, "Jan", NULL, "2004-12-31 23:00:00");

CREATE TABLE Department (
  department_id INT,
  name VARCHAR(32));
CREATE TABLE Employee (
  employee_id INT(10),
  name VARCHAR(32),
  surname VARCHAR(32),
  since TIMESTAMP);

One Department with has multiple Employees

<?xml version="1.0"?>
<Department><name>Enterprise Tools</name><employees><Employee><employee_id>123</employee_id><name>Jan</name><since>2004-12-31T23:00:00Z</since></Employee></employees></Department>

INSERT INTO Employee (employee_id, name, surname, since) VALUES (123, "Jan", NULL, "2004-12-31 23:00:00");
INSERT INTO Department (department_id, name) VALUES (NULL, "Enterprise Tools");

CREATE TABLE Department (
  department_id INT,
  name VARCHAR(32));
CREATE TABLE Employee (
  employee_id INT(10),
  name VARCHAR(32),
  surname VARCHAR(32),
  since TIMESTAMP);
CREATE TABLE Department (
  department_id INT,
  name VARCHAR(32));