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 @@ +