diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..e69d7b7
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+tests/settings.php
+*.swp
diff --git a/BasicObject.php b/BasicObject.php
index bb443c6..9623721 100644
--- a/BasicObject.php
+++ b/BasicObject.php
@@ -9,8 +9,65 @@ abstract class BasicObject {
protected $_old_key = array();
protected $_exists;
+ /* Cache of objects returned from FooObject::from_field()
+ * var_dump of $_from_field_cache would output something like:
+ * array(x) {
+ * ["table1:field1"]=>
+ * array(x) {
+ * ["value"]=>
+ * object(FooObject)#X (x) {
+ * ... object properties ...
+ * }
+ * }
+ * ["table1:field2"]=>
+ * array(x) {
+ * ...
+ * }
+ * }
+ */
+ protected static $_from_field_cache = array();
+
+ protected static $_enable_cache = false;
+
+ /*
+ * [ 'query' => result ]
+ */
+ protected static $_selection_cache = array();
+ protected static $_count_cache = array();
+ protected static $_sum_cache = array();
+
+ /*
+ * Memcache for caching database structure between requests
+ */
+ private static $memcache = null;
+ private static $memcache_prefix;
+
+ private static $column_ids = array();
+ private static $connection_table = array();
+ private static $tables = null;
+ private static $columns = array();
+
public static $output_htmlspecialchars;
+ /**
+ * Methods for toggling query caching on and off
+ * Default: Off
+ */
+ public static function disable_cache() {
+ BasicObject::$_enable_cache = false;
+ }
+
+ public static function enable_cache() {
+ BasicObject::$_enable_cache = true;
+ }
+
+ public static function invalidate_cache() {
+ BasicObject::$_from_field_cache = array();
+ BasicObject::$_selection_cache = array();
+ BasicObject::$_sum_cache = array();
+ BasicObject::$_count_cache = array();
+ }
+
/**
* Runs the callback with a output_htmlspecialchars temporary value set
* and returns the value that the callback returned
@@ -33,7 +90,7 @@ public static function with_tmp_htmlspecialchars($tmp_value, $callback) {
* Returns the table name associated with this class.
* @return The name of the table this class is associated with.
*/
- private static function id_name($class_name = null){
+ protected static function id_name($class_name = null){
$pk = static::primary_key($class_name);
if(count($pk) < 1) {
return null;
@@ -46,8 +103,6 @@ private static function id_name($class_name = null){
private static function primary_key($class_name = null) {
global $db;
- static $column_ids = array();
-
if(class_exists($class_name) && is_subclass_of($class_name, 'BasicObject')){
$table_name = $class_name::table_name();
} elseif($class_name == null) {
@@ -55,7 +110,7 @@ private static function primary_key($class_name = null) {
} else {
$table_name = $class_name;
}
- if(!array_key_exists($table_name, $column_ids)){
+ if(!array_key_exists($table_name, BasicObject::$column_ids)){
$stmt = $db->prepare("
SELECT
`COLUMN_NAME`
@@ -73,14 +128,71 @@ private static function primary_key($class_name = null) {
$stmt->store_result();
$stmt->bind_result($index);
- $column_ids[$table_name] = array();
+ BasicObject::$column_ids[$table_name] = array();
while($stmt->fetch()) {
- $column_ids[$table_name][] = $index;
+ BasicObject::$column_ids[$table_name][] = $index;
}
+ static::store_column_ids();
$stmt->close();
}
- return $column_ids[$table_name];
+ return BasicObject::$column_ids[$table_name];
+ }
+
+ /**
+ * Enables structure cache using the provided Memcache object
+ * The memcache instance must be connected
+ */
+ public static function enable_structure_cache($memcache, $prefix = "basicobject_") {
+ BasicObject::$memcache = $memcache;
+ BasicObject::$memcache_prefix = $prefix;
+
+ $stored = BasicObject::$memcache->get(BasicObject::$memcache_prefix . "column_ids");
+ if($stored) BasicObject::$column_ids = unserialize($stored);
+
+ $stored = BasicObject::$memcache->get(BasicObject::$memcache_prefix . "connection_table");
+ if($stored) BasicObject::$connection_table = unserialize($stored);
+
+ $stored = BasicObject::$memcache->get(BasicObject::$memcache_prefix . "tables");
+ if($stored) BasicObject::$tables = unserialize($stored);
+
+ $stored = BasicObject::$memcache->get(BasicObject::$memcache_prefix . "columns");
+ if($stored) BasicObject::$columns = unserialize($stored);
+ }
+
+ public static function clear_structure_cache($memcache, $prefix = "basicobject_") {
+ $memcache->delete($prefix . "column_ids");
+ $memcache->delete($prefix . "connection_table");
+ $memcache->delete($prefix . "tables");
+ $memcache->delete($prefix . "columns");
+ BasicObject::$column_ids = array();
+ BasicObject::$connection_table = array();
+ BasicObject::$tables = null;
+ BasicObject::$columns = array();
+ }
+
+ private static function store_column_ids() {
+ if(BasicObject::$memcache) {
+ BasicObject::$memcache->set(BasicObject::$memcache_prefix . "column_ids", serialize(BasicObject::$column_ids), 0, 0); /* no expire */
+ }
+ }
+
+ private static function store_connection_table() {
+ if(BasicObject::$memcache) {
+ BasicObject::$memcache->set(BasicObject::$memcache_prefix . "connection_table", serialize(BasicObject::$connection_table), 0, 0); /* No expire */
+ }
+ }
+
+ private static function store_tables() {
+ if(BasicObject::$memcache) {
+ BasicObject::$memcache->set(BasicObject::$memcache_prefix . "tables", serialize(BasicObject::$tables), 0, 0); /* No expire */
+ }
+ }
+
+ private static function store_columns() {
+ if(BasicObject::$memcache) {
+ BasicObject::$memcache->set(BasicObject::$memcache_prefix . "columns", serialize(BasicObject::$columns), 0, 0); /* No expire */
+ }
}
private static function unique_identifier($class_name = null) {
@@ -109,16 +221,36 @@ public function __construct($array = null, $exists=false) {
if($exists && empty($array)) {
throw new Exception("Can't create new instance marked as existing with an empty data array");
}
+ $columns = self::columns(static::table_name());
+
+ if(is_array($array)) {
+ foreach($array as $key => $value) {
+ if($key != "id" && !in_array($key, $columns)) {
+ unset($array[$key]);
+ }
+ }
+ }
+
$this->_exists = $exists;
$this->_data = $array;
}
/**
- * Clone is called on the new object once cloning is complete
+ * Creates a duplicate with all the attributes from this instance, but with id set to null, and exist set to false
+ */
+ public function duplicate() {
+ $dup = clone $this;
+ $dup->_exists = false;
+ $dup->_data[$this->id_name()]=null;
+ return $dup;
+ }
+
+ /**
+ * Called after a clone is completed.
+ * Don't do anything, but with this one undefined __call get called instead
*/
public function __clone() {
- $this->_exists = false;
- $this->_data[$this->id_name()]=null;
+
}
/**
@@ -147,6 +279,9 @@ public function __call($name, $arguments){
} else {
// They know us (multiple values)
$params[$con['COLUMN_NAME']] = $this->id;
+ if(count($arguments) == 1 && is_array($arguments[0])) {
+ $params = array_merge($arguments[0], $params);
+ }
return $name::selection($params);
}
}
@@ -208,6 +343,8 @@ protected function is_protected($name) {
* @returns bool Returns True if the value exists an is not null, false otherwise.
*/
public function __isset($name) {
+ if($name == 'id') return true;
+
if(isset($this->_data[$name])) {
return true;
}
@@ -246,8 +383,18 @@ public function __set($name, $value) {
$this->_old_key[$name] = $this->$name;
}
$this->_data[$name] = $value;
- } elseif($this->is_table($name) && $this->in_table($this->id_name($name), $this->table_name())) {
- $name = $this->id_name($name);
+ } elseif($this->is_table($name)) {
+ $connection = self::connection($name, $this->table_name());
+ if($connection && $connection['TABLE_NAME'] == $this->table_name()) {
+ $name = $connection['COLUMN_NAME'];
+ } else {
+ $other_id = self::id_name($name);
+ if($other_id != 'id' && in_array($other_id, self::columns($this->table_name()))) {
+ $name = $other_id;
+ } else {
+ throw new Exception("No connection from '{$this->table_name()}' to table '$name'");
+ }
+ }
$this->$name = $value->id;
} else {
throw new Exception("unknown property '$name'");
@@ -270,6 +417,24 @@ private function get_fresh_instance() {
return array_shift($ret);
}
+ private static function cache_clone(&$obj) {
+ if(is_array($obj)) {
+ $ret = array();
+ foreach($obj as $k=>$v) {
+ $ret[$k] = clone $v;
+ }
+ return $ret;
+ } else if($obj !== null) {
+ return clone $obj;
+ } else {
+ return null;
+ }
+ }
+
+ private static function in_cache(&$cache, $key) {
+ return isset($cache) && array_key_exists($key, $cache);
+ }
+
private static function changed($old, $cur){
if ( $old != $cur ) return true;
if ( $old === null && $cur !== null ) return true;
@@ -356,6 +521,8 @@ public function commit() {
}
$this->_data = $object->_data;
}
+
+ BasicObject::invalidate_cache();
}
/**
@@ -404,13 +571,22 @@ public function delete() {
* @return Object The Object specified by $id.
*/
public static function from_id($id){
- $id_name = static::id_name();
+ $id_name = static::id_name();
return static::from_field($id_name, $id);
}
protected static function from_field($field, $value, $type='s'){
global $db;
- $table_name = static::table_name();
+
+ $field_name = $field;
+ $table_name = static::table_name();
+ $cache_key = "$table_name:$field_name";
+
+ /* test if a cached result exists */
+ if(BasicObject::$_enable_cache && self::in_cache(BasicObject::$_from_field_cache[$cache_key], $value)){
+ return self::cache_clone(BasicObject::$_from_field_cache[$cache_key][$value]);
+ }
+
if(!self::in_table($field, $table_name)){
throw new Exception("No such column '$field' in table '$table_name'");
}
@@ -433,12 +609,28 @@ protected static function from_field($field, $value, $type='s'){
$object = new static($bind_results, true);
}
$stmt->close();
+
+ /* store result in cache */
+ if(BasicObject::$_enable_cache){
+ if(!isset(BasicObject::$_from_field_cache[$cache_key])) BasicObject::$_from_field_cache[$cache_key] = array();
+ BasicObject::$_from_field_cache[$cache_key][$value] = self::cache_clone($object);
+ }
+
return $object;
}
public static function sum($field, $params = array()) {
global $db;
$data = static::build_query($params, '*');
+
+ $cache_string = null;
+ if(BasicObject::$_enable_cache) {
+ $cache_string = implode(";", $data);
+ if(self::in_cache(BasicObject::$_sum_cache,$cache_string)) {
+ return BasicObject::$_sum_cache[$cache_string];
+ }
+ }
+
$query = array_shift($data);
$allowed_symbols=array('*', '+', '/', '-', );
if(is_array($field)) {
@@ -459,7 +651,7 @@ public static function sum($field, $params = array()) {
throw new Exception("No such column '$f' in table '".static::table_name()."'");
}
$exp .= "`$f`";
- }
+ }
$query = "SELECT SUM($exp) FROM ($query) q";
} else {
if(!self::in_table($field, static::table_name())){
@@ -479,17 +671,31 @@ public static function sum($field, $params = array()) {
$stmt->bind_result($result);
$stmt->fetch();
$stmt->close();
+
+ if(BasicObject::$_enable_cache) {
+ BasicObject::$_sum_cache[$cache_string] = $result;
+ }
+
return $result;
}
/**
* Returns the number of items matching the conditions.
- * @param $params Array Se selection for structure of $params.
+ * @param $params Array See selection for structure of $params.
* @returns Int the number of items matching the conditions.
*/
public static function count($params = array(), $debug = false){
global $db;
$data = static::build_query($params, 'count');
+
+ $cache_string = null;
+ if(BasicObject::$_enable_cache) {
+ $cache_string = implode(";", $data);
+ if(self::in_cache(BasicObject::$_count_cache,$cache_string)) {
+ return BasicObject::$_count_cache[$cache_string];
+ }
+ }
+
$query = array_shift($data);
if($debug) {
echo "
$query
\n";
@@ -507,6 +713,11 @@ public static function count($params = array(), $debug = false){
$stmt->bind_result($result);
$stmt->fetch();
$stmt->close();
+
+ if(BasicObject::$_enable_cache) {
+ BasicObject::$_count_cache[$cache_string] = $result;
+ }
+
return $result;
}
@@ -529,16 +740,34 @@ public static function count($params = array(), $debug = false){
* '@and' => array([]),
* '@order' => array(<> [, <> ...]) | <>,
* '@limit' => array(<> [, <>]),
+ * '@join[:<>]' => array(
+ * '<>[:<>]' => <> ,
+ * ...
+ * )
* )
+ *
+ * Joins: <>: The join type (eg. LEFT,RIGHT OUTER etc)
+ * This produces the join " <> JOIN <> <> <>
+ * Operator can be 'on' or 'using' (default 'on')
+ *
* @returns Array An array of Objects.
*/
public static function selection($params = array(), $debug=false){
global $db;
$data = self::build_query($params, '*');
+
+ $cache_string = null;
+ if(BasicObject::$_enable_cache) {
+ $cache_string = implode(";", $data);
+ if(self::in_cache(BasicObject::$_selection_cache,$cache_string)) {
+ return self::cache_clone( BasicObject::$_selection_cache[$cache_string]);
+ }
+ }
+
$query = array_shift($data);
$stmt = $db->prepare($query);
if(!$stmt) {
- throw new Exception("BasicObject: error parcing query: $query\n $db->error");
+ throw new Exception("BasicObject: error parsing query: $query\n $db->error");
}
foreach($data as $key => $value) {
$data[$key] = &$data[$key];
@@ -575,12 +804,17 @@ public static function selection($params = array(), $debug=false){
$ret[] = new static($tmp, true);
}
$stmt->close();
+
+ if(BasicObject::$_enable_cache) {
+ BasicObject::$_selection_cache[$cache_string] = self::cache_clone($ret);
+ }
+
return $ret;
}
private static function build_query($params, $select){
- $table_name = static::table_name();
- $id_name = static::id_name();
+ $table_name = static::table_name();
+ $id_name = static::id_name();
$joins = array();
$wheres = '';
$order = array();
@@ -589,8 +823,8 @@ private static function build_query($params, $select){
if(count($order) == 0 && strpos(strtolower($wheres),'order by') === false) {
// Set default order
- if(static::default_order() != null)
- $order[] = static::default_order();
+ if(static::default_order() != null)
+ self::handle_order(static::default_order(), $joins, $order, $table_name, self::columns($table_name));
}
$query = "SELECT ";
@@ -604,11 +838,13 @@ private static function build_query($params, $select){
$group = "";
break;
}
- $query .=
+ $query .=
"FROM\n".
" `".$table_name."`";
foreach($joins as $table => $join){
- $query .= " JOIN\n";
+ $type = isset($join['type']) ? $join['type'] : "";
+ $query .= " $type JOIN\n";
+
if(isset($join['using'])){
$query .= " `".$table."` USING (`".$join['using']."`)";
} else {
@@ -646,36 +882,15 @@ private static function handle_params($params, &$joins, &$wheres, &$order, &$tab
$value = $value['value'];
}
if($column[0] == '@'){
- $column = explode(':', $column);
- $column = $column[0];
+ $column_split = explode(':', $column);
+ $column = $column_split[0];
// special parameter
switch($column){
case '@custom_order':
$order[] = $value;
break;
case '@order':
- if(!is_array($value)){
- $value = array($value);
- }
- foreach($value as $o){
- $desc = false;
- if(substr($o,-5) == ':desc'){
- $desc = true;
- $o = substr($o, 0,-5);
- }
- $path = explode('.', $o);
- if(count($path)>1){
- $o = '`'.self::fix_join($path, $joins, $columns, $table_name).'`';
- } elseif(self::in_table($o, $table_name)){
- $o = "`$table_name`.`$o`";
- } else {
- throw new Exception("No such column '$o' in table '$table_name' (value '$value')");
- }
- if($desc){
- $o .= ' DESC';
- }
- $order[] = $o;
- }
+ self::handle_order($value, $joins, $order, $table_name, $columns);
break;
case '@limit':
if(is_numeric($value)){
@@ -713,6 +928,40 @@ private static function handle_params($params, &$joins, &$wheres, &$order, &$tab
$types .= self::handle_params($value, $joins, $where, $order, $table_name, $limit, $user_params, 'AND');
$wheres .= "(\n".substr($where, 0, -5)."\n) $glue\n";
break;
+ case '@join':
+
+ if(count($column_split) > 1) {
+ $join_type = $column_split[1];
+ } else {
+ $join_type = null;
+ }
+
+ if(!is_array($value)) {
+ throw new Exception("Join must be array");
+ }
+ foreach($value as $table => $condition) {
+ $table = explode(':', $table);
+ if(count($table) > 1) {
+ $operator = strtolower($table[1]);
+ if(! ($operator == "on" || $operator == "using") ) {
+ throw new Exception("Join operator must be 'on' or 'using'");
+ }
+ } else {
+ $operator = "on";
+ }
+ $table = $table[0];
+
+ $join = array(
+ $operator => $condition,
+ 'to' => static::table_name()
+ );
+ if($join_type != null) {
+ $join['type'] = $join_type;
+ }
+
+ $joins[$table] = $join;
+ }
+ break;
default:
throw new Exception("No such operator '".substr($column,1)."' (value '$value')");
}
@@ -783,8 +1032,8 @@ private static function handle_params($params, &$joins, &$wheres, &$order, &$tab
* By default this method performs commit() on the object before it is returned, but that
* can be turned of (see @param $options)
*
- * @param $array An assoc array (for example from postdata) with $field_name=>$value.
- * If ["id"] or [id_name] is set the model is marked as existing,
+ * @param $array An assoc array (for example from postdata) with $field_name=>$value.
+ * If ["id"] or [id_name] is set the model is marked as existing,
* otherwise it is treated as a new object.
*
* Note: To use this method with checkboxes a hidden field with the same name and value
@@ -811,7 +1060,7 @@ public static function update_attributes($array, $options=array()) {
$obj = new static($array);
//Change [id] to [id_name] if [id] is set but id_name()!='id'
- if($obj->id_name() != "id"
+ if($obj->id_name() != "id"
&& isset($obj->_data['id'])
&& !is_null($obj->_data['id'])
&& !empty($obj->_data['id'])
@@ -822,7 +1071,7 @@ public static function update_attributes($array, $options=array()) {
//Prevent errors where the id field has another name and ['id'] is null
unset($obj->_data['id']);
}
-
+
$id = $obj->id;
if($id!=null && $id!="") {
@@ -830,7 +1079,7 @@ public static function update_attributes($array, $options=array()) {
$obj->_data = array_merge($old_obj->_data,$obj->_data);
$obj->_exists = true; //Mark as existing
}
-
+
if(!isset($options["commit"]) || $options["commit"] == true) {
$obj->commit();
}
@@ -840,8 +1089,7 @@ public static function update_attributes($array, $options=array()) {
private static function columns($table){
global $db;
- static $columns = array();
- if(!isset($columns[$table])){
+ if(!isset(BasicObject::$columns[$table])){
if(!self::is_table($table)){
throw new Exception("No such table '$table'");
}
@@ -859,11 +1107,12 @@ private static function columns($table){
$stmt->store_result();
$stmt->bind_result($column);
while($stmt->fetch()){
- $columns[$table][] = $column;
+ BasicObject::$columns[$table][] = $column;
}
$stmt->close();
+ BasicObject::store_columns();
}
- return $columns[$table];
+ return BasicObject::$columns[$table];
}
private static function operator($expr){
@@ -887,8 +1136,9 @@ private static function operator($expr){
private static function is_table($table){
global $db;
- static $tables;
- if(!isset($tables)){
+ if(!isset(BasicObject::$tables)){
+ BasicObject::$tables = array();
+
$db_name = static::get_database_name();
$stmt = $db->prepare("
SELECT `table_name`
@@ -900,49 +1150,56 @@ private static function is_table($table){
$stmt->store_result();
$stmt->bind_result($table_);
while($stmt->fetch()){
- $tables[] = strtolower($table_);
+ BasicObject::$tables[] = strtolower($table_);
}
$stmt->close();
+ BasicObject::store_tables();
}
- return in_array(strtolower($table), $tables);
+ return in_array(strtolower($table), BasicObject::$tables);
}
private static function fix_join($path, &$joins, $parent_columns, $parent){
$first = array_shift($path);
+
if(class_exists($first) && is_subclass_of($first, 'BasicObject')){
$first = $first::table_name();
}
-
+
if(!self::is_table($first)){
throw new Exception("No such table '$first'");
}
- $connection = self::connection($first, $parent);
+
$columns = self::columns($first);
- if($connection){
- $joins[$first] = array(
- 'to' => $parent,
- 'on' => "`{$connection['TABLE_NAME']}`.`{$connection['COLUMN_NAME']}` = `{$connection['REFERENCED_TABLE_NAME']}`.`{$connection['REFERENCED_COLUMN_NAME']}`"
- );
- } else {
- $parent_id = self::id_name($parent);
- $first_id = self::id_name($first);
- if(in_array($first_id, $parent_columns)){
- $joins[$first] = array(
- "to" => $parent,
- "on" => "`$parent`.`$first_id` = `$first`.`$first_id`");
- } elseif(in_array($parent_id, $columns)) {
+
+ if(!isset($joins[$first])) {
+ $connection = self::connection($first, $parent);
+ if($connection){
$joins[$first] = array(
- "to" => $parent,
- "on" => "`$parent`.`$parent_id` = `$first`.`$parent_id`");
+ 'to' => $parent,
+ 'on' => "`{$connection['TABLE_NAME']}`.`{$connection['COLUMN_NAME']}` = `{$connection['REFERENCED_TABLE_NAME']}`.`{$connection['REFERENCED_COLUMN_NAME']}`"
+ );
} else {
- throw new Exception("No connection from '$parent' to table '$first'");
+ $parent_id = self::id_name($parent);
+ $first_id = self::id_name($first);
+ if(in_array($first_id, $parent_columns)){
+ $joins[$first] = array(
+ "to" => $parent,
+ "on" => "`$parent`.`$first_id` = `$first`.`$first_id`");
+ } elseif(in_array($parent_id, $columns)) {
+ $joins[$first] = array(
+ "to" => $parent,
+ "on" => "`$parent`.`$parent_id` = `$first`.`$parent_id`");
+ } else {
+ throw new Exception("No connection from '$parent' to table '$first'");
+ }
}
}
+
if(count($path) == 1) {
$key = array_shift($path);
if(!in_array($key, $columns)){
throw new Exception("No such column '$key' in table '$first'");
- }
+ }
return $first.'`.`'.$key;
} else {
return self::fix_join($path, $joins, $columns, $first);
@@ -950,11 +1207,7 @@ private static function fix_join($path, &$joins, $parent_columns, $parent){
}
private static function in_table($column, $table){
- static $tables = array();
- if(!isset($tables[$table])){
- $tables[$table] = self::columns($table);
- }
- return in_array($column, $tables[$table]);
+ return in_array($column, self::columns($table));
}
/**
@@ -986,21 +1239,20 @@ public static function one($params = array()) {
private static function connection($table1, $table2) {
global $db;
- static $data;
if(strcmp($table1, $table2) < 0){
$tmp = $table1;
$table1 = $table2;
$table2 = $tmp;
}
- if(!isset($data[$table1]) || !isset($data[$table1][$table2])){
- $data[$table1][$table2] = array();
+ if(!isset(BasicObject::$connection_table[$table1]) || !isset(BasicObject::$connection_table[$table1][$table2])){
+ BasicObject::$connection_table[$table1][$table2] = array();
$stmt = $db->prepare("
SELECT
`key_column_usage`.`TABLE_NAME`,
`COLUMN_NAME`,
`REFERENCED_TABLE_NAME`,
`REFERENCED_COLUMN_NAME`
- FROM
+ FROM
`information_schema`.`table_constraints` join
`information_schema`.`key_column_usage` using (`CONSTRAINT_NAME`, `CONSTRAINT_SCHEMA`)
WHERE
@@ -1021,19 +1273,21 @@ private static function connection($table1, $table2) {
$stmt->execute();
$stmt->store_result();
$stmt->bind_result(
- $data[$table1][$table2]['TABLE_NAME'],
- $data[$table1][$table2]['COLUMN_NAME'],
- $data[$table1][$table2]['REFERENCED_TABLE_NAME'],
- $data[$table1][$table2]['REFERENCED_COLUMN_NAME']
+ BasicObject::$connection_table[$table1][$table2]['TABLE_NAME'],
+ BasicObject::$connection_table[$table1][$table2]['COLUMN_NAME'],
+ BasicObject::$connection_table[$table1][$table2]['REFERENCED_TABLE_NAME'],
+ BasicObject::$connection_table[$table1][$table2]['REFERENCED_COLUMN_NAME']
);
if(!$stmt->fetch()){
- $data[$table1][$table2] = false;
+ BasicObject::$connection_table[$table1][$table2] = false;
} else if($stmt->num_rows > 1) {
throw new Exception("Ambigious database, can't tell which relation between $table1 and $table2 to use. Remove one relation or override __get.");
}
$stmt->close();
+
+ BasicObject::store_connection_table();
}
- return $data[$table1][$table2];
+ return BasicObject::$connection_table[$table1][$table2];
}
private static function get_database_name() {
@@ -1050,6 +1304,34 @@ private static function get_database_name() {
return $db_name;
}
+ /**
+ * Helper method for handling @order and default_order
+ */
+ private static function handle_order($value,&$joins, &$order, &$table_name, $columns) {
+ if(!is_array($value)){
+ $value = array($value);
+ }
+ foreach($value as $o){
+ $desc = false;
+ if(substr($o,-5) == ':desc'){
+ $desc = true;
+ $o = substr($o, 0,-5);
+ }
+ $path = explode('.', $o);
+ if(count($path)>1){
+ $o = '`'.self::fix_join($path, $joins, $columns, $table_name).'`';
+ } elseif(self::in_table($o, $table_name)){
+ $o = "`$table_name`.`$o`";
+ } else {
+ throw new Exception("No such column '$o' in table '$table_name' (value '$value')");
+ }
+ if($desc){
+ $o .= ' DESC';
+ }
+ $order[] = $o;
+ }
+ }
+
public function __toString() {
$content = array();
foreach(self::columns($this->table_name()) as $c) {
@@ -1059,5 +1341,6 @@ public function __toString() {
return get_class($this). "{".implode(", ",$content)."}";
}
}
+
class UndefinedMemberException extends Exception{}
class UndefinedFunctionException extends Exception{}
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..36375a0
--- /dev/null
+++ b/README.md
@@ -0,0 +1,13 @@
+BasicObject
+==========
+
+Tests
+---------
+Run tests before you do a pull requests, and add tests for new features.
+
+See tests/README.md
+
+Dependencies (Only for running tests)
+--------------------
+
+* PHPUnit: https://github.com/sebastianbergmann/phpunit/
diff --git a/ValidatingBasicObject.php b/ValidatingBasicObject.php
index ae152ea..0e86582 100644
--- a/ValidatingBasicObject.php
+++ b/ValidatingBasicObject.php
@@ -2,11 +2,11 @@
/**
* This is a variant of basic object that has validations.
*
- * Validations are added to the model by implementing
+ * Validations are added to the model by implementing
* protected function validation_hooks() (void)
- *
+ *
* In this function you can either use predefined validations by calling
- * $this->predefined_validation_name('column_name') (see below)
+ * $this->predefined_validation_name('column_name') (see below)
* or by creating your own validations.
*
* To indicate an error call $this->add_error($variable_name,$error_msg)
@@ -35,11 +35,11 @@
*
* validate_lenght_of($var)
*
- * Validates the lenght of $var
+ * Validates the lenght of $var
* If no options are set no check is made
* If more than one options are set, multiple checks will be made
* options:
- * is: Lenght must be exactly this value
+ * is: Lenght must be exactly this value
* minimum: Lenght must be at least this value
* maximum: Lenght must be at most this value
* ------------------------
@@ -103,7 +103,7 @@ public function has_errors() {
protected function validation_hooks() {}
public function add_error($var, $msg) {
- //if(!isset($this->errors[$var]))
+ //if(!isset($this->errors[$var]))
//$this->errors[$var] = array();
$this->errors[$var][] = $msg;
}
@@ -145,11 +145,11 @@ protected function validate_numericality_of($var,$options=array()) {
if(isset($options['allow_null']) && $options['allow_null'] && $this->$var == null)
return;
- if(isset($options['only_integers']) && $options['only_integers']) {
+ if(isset($options['only_integers']) && $options['only_integers']) {
if(!is_numeric($this->$var) || preg_match('/\A[+-]?\d+\Z/',$this->$var)!=1) {
$message = "måste vara ett heltal";
$this->add_error($var,isset($options['message'])?$options['message']:$message);
- }
+ }
} else if(!is_numeric($this->$var)){
$message = "måste vara ett nummer";
$this->add_error($var,isset($options['message'])?$options['message']:$message);
@@ -168,6 +168,9 @@ protected function validate_numericality_of($var,$options=array()) {
protected function validate_length_of($var,$options=array()) {
$len = strlen($this->$var);
+ if ( isset($options['min']) ) $options['minimum'] = $options['min'];
+ if ( isset($options['max']) ) $options['maximum'] = $options['max'];
+
if(isset($options['is']) && $len != $options['is']) {
$message = "måste vara exakt {$options['is']} tecken lång";
$this->add_error($var,isset($options['message'])?$options['message']:$message);
@@ -190,13 +193,15 @@ protected function validate_length_of($var,$options=array()) {
* minimum: Smallest allowed value
* maximum: Largest allowed value
*/
-
protected function validate_in_range($var,$options=array()) {
- if(isset($options['minimum']) && $options['minimum'] >= $this->$var) {
- $message = "måste vara minst {$options['minimum']}";
+ if ( isset($options['min']) ) $options['minimum'] = $options['min'];
+ if ( isset($options['max']) ) $options['maximum'] = $options['max'];
+
+ if(isset($options['minimum']) && $options['minimum'] > $this->$var ) {
+ $message = "måste vara minst {$options['minimum']}";
$this->add_error($var,isset($options['message'])?$options['message']:$message);
}
- if(isset($options['maximum']) && $options['maximum'] <= $this->$var) {
+ if(isset($options['maximum']) && $options['maximum'] < $this->$var) {
$message = "får inte vara större än {$options['maximum']}";
$this->add_error($var,isset($options['message'])?$options['message']:$message);
}
@@ -251,6 +256,20 @@ protected function validate_equal_to($var,$val,$options=array()) {
$this->add_error($var,isset($options['message'])?$options['message']:$message);
}
}
+
+ /**
+ * Validates that $var is unique
+ */
+ protected function validate_uniqueness_of($var,$options=array()) {
+ $sel = array($var => $this->$var, '@limit' => 1);
+ if($this->_exists) {
+ $sel[static::id_name().":!="] = $this->id;
+ }
+ if(static::count($sel) != 0) {
+ $message = "måste vara unik";
+ $this->add_error($var,isset($options['message'])?$options['message']:$message);
+ }
+ }
}
/**
diff --git a/tests/Blueprint.php b/tests/Blueprint.php
new file mode 100644
index 0000000..13a3cf2
--- /dev/null
+++ b/tests/Blueprint.php
@@ -0,0 +1,144 @@
+create($set, $name, $commit);
+ }
+
+ private static $blueprints = array();
+
+ private static function find_blueprint($class) {
+
+ if(isset(Blueprint::$blueprints[$class])) return Blueprint::$blueprints[$class];
+
+ $dir = realpath(dirname(__FILE__) . "/" . Blueprint::$blueprints_dir) . "/" ;
+ if(file_exists("$dir/$class.json")) {
+ Blueprint::$blueprints[$class] = new Blueprint($class, "$dir/$class.json");
+ return Blueprint::$blueprints[$class];
+ } else {
+ throw new BlueprintException("Couldn't find blueprint: $dir/$class.json");
+ }
+ }
+
+ private $class_name, $data;
+
+ private function create($set, $name, $commit) {
+ if(!isset($this->data[$name])) {
+ throw new BlueprintException("Unknow blueprint '$name' for {$this->class_name}");
+ }
+ $data = array_merge($this->data[$name], $set);
+
+ $attr = array(
+ 'sn' => Blueprint::$sn++
+ );
+
+ $class_name = $this->class_name;
+
+ $obj = new $class_name;
+
+ foreach($data as $key => $value) {
+ if(is_string($value)) {
+ $obj->$key = preg_replace_callback("/#\{(.+?)\}/", function($matches) use ($attr, $data) {
+ $k = $matches[1];
+ $replace = false;
+ if(isset($attr[$k])) {
+ $replace = $attr[$k];
+ } else {
+ $replace = $obj->$k;
+ }
+ return $replace;
+ }, $value);
+ } else if(is_array($value)) {
+ $blueprint = "default";
+ $class = $key;
+ $values = array();
+ if(isset($value['blueprint'])) {
+ $blueprint = $value['blueprint'];
+ unset($value['blueprint']);
+ }
+
+ if(isset($value['class'])) {
+ $class = $value['class'];
+ unset($value['class']);
+ }
+
+ if(isset($value['values'])) {
+ $values = $value['values'];
+ unset($value['values']);
+ }
+
+ $values = array_merge($values, $value);
+ $obj->$key = Blueprint::make($class, $blueprint, $values, true);
+ } else {
+ $obj->$key = $value;
+ }
+ }
+
+ if($commit) $obj->commit();
+
+ return $obj;
+ }
+
+ private function __construct($class_name, $path) {
+ $this->class_name = $class_name;
+
+ $contents = file_get_contents($path);
+
+ $contents = preg_replace('/^(\s+)(\w+):/m', '$1"$2":', $contents);
+
+ $this->data = json_decode($contents, true);
+
+ if($this->data === NULL) {
+ // Define the errors.
+ $constants = get_defined_constants(true);
+ $json_errors = array();
+ foreach ($constants["json"] as $name => $value) {
+ if (!strncmp($name, "JSON_ERROR_", 11)) {
+ $json_errors[$value] = $name;
+ }
+ }
+
+ throw new BlueprintException("JSON parse error: {$json_errors[json_last_error()]}.\nParsed source: \n" . var_export($contents, true));
+ }
+ }
+}
+
+class BlueprintException extends Exception {}
diff --git a/tests/DatabaseTestCase.php b/tests/DatabaseTestCase.php
new file mode 100644
index 0000000..4e9d386
--- /dev/null
+++ b/tests/DatabaseTestCase.php
@@ -0,0 +1,25 @@
+connect($memcache_settings['host'], $memcache_settings['port']) === false) {
+ trigger_error("Unable to connect to memcache at ".$memcache_settings['host']." on port ".$memcache_settings['port'], E_USER_WARNING);
+ throw new Exception("Failed to connect");
+ }
+ } else {
+ throw new Exception("Failed to connect");
+ }
+ }
+
+ public static function get_instance() {
+ if(empty(self::$instance)) {
+ self::$instance = new MC();
+ }
+ return self::$instance;
+ }
+}
diff --git a/tests/README.md b/tests/README.md
new file mode 100644
index 0000000..e94513d
--- /dev/null
+++ b/tests/README.md
@@ -0,0 +1,24 @@
+PHPUnit
+======
+
+The tests requires phpunit: https://github.com/sebastianbergmann/phpunit/
+Install it by running
+
+ sudo ./install_phpunit.sh
+
+Tests
+=====
+
+To run the tests:
+
+ ./tests.sh
+
+To create new tests, add files to suites/ (see exists tests for examples),
+and optionally edit phpunit.xml if you create a new suite
+
+Blueprints
+=======
+
+To ease creation of tests there are a Blueprint-class for creating models.
+Documentation on that is located in the blueprints-directory.
+I recommend using Blueprints for your own projects BO-related tests too.
diff --git a/tests/blueprints/Model1.json b/tests/blueprints/Model1.json
new file mode 100644
index 0000000..11594da
--- /dev/null
+++ b/tests/blueprints/Model1.json
@@ -0,0 +1,11 @@
+{
+ default: {
+ int1: "100#{sn}",
+ str1: "str-#{sn}"
+ },
+ with_model2: {
+ int1: "100#{sn}",
+ str1: "str-#{sn}",
+ Model2: {}
+ }
+}
diff --git a/tests/blueprints/Model2.json b/tests/blueprints/Model2.json
new file mode 100644
index 0000000..5a8797a
--- /dev/null
+++ b/tests/blueprints/Model2.json
@@ -0,0 +1,6 @@
+{
+ default: {
+ int1: "200#{sn}",
+ str1: "A String #{sn}"
+ }
+}
diff --git a/tests/blueprints/README.md b/tests/blueprints/README.md
new file mode 100644
index 0000000..e7f74fe
--- /dev/null
+++ b/tests/blueprints/README.md
@@ -0,0 +1,127 @@
+Introduction
+======
+Blueprints are a handy way to create models for your tests.
+
+Usage
+========
+
+Blueprints
+----------
+For each class you want to have a blueprint, create a file {class_name}.json here.
+(ex for class "User" create "User.json")
+
+A simple blueprint might look like this:
+
+ _blueprints/User.json_
+
+ {
+ default: {
+ username: "Foobar",
+ real_name: "Foo Bar"
+ }
+ }
+
+You can now create a user with:
+
+ Blueprint::make('User');
+
+The "default" is the name of the blueprint. You should always have at least a default blueprint for each model,
+but you can create more blueprints with different names, to use in different situations, eg:
+
+ {
+ default: {
+ ...
+ },
+ foobar: {
+ ...
+ },
+ baz: {
+ ...
+ }
+
+To use a named blueprint instead of the default, send the name as a argument to make:
+
+ Blueprint::make('User', 'foobar');
+
+You can also override the value set by the blueprint by sending new values as a array to make:
+
+ Blueprint::make('User', array('username' => 'Baz'));
+
+By default make commits the model to the database, but you can tell it not to:
+
+ Blueprint::make('User', false);
+
+You can use all or none of the optional arguments (name, value, commit) to make at any call,
+and they can be in any order, but this order is recommended:
+
+ Blueprint::make('User', {name}, {values}, {commit})
+
+Unique Attributes
+---------------
+
+For attributes that need to be unique you can add #{sn} to the value to get a unique serial number:
+
+ {
+ default: {
+ username: "User-#{sn}",
+ ...
+ }
+ }
+
+The sn remains the same for the whole object
+
+Reusing attribute values
+-----------------
+
+You can also use #{other_field_name} which will yield the value of that field. Note that
+all fields are set in the order they are defined, so you can only refeere to a previously declared value:
+
+ {
+ default: {
+ username: "User-#{sn}",
+ foobar: "#{username}"
+ }
+ }
+
+Associations
+---------------
+
+If your object have associations with other objects, you can do like this:
+
+ {
+ default: {
+ Model2: {}
+ }
+ }
+
+This will set the field Model2 to a new blueprint instance of the class Model2.
+You can also specify attributes:
+* blueprint: The name of the blueprint to use
+* class: The name of the other class
+* values: Values to set
+
+Any other key/value-pair will be used as values as well, so this works:
+
+ other_model: {
+ blueprint: 'test',
+ class: "Model2",
+ derp: "Foobar"
+ }
+
+Thus you only need to use the values field if you need to specify a field named blueprint, class or values.
+
+JSON modifications
+-----------
+Unfortunatly foo: "bar" is not correct json syntax, the correct way to do this would be "foo": "bar",
+but it's just annoying to have to write the extra quotation-marks for each attributes.
+
+To prevent errors this is though only done if the attribute is on the beginning of the row, so this works:
+
+ foo: "bar",
+ baz: "derp"
+
+But this will give a syntax error:
+
+ foo: {class: "bar", blueprint: "derp"}
+
+
diff --git a/tests/database.php b/tests/database.php
new file mode 100644
index 0000000..12555d9
--- /dev/null
+++ b/tests/database.php
@@ -0,0 +1,95 @@
+query("DROP DATABASE `{$db_settings['database']}`");
+ $db->query("CREATE DATABASE `{$db_settings['database']}`");
+ db_select_database();
+ db_run_file("db.sql");
+ BasicObject::clear_structure_cache(MC::get_instance(), "bo_unit_test_");
+}
+
+function db_select_database() {
+ global $db, $db_settings;
+ $db->select_db($db_settings['database']);
+}
+
+function db_run_file($filename) {
+ global $db;
+ $handle = fopen(realpath(dirname(__FILE__) . "/" . $filename ), "r");
+ $contents = fread($handle, filesize($filename));
+ fclose($handle);
+
+ if(!$db->multi_query($contents)) {
+ throw new Exception("Failed to execute query: {$db->error}\n");
+ }
+
+
+ do {
+ $result = $db->use_result();
+ if($result) $result->free();
+ } while($db->more_results() && $db->next_result());
+}
+
+function db_query($query) {
+ global $db;
+ if(!$db->query($db)) {
+ throw new Exception("Failed execute manual query '$query': ".$db->error);
+ }
+}
+
+function db_close() {
+ global $db, $db_settings;
+ $db->close();
+}
+
+/**
+ * Counting database class
+ */
+
+class CountingDB extends MySQLi {
+
+ public static $queries = 0;
+
+ public function __construct($host, $username, $password, $database, $port) {
+ parent::__construct($host, $username, $password, $database, $port);
+ }
+
+ public function prepare($query) {
+ return new CountingStatement($this, $query);
+ }
+}
+
+class CountingStatement extends mysqli_stmt {
+ public function __construct($db, $query) {
+ parent::__construct($db, $query);
+ }
+
+ public function execute() {
+ ++CountingDB::$queries;
+ return parent::execute();
+ }
+}
diff --git a/tests/db.sql b/tests/db.sql
new file mode 100644
index 0000000..0aaeee7
--- /dev/null
+++ b/tests/db.sql
@@ -0,0 +1,19 @@
+CREATE TABLE IF NOT EXISTS `model1` (
+ `id` int(11) NOT NULL AUTO_INCREMENT,
+ `int1` int(11),
+ `str1` varchar(64),
+ `model2_id` int(11) DEFAULT NULL,
+ PRIMARY KEY (`id`),
+ KEY `model2_id` (`model2_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=latin1 AUTO_INCREMENT=1 ;
+
+CREATE TABLE IF NOT EXISTS `model2` (
+ `id` int(11) NOT NULL AUTO_INCREMENT,
+ `int1` int(11) DEFAULT NULL,
+ `str1` varchar(128),
+ PRIMARY KEY (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=latin1 AUTO_INCREMENT=1 ;
+
+ALTER TABLE `model1`
+ ADD CONSTRAINT `model1_ibfk_1` FOREIGN KEY (`model2_id`) REFERENCES `model2` (`id`) ON DELETE CASCADE ON UPDATE CASCADE;
+
diff --git a/tests/helpers.php b/tests/helpers.php
new file mode 100644
index 0000000..72b5f65
--- /dev/null
+++ b/tests/helpers.php
@@ -0,0 +1,24 @@
+
+
+
+
+ suites/BasicObject/BasicTest.php
+ suites/BasicObject/SelectionTest.php
+
+
+
+ suites/BasicObject
+ suites/BasicObject/BasicTest.php
+ suites/BasicObject/SelectionTest.php
+
+
+
+
diff --git a/tests/settings.php.sample b/tests/settings.php.sample
new file mode 100644
index 0000000..18ed4fc
--- /dev/null
+++ b/tests/settings.php.sample
@@ -0,0 +1,14 @@
+'localhost',
+ 'port'=>'3306',
+ 'username'=>'root',
+ 'password'=>'',
+ 'database'=>'basicobject-testing',
+ 'charset'=>'utf8'
+);
+
+$memcache_settings = array(
+ 'host' => 'localhost',
+ 'port' => 11211
+);
diff --git a/tests/suites/BasicObject/BasicTest.php b/tests/suites/BasicObject/BasicTest.php
new file mode 100644
index 0000000..2b3dc25
--- /dev/null
+++ b/tests/suites/BasicObject/BasicTest.php
@@ -0,0 +1,119 @@
+query("INSERT INTO `model1` SET `int1` = 5, `str1` = 'foobar'")) {
+ throw new Exception("Failed to insert model by manual query: ".$db->error);
+ }
+ $id = $db->insert_id;
+ $obj = Model1::from_id($id);
+ $this->assertNotNull($obj);
+ $this->assertEquals(5, $obj->int1);
+ $this->assertEquals("foobar", $obj->str1);
+ }
+
+ /**
+ * @depends testFromId
+ */
+ public function testInsert() {
+ $model1 = new Model1();
+ $model1->int1 = 1;
+ $model1->str1 = "Test";
+ $model1->commit();
+
+ $this->assertNotNull($model1->id);
+
+ $id = $model1->id;
+ unset($model1);
+
+ $model1 = Model1::from_id($id);
+
+ $this->assertEquals(1, $model1->int1);
+ $this->assertEquals("Test", $model1->str1);
+ }
+
+ /**
+ * @depends testFromId
+ */
+ public function testArrayInsert() {
+ $model1 = new Model1(array('int1'=>1, 'str1' => "Test"));
+ $model1->commit();
+
+ $this->assertNotNull($model1->id);
+
+ $id = $model1->id;
+ unset($model1);
+
+ $model1 = Model1::from_id($id);
+
+ $this->assertEquals(1, $model1->int1);
+ $this->assertEquals("Test", $model1->str1);
+ }
+
+ /**
+ * @depends testInsert
+ */
+ public function testIsset() {
+ $model = Blueprint::make('Model1', false);
+ $this->assertTrue(isset($model->id), 'id');
+ $this->assertTrue(isset($model->int1), 'int1');
+ $this->assertTrue(isset($model->str1), 'str1');
+ $this->assertFalse(isset($model->foobar));
+ }
+
+ /**
+ * @depends testInsert
+ */
+ public function testDelete() {
+ $model = Blueprint::make('Model1');
+ $id = $model->id;
+
+ $model->delete();
+
+ $model = Model1::from_id($id);
+ $this->assertNull($model);
+ }
+
+ /**
+ * @depends testInsert
+ */
+ public function testSelection() {
+ $key = 'selection_test';
+ $models = array(
+ Blueprint::make('Model1', array('str1' => $key)),
+ Blueprint::make('Model1', array('str1' => $key)),
+ );
+ $res = Model1::selection(array('str1' => $key));
+ $this->assertCount(count($models), $res);
+
+ $this->assertTrue(compare_result($res, $models));
+ }
+
+ /**
+ * @depends testSelection
+ */
+ public function testSum() {
+ $sum = 0;
+ $key = "sumtest";
+ for($i = 0; $i < 100; ++$i) {
+ Blueprint::make('Model1', array('str1' => $key, 'int1' => $i));
+ $sum += $i;
+ }
+ $this->assertEquals($sum, Model1::sum('int1', array('str1' => $key)));
+ }
+
+ /**
+ * @depends testSelection
+ */
+ public function testCount() {
+ $key = "counttest";
+ for($i = 0; $i < 50; ++$i) {
+ Blueprint::make('Model1', array('str1' => $key));
+ }
+ $this->assertEquals(50, Model1::count(array('str1' => $key)));
+ }
+
+}
+
diff --git a/tests/suites/BasicObject/CacheTest.php b/tests/suites/BasicObject/CacheTest.php
new file mode 100644
index 0000000..d6d5769
--- /dev/null
+++ b/tests/suites/BasicObject/CacheTest.php
@@ -0,0 +1,151 @@
+id);
+ $val = $m1->int1 + 10;
+ $m1->int1 = $val;
+ $m1->commit();
+
+ $m1 = Model1::from_id($m1->id);
+ $this->assertEquals($val, $m1->int1);
+ }
+
+ public function testQueryReductionFromId() {
+ CountingDB::$queries = 0;
+
+ $m1 = Blueprint::make('Model1');
+ $id = $m1->id;
+
+ Model1::from_id($id);
+ $num_queries = CountingDB::$queries;
+ for($i=0; $i<100; ++$i) {
+ Model1::from_id($id);
+ }
+
+ $this->assertEquals($num_queries, CountingDB::$queries);
+ }
+
+ public function testQueryReductionSelection() {
+ CountingDB::$queries = 0;
+
+ $m1 = Blueprint::make('Model1');
+
+ Model1::selection(array('int1' => $m1->int1));
+ $num_queries = CountingDB::$queries;
+ for($i=0; $i<100; ++$i) {
+ Model1::selection(array('int1' => $m1->int1));
+ }
+
+ $this->assertEquals($num_queries, CountingDB::$queries);
+ }
+
+ public function testQueryReductionCount() {
+ CountingDB::$queries = 0;
+
+ $m1 = Blueprint::make('Model1');
+
+ Model1::count(array('int1' => $m1->int1));
+ $num_queries = CountingDB::$queries;
+ for($i=0; $i<100; ++$i) {
+ Model1::count(array('int1' => $m1->int1));
+ }
+
+ $this->assertEquals($num_queries, CountingDB::$queries);
+ }
+
+ public function testQueryReductionSum() {
+ CountingDB::$queries = 0;
+
+ $m1 = Blueprint::make('Model1');
+
+ Model1::sum('int1', array('int1' => $m1->int1));
+ $num_queries = CountingDB::$queries;
+ for($i=0; $i<100; ++$i) {
+ Model1::sum('int1', array('int1' => $m1->int1));
+ }
+
+ $this->assertEquals($num_queries, CountingDB::$queries);
+ }
+
+ public function testStructureCacheFill() {
+ $sc_vars = $this->getStructureCacheVariables();
+
+ Model1::from_id(1);
+ Model1::selection(array('model2.int1' => 1));
+ $vals = array();
+ foreach($sc_vars as $v => $prop) {
+ $vals[$v] = $prop->getValue();
+ $this->assertNotEmpty($vals[$v]);
+ }
+ }
+
+ public function testStructureCacheRestore() {
+ $sc_vars = $this->getStructureCacheVariables();
+
+ Model1::from_id(1);
+ Model1::selection(array('model2.int1' => 1));
+ $vals = array();
+ foreach($sc_vars as $v => $prop) {
+ $vals[$v] = $prop->getValue();
+ $prop->setValue(array());
+ }
+
+ BasicObject::enable_structure_cache(MC::get_instance(), "bo_unit_test_");
+ foreach($sc_vars as $v => $prop) {
+ $this->assertEquals($vals[$v], $prop->getValue());
+ }
+ }
+
+ public function testClearStructureCache() {
+ $sc_vars = $this->getStructureCacheVariables();
+ Model1::from_id(1);
+ Model1::selection(array('model2.int1' => 1));
+ BasicObject::clear_structure_cache(MC::get_instance(), "bo_unit_test_");
+
+ BasicObject::enable_structure_cache(MC::get_instance(), "bo_unit_test_");
+ foreach($sc_vars as $v => $prop) {
+ $this->assertEmpty($prop->getValue());
+ }
+ }
+
+ public function testClearStructureCachePrefixSeparation() {
+ $sc_vars = $this->getStructureCacheVariables();
+ Model1::from_id(1);
+ Model1::selection(array('model2.int1' => 1));
+
+ BasicObject::clear_structure_cache(MC::get_instance(), "bo_unit_test2_");
+
+ BasicObject::enable_structure_cache(MC::get_instance(), "bo_unit_test_");
+ foreach($sc_vars as $v => $prop) {
+ $this->assertNotEmpty($prop->getValue());
+ }
+ }
+
+ private function getStructureCacheVariables() {
+ $ret = array();
+ $vars = array('column_ids', 'connection_table', 'tables', 'columns');
+ foreach($vars as $var) {
+ $ret[$var] = new ReflectionProperty('BasicObject', $var);
+ $ret[$var]->setAccessible(true);
+ }
+ return $ret;
+ }
+}
diff --git a/tests/suites/BasicObject/RelationsTest.php b/tests/suites/BasicObject/RelationsTest.php
new file mode 100644
index 0000000..178bb5a
--- /dev/null
+++ b/tests/suites/BasicObject/RelationsTest.php
@@ -0,0 +1,29 @@
+model2_id = $m2->id;
+ $m1->commit();
+
+ $m2_ret = $m1->Model2();
+ $this->assertEquals($m2, $m2_ret);
+ }
+
+ /**
+ * @depends testGetOtherModel
+ */
+ public function testSetOtherModel() {
+ $m1 = Blueprint::make('Model1');
+ $m2 = Blueprint::make('Model2');
+
+ $m1->Model2 = $m2;
+
+ $this->assertEquals($m2->id, $m1->model2_id);
+ $this->assertEquals($m2, $m1->Model2());
+
+ $m1->commit();
+ }
+}
diff --git a/tests/suites/BasicObject/SelectionTest.php b/tests/suites/BasicObject/SelectionTest.php
new file mode 100644
index 0000000..06cc57c
--- /dev/null
+++ b/tests/suites/BasicObject/SelectionTest.php
@@ -0,0 +1,52 @@
+ $m1->Model2()->int1));
+ $this->assertCount(1, $m1_ref);
+ $this->assertEquals($m1, $m1_ref[0]);
+ }
+
+ public function testManualJoin() {
+ $m1 = Blueprint::make('Model1', array('int1' => 5000));
+ $m2 = Blueprint::make('Model2', array('int1' => 5000, 'str1' => 'derp'));
+
+ $m1_ref = Model1::selection(array( '@join' => array( 'model2:using' => 'int1')) );
+ $this->assertCount(1, $m1_ref);
+ $this->assertEquals($m1, $m1_ref[0]);
+ }
+
+ public function testOrder() {
+ $key = "ordertest";
+ $m1 = Blueprint::make('Model1', array('int1' => 1, 'str1' => $key));
+ $m2 = Blueprint::make('Model1', array('int1' => 2, 'str1' => $key));
+
+ $selection = Model1::selection(array('str1' => $key, '@order' => 'int1'));
+ $this->assertEquals($m1->id, $selection[0]->id);
+ $this->assertEquals($m2->id, $selection[1]->id);
+
+ $selection = Model1::selection(array('str1' => $key, '@order' => 'int1:desc'));
+ $this->assertEquals($m2->id, $selection[0]->id);
+ $this->assertEquals($m1->id, $selection[1]->id);
+ }
+
+ /**
+ * @expectedException Exception
+ * @expectedExceptionMessage No such column 'foobar' in table 'model1' (value 'bar')
+ */
+ public function testUnknowColumn() {
+ Model1::selection(array('foobar' => 'bar'));
+ }
+
+ /**
+ * @expectedException Exception
+ * @expectedExceptionMessage No such table 'foobar'
+ */
+ public function testInvalidJoin() {
+ Model1::selection(array('foobar.foo' => 'a'));
+ }
+
+ /* TODO: Add much more tests */
+}
diff --git a/tests/suites/BasicObject/SimpleFunctionsTest.php b/tests/suites/BasicObject/SimpleFunctionsTest.php
new file mode 100644
index 0000000..f93a562
--- /dev/null
+++ b/tests/suites/BasicObject/SimpleFunctionsTest.php
@@ -0,0 +1,81 @@
+ 1, 'str1' => 'firsttest'));
+ $m2 = Blueprint::make('Model1', array('int1' => 2, 'str1' => 'firsttest'));
+
+ $this->assertEquals($m1->id, Model1::first(array('str1' => 'firsttest', '@order' => 'int1'))->id);
+ $this->assertEquals($m2->id, Model1::first(array('str1' => 'firsttest', '@order' => 'int1:desc'))->id);
+ }
+
+ public function testOne() {
+ $m1 = Blueprint::make('Model1');
+
+ $obj = Model1::one(array('int1' => $m1->int1));
+
+ $this->assertEquals($m1->id, $obj->id);
+ }
+
+ /**
+ * @expectedException Exception
+ */
+ public function testOneFailsCorrectly() {
+ $m1 = Blueprint::make('Model1', array('str1' => 'testonefail'));
+ $m1 = Blueprint::make('Model1', array('str1' => 'testonefail'));
+
+ Model1::one(array('str1' => 'testonefail'));
+ }
+
+ public function testDuplicate() {
+ $m1 = Blueprint::make('Model1');
+ $m2 = $m1->duplicate();
+
+ $this->assertNull($m2->id);
+ $this->assertEquals($m1->int1, $m2->int1);
+ $this->assertEquals($m1->str1, $m2->str1);
+
+ $m2->int1++;
+ $this->assertEquals($m1->int1 + 1, $m2->int1);
+
+ $m2->commit();
+
+ $this->assertNotNull($m2->id);
+
+ $this->assertNotEquals($m1->id, $m2->id);
+ }
+
+ public function testOutputHTMLSpecialChars() {
+ $html = "Foobar \" ";
+ $m1 = Blueprint::make('Model1', array('str1' => $html));
+
+ BasicObject::$output_htmlspecialchars = false;
+
+ $m1 = Model1::from_id($m1->id);
+
+ $this->assertEquals($html, $m1->str1);
+
+ BasicObject::$output_htmlspecialchars = true;
+ $escaped = htmlspecialchars($html, ENT_QUOTES, 'utf-8');
+ $this->assertEquals($escaped, $m1->str1);
+
+ BasicObject::$output_htmlspecialchars = false;
+ }
+
+ public function testDefaultOrder() {
+ $key = "ordertest";
+ $m1 = Blueprint::make('Model1', array('int1' => 1, 'str1' => $key));
+ $m2 = Blueprint::make('Model1', array('int1' => 2, 'str1' => $key));
+
+ Model1Ordered::$order = "int1";
+ $selection = Model1Ordered::selection(array('str1' => $key));
+ $this->assertEquals($m1->id, $selection[0]->id);
+ $this->assertEquals($m2->id, $selection[1]->id);
+
+ Model1Ordered::$order = "int1:desc";
+ $selection = Model1Ordered::selection(array('str1' => $key));
+ $this->assertEquals($m2->id, $selection[0]->id);
+ $this->assertEquals($m1->id, $selection[1]->id);
+ }
+
+}
diff --git a/tests/tests.sh b/tests/tests.sh
new file mode 100755
index 0000000..2110134
--- /dev/null
+++ b/tests/tests.sh
@@ -0,0 +1,14 @@
+echo "Running tests without cache\n"
+phpunit --bootstrap "no_cache.php" --verbose --exclude-group cache $@
+
+ret=$?
+
+if [ $ret -ne 0 ]; then
+ exit $ret
+fi
+
+
+echo "\n----------------------\n"
+echo "Running tests with cache\n"
+phpunit --bootstrap "with_cache.php" --verbose $@
+exit $?
diff --git a/tests/with_cache.php b/tests/with_cache.php
new file mode 100644
index 0000000..4e5659f
--- /dev/null
+++ b/tests/with_cache.php
@@ -0,0 +1,6 @@
+