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 $o, SimpleXMLElement $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($k, gmstrftime("%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($type, 0, -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'", $type, var_export($v, 1)));
}
$basetype = substr($type, 0, -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($v, get_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) == 0 || 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(0, 0, 0, 1, 1, 2005);
$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));