diff options
author | Dracony <draconyster@gmail.com> | 2012-12-26 15:13:57 +0200 |
---|---|---|
committer | Dracony <draconyster@gmail.com> | 2012-12-26 15:13:57 +0200 |
commit | db5bd4e2a84cfbbd662a7dbd9c6fc93edfb376d1 (patch) | |
tree | 77335fecf9b3e0995323df3c9887b593cb79aedf | |
download | PHPixie-db5bd4e2a84cfbbd662a7dbd9c6fc93edfb376d1.zip PHPixie-db5bd4e2a84cfbbd662a7dbd9c6fc93edfb376d1.tar.gz PHPixie-db5bd4e2a84cfbbd662a7dbd9c6fc93edfb376d1.tar.bz2 |
Moving to github
27 files changed, 2956 insertions, 0 deletions
diff --git a/.htaccess b/.htaccess new file mode 100644 index 0000000..90e2456 --- /dev/null +++ b/.htaccess @@ -0,0 +1,11 @@ +RewriteEngine On +RewriteBase / +RewriteCond %{DOCUMENT_ROOT}/web/%{REQUEST_URI} -f +RewriteRule ^(.*)$ /web/$1 [L,QSA] +RewriteCond %{REQUEST_FILENAME} !-f +RewriteRule .* index.php?$0 [PT,L,QSA] + + + + + diff --git a/index.php b/index.php new file mode 100644 index 0000000..0dc1212 --- /dev/null +++ b/index.php @@ -0,0 +1,14 @@ +<?php + +/** + * Working directory + */ +define('ROOTDIR',dirname(__FILE__)); + +/** + * Bootstrap the system + */ +require_once('system/bootstrap.php'); +Bootstrap::run(); +Request::create()->execute()->send_headers()->send_body(); + diff --git a/modules/database/classes/database/expression.php b/modules/database/classes/database/expression.php new file mode 100644 index 0000000..03cfd99 --- /dev/null +++ b/modules/database/classes/database/expression.php @@ -0,0 +1,31 @@ +<?php + +/** + * This class allows you to wrap fields or values that you don't want to be escaped + * inside the query + */ +class Expression_Database{ + + /** + * Part of query that shoud not be escaped + * @var mixed + * @access public + */ + public $value; + + /** + * Marks a part of query as a database specific expression, + * e.g. calls to SQL functions like MAX(), SUBSTR() etc. + * Example + * <code> + * $q->fields(DB::expr('COUNT(*)')); + * </code> + * + * @param mixed $value Part of query that shoud not be escaped + * @return Expression_Database + * @access public + */ + public function __construct($value){ + $this->value=$value; + } +}
\ No newline at end of file diff --git a/modules/database/classes/database/query.php b/modules/database/classes/database/query.php new file mode 100644 index 0000000..ec36b9e --- /dev/null +++ b/modules/database/classes/database/query.php @@ -0,0 +1,380 @@ +<?php + +/** + * Query builder. It allows building queries by using methods to set specific query parameters. + * Database drivers extend this class so that they can generate database specific queries. + * The idea is to provide a database agnostic interface to query writing. + * + * @method mixed table(string $table = null) Set table to query. + * Without arguments returns current table, returns self otherwise. + * + * @method mixed data(array $data = null) Set data for insert or update queries. + * Without arguments returns current data, returns self otherwise. + * + * @method mixed limit(int $limit = null) Set number of rows to return. + * Without arguments returns current limit, returns self otherwise. + * + * @method mixed offset(string $offset = null) Set the offset for the first row in result. + * Without arguments returns current offset, returns self otherwise. + * + * @method mixed groupby(string $groupby = null) A column to group rows by for aggregator functions. + * Without arguments returns current groupby argument, returns self otherwise. + * + * @method mixed type(string $type = null) Set query type. Available types: select, update, insert, delete, count. + * Without arguments returns current type argument, returns self otherwise. + */ +abstract class Query_Database { + + /** + * Array of conditions that rows must meet + * @var array + * @access protected + */ + protected $_conditions = array(); + + /** + * Table to query + * @var unknown + * @access protected + */ + protected $_table; + + /** + * Fields to return in the query + * @var array + * @access protected + */ + protected $_fields; + + /** + * Data for row insertion or update + * @var unknown + * @access protected + */ + protected $_data; + + /** + * Query type. Available types: select, update, insert, delete, count + * @var string + * @access protected + */ + protected $_type; + + /** + * Parameters for tables to join + * @var array + * @access protected + */ + protected $_joins = array(); + + /** + * Number of rows to return + * @var int + * @access protected + */ + protected $_limit; + + /** + * Offset of the first row + * @var int + * @access protected + */ + protected $_offset; + + /** + * Columns and directions to order by + * @var array + * @access protected + */ + protected $_orderby = array(); + + /** + * Database connection + * @var DB + * @access protected + */ + protected $_db; + + /** + * Conditions for aggregator functions + * @var array + * @access protected + */ + protected $_having=array(); + + /** + * Column to group by for aggregator functions + * @var string + * @access protected + */ + protected $_groupby; + + /** + * Last alias used on the table + * @var string + * @access protected + */ + protected $alias = null; + + /** + * Methods and type of value they allow that are available via __call + * @var array + * @access protected + */ + protected $methods = array('table' => 'string','data' => 'array','limit' => 'integer','offset' => 'integer','groupby' => 'string','type' => 'string'); + + /** + * Generates a query in format tht can be executed on current database implementation + * + * @access public + */ + public abstract function query(); + + /** + * Creates a new query + * + * @param DB $db Database connection + * @param string $type Query type. Available types: select, update, insert, delete, count + * @return void + * @access public + */ + public function __construct($db, $type) { + $this->_db=$db; + $this->_type=$type; + } + + /** + * Sets fields to be queried from the database + * + * @param mixed A single field or an array of them + * @return mixed If no parameters are passed returns current array of fields, + * otherwise returns self. + * @access public + */ + public function fields() { + $p = func_get_args(); + if (empty($p)) { + return $this->_fields; + }elseif (is_array($p[0])) { + $this->_fields=$p[0]; + }else { + $this->_fields=array($p[0]); + } + return $this; + } + + /** + * Magic methods to create methods for all generic query parts + * + * @param string $method Name of the method to call + * @param array $args Array of parameters + * @return mixed If no arguments are passed returns the current value of the property, + * otherwise returns self. + * @access public + * @throws Exception If method doesn't exist + * @throws Exception If value is of incorrect type + * @see $methods + */ + public function __call($method, $args) { + + if (isset($this->methods[$method])) { + + $property = '_'.$method; + + if (empty($args)) + return $this->$property; + $val = $args[0]; + if (is_int($val)) + $val=(int) $val; + if (gettype($val) != $this->methods[$method]) + throw new Exception("Method '{$method}' only accepts values of type: '{$this->methods[$method]}', '{$val}' was passed"); + $this->$property = $val; + return $this; + } + throw new Exception("Method '{$method}' doesn't exist."); + } + + /** + * Executes the query + * + * @return object Executes current query on its database connection + * @access public + * @see DB + */ + public function execute() { + $query = $this->query(); + $result = $this->_db->execute($query[0], $query[1]); + if ($this->_type == 'count') + return $result->current()->count; + return $result; + } + + /** + * Adds a joined table to the query. + * + * @param string $table Table to join + * @param array $conds Conditions to join tables on, same behavior as with where() method + * @param string $type Type of join. Defaults to 'left' + * @return Query_Database Returns self + * @access public + * @see where() + */ + public function join($table,$conds,$type='left'){ + $this->_joins[] = array($table, $type, $this->get_condition_part($conds)); + return $this; + } + + /** + * Sets conditions for aggregator functions, same behavior as with where() method + * + * @return Query_Database Returns self + * @access public + * @see where() + */ + public function having() { + $p = func_get_args(); + $cond = $this->get_condition_part($p); + $this->_having = array_merge($this->_having,$cond); + return $this; + } + + /** + * Adds a column to ordering parameters. + * + * @param string $column Column to order by + * @param string $dir Ordering direction. + * @return Query_Database Returns self + * @throws Exception If ordering direction isn't DESC or ASC + * @access public + */ + public function orderby($column, $dir) { + $dir=strtoupper($dir); + if ($dir != 'DESC' && $dir != 'ASC') + throw new Exception("Invalid sorting direction {$dir} specified"); + $this->_orderby[]=array($column,$dir); + return $this; + } + + /** + * Sets conditions for the query. + * Can be called in many ways, examples: + * Shorthand equals condition: + * <code> + * $q->where('name', 'Tinkerbell') + * </code> + * Conditions with operator: + * <code> + * $q->where('id', '>', 3) + * </code> + * OR logic: + * <code> + * $q->where('or', array('name', 'Tinkerbell')) + * </code> + * OR logic with operator + * <code> + * ->where('or', array('id', '>', 3)) + * </code> + * Arrays represent brackets. e.g + * <code> + * $q->where('name', 'Tinkerbell') + * ->where('or', array( + * array('id', '>', 7), + * array('id', '<', 15) + * ); + * //Will produce "WHERE `name`='Tinkerbell' OR (`id` > 7 AND `id` < 15)" + * </code> + * Multiple calls to where() append new conditions to previous ones + * + * @param mixed $column Column name, logic parameter 'OR' or 'AND' or an array of conditions + * @param mixed $operator Condition value, operator or an array of parameters + * @param mixed $val Condition value + * + * @return Query_Database Returns self + * @access public + */ + public function where() { + $p = func_get_args(); + $cond = $this->get_condition_part($p); + $this->_conditions= array_merge($this->_conditions,$cond); + + return $this; + } + + /** + * Recursively builds condition arrays for methods like where(), having() + * + * @param array $p Parameters passed to the method + * @return array Array in condition format + * @access private + * @throws Exception If condition format is incorrect + */ + private function get_condition_part($p) { + if (is_string($p[0]) && (strtolower($p[0]) == 'or'||strtolower($p[0]) == 'and') && isset($p[1]) && is_array($p[1])) { + $cond = $this->get_condition_part($p[1]); + $cond['logic'] = strtolower($p[0]); + return $cond; + } + + if (is_array($p[0])) { + if (count($p) == 1) + return $this->get_condition_part($p[0]); + $conds = array(); + foreach($p as $q) { + $conds[]=$this->get_condition_part($q); + } + if (count($conds) == 1) + return $conds; + return array('logic'=>'and','conditions'=>$conds); + } + + + if ((is_string($p[0]) || get_class($p[0]) == 'Expression_Database') && isset($p[1])) { + if (strpos($p[0], '.') === false) + $p[0]=$this->lastAlias().'.'.$p[0]; + return array( + 'logic' => 'and', + 'conditions'=>array( + 'field' => $p[0], + 'operator' => isset($p[2])?$p[1]:'=', + 'value' => isset($p[2])?$p[2]:$p[1] + ) + ); + } + + throw new Exception('Incorrect conditional statement passed'); + } + + /** + * Gets last generated alias + * + * @return string Last generated alias. If no alias were created returns table name. + * @access public + */ + public function lastAlias() { + if ($this->alias === null) + return $this->_table; + return 'a'.$this->alias; + } + + /** + * Generates new alias. Useful for programmatically adding aliases to joins. + * Alias is just a letter 'a' with an incremeted number. + * + * @return string New alias + * @access public + */ + public function addAlias() { + if ($this->alias === null){ + $this->alias = 0; + }else { + $this->alias++; + } + return $this->lastAlias(); + } + + + + + + +}
\ No newline at end of file diff --git a/modules/database/classes/database/result.php b/modules/database/classes/database/result.php new file mode 100644 index 0000000..ba83f25 --- /dev/null +++ b/modules/database/classes/database/result.php @@ -0,0 +1,74 @@ +<?php + +/** + * Allows to access database results in a unified way and + * provides iterator support, so it can be used inside loops like 'foreach' + */ +abstract class Result_Database implements Iterator { + + /** + * Current row number + * @var integer + * @access protected + */ + protected $_position = 0; + + /** + * Database result object + * @var mixed + * @access protected + */ + protected $_result; + + /** + * Current row + * @var object + * @access protected + */ + protected $_row; + + + /** + * Returns current row + * + * @return object Current row in result set + * @access public + */ + public function current() { + return $this->_row; + } + + /** + * Gets the number of the current row + * + * @return integer Row number + * @access public + */ + public function key() { + return $this->_position; + } + + /** + * Check if current row exists. + * + * @return bool True if row exists + * @access public + */ + public function valid() { + return $this->_row!=null; + } + + /** + * Returns all rows as array + * + * @return array Array of rows + * @access public + */ + public function as_array() { + $arr = array(); + foreach($this as $row) + $arr[] = $row; + return $arr; + } + +}
\ No newline at end of file diff --git a/modules/database/classes/db.php b/modules/database/classes/db.php new file mode 100644 index 0000000..acbec0f --- /dev/null +++ b/modules/database/classes/db.php @@ -0,0 +1,127 @@ +<?php + +/** + * Daatabase related functions. Creates connections, + * executes queries and returns results. It is also the + * generic connectionc class used by drivers. + */ +abstract class DB { + + /** + * An associative array of connections to databases + * @var array + * @access private + * @static + */ + private static $_instances=array(); + + /** + * Executes a prepared statement query + * + * @param string $query A prepared statement query + * @param array $params Parameters for the query + * @return Result_Database + * @access public + * @see Result_Database + */ + public abstract function execute($query, $params = array()); + + /** + * Builds a new Query to the database + * + * @param string $type Query type. Available types: select, update, insert, delete, count + * @return Result_Database + * @access public + * @see Query_Database + */ + public abstract function build_query($type); + + /** + * Gets the id of the last inserted row. + * + * @return mixed The id of the last inserted row + * @access public + */ + public abstract function get_insert_id(); + + /** + * Executes a named query where parameters are passed as an associative array + * Example: + * Query: SELECT * FROM fairies where name = :name + * Params: array('name'=>'Tinkerbell'); + * + * @param string $query A named query + * @param array $params Associative array of parameters + * @return Result_Database Current drivers implementation of Result_Database + * @access public + */ + public function namedQuery($query, $params=array()) { + $bind = array(); + preg_match_all('#:(\w+)#is', $query, $matches,PREG_SET_ORDER); + foreach($matches as $match) + if(isset($params[$match[1]])){ + $query = preg_replace("#{$match[0]}#", '?', $query, 1); + $bind[] = $params[$match[1]]; + } + return $this->execute($query,$bind); + } + + /** + * Returns an Expression_Database representation of the value. + * Values wrapped inside Expression_Database are not escaped in queries + * + * @param mixed $value Value to be wrapped + * @return Expression_Database Raw value that will not be escaped during query building + * @access public + * @static + */ + public static function expr($value){ + return new Expression_Database($value); + } + + /** + * Builds a query for specified connection. + * + * @param string $type Query type. Available types: select,update,insert,delete,count + * @param string $config Configuration name of the connection. + * Defaults to 'default'. + * @return Query_Database Driver implementation of the Query_Database class. + * @access public + * @static + */ + public static function query($type,$config = 'default') { + return DB::instance($config)->build_query($type); + } + + /** + * Gets the id of the last inserted row + * + * @param string $config Configuration name of the connection. + * Defaults to 'default'. + * @return mixed Id of the last inserted row + * @access public + * @static + */ + public static function insert_id($config = 'default') { + return DB::instance($config)->get_insert_id(); + } + + /** + * Gets an instance of a connection to the database + * + * @param string $config Configuration name of the connection. + * Defaults to 'default'. + * @return DB Driver implementation of the DB class. + * @access public + * @static + */ + public static function instance($config='default'){ + if (!isset(DB::$_instances[$config])) { + $driver = Config::get("database.{$config}.driver"); + $driver="DB_{$driver}_Driver"; + DB::$_instances[$config] = new $driver($config); + } + return DB::$_instances[$config]; + } + +}
\ No newline at end of file diff --git a/modules/database/classes/driver/mysql/db.php b/modules/database/classes/driver/mysql/db.php new file mode 100644 index 0000000..553cfb9 --- /dev/null +++ b/modules/database/classes/driver/mysql/db.php @@ -0,0 +1,88 @@ +<?php + +/** + * Mysqli Database Implementation + */ +class DB_Mysql_Driver extends DB{ + + /** + * Mysqli database connection object + * @var mysqli + * @access public + * @link http://php.net/manual/en/class.mysqli.php + */ + public $conn; + + /** + * Initializes database connection + * + * @param string $config Name of the connection to initialize + * @return void + * @access public + */ + public function __construct($config) { + $this->conn = mysqli_connect( + Config::get("database.{$config}.host",'localhost'), + Config::get("database.{$config}.user",''), + Config::get("database.{$config}.password",''), + Config::get("database.{$config}.db") + ); + } + + /** + * Builds a new Query implementation + * + * @param string $type Query type. Available types: select,update,insert,delete,count + * @return Query_Mysql_Driver Returns a Mysqli implementation of a Query. + * @access public + * @see Query_Database + */ + public function build_query($type) { + return new Query_Mysql_Driver($this,$type); + } + + /** + * Gets the id of the last inserted row. + * + * @return mixed Row id + * @access public + */ + public function get_insert_id() { + return $this->conn->insert_id; + } + + /** + * Executes a prepared statement query + * + * @param string $query A prepared statement query + * @param array $params Parameters for the query + * @return Result_Mysql_Driver Mysqli implementation of a database result + * @access public + * @throws Exception If the query resulted in an error + * @see Database_Result + */ + public function execute($query, $params = array()) { + $cursor = $this->conn->prepare($query); + if (!$cursor) + throw new Exception("Database error: {$this->conn->error} \n in query:\n{$query}"); + $types = ''; + $bind = array(); + $refs = array(); + if(!empty($params)){ + foreach($params as $key=>$param) { + $refs[$key]=is_array($param)?$param[0]:$param; + $bind[]=&$refs[$key]; + $types.=is_array($param)?$param[1]:'s'; + } + array_unshift($bind, $types); + + call_user_func_array(array($cursor, 'bind_param'), $bind); + } + $cursor->execute(); + $res = $cursor->get_result(); + if (is_object($res)){ + $res=new Result_Mysql_Driver($res); + } + return $res; + } +}
\ No newline at end of file diff --git a/modules/database/classes/driver/mysql/query.php b/modules/database/classes/driver/mysql/query.php new file mode 100644 index 0000000..9f38cc5 --- /dev/null +++ b/modules/database/classes/driver/mysql/query.php @@ -0,0 +1,199 @@ +<?php + +/** + * Mysqli implementation of the database Query + */ +class Query_Mysql_Driver extends Query_Database{ + + /** + * If a string is passed escapes a field by enclosing it in `` quotes. + * If you pass an Expression_Database object the value will be inserted into the query uneascaped + * + * @param mixed $field Field to be escaped or an Expression_Database object + * if the field must not be escaped + * @return string Escaped field representation + * @access public + * @see Expression_Database + */ + public function escape_field($field) { + if (is_object($field) && get_class($field) == 'Expression_Database') + return $field->value.' '; + $field = explode('.', $field); + if (count($field) == 1) + array_unshift($field,$this->lastAlias()); + $str = '`'.$field[0].'`.'; + if (trim($field[1]) == '*') + return $str.'* '; + return $str."`{$field[1]}` "; + } + + /** + * Replaces the value with ? and appends it to the parameters array + * If you pass an Expression_Database object the value will be inserted into the query uneascaped + * @param mixed $val Value to be escaped or an Expression_Database object + * if the value must not be escaped + * @param array &$params Reference to parameters array + * @return string Escaped value representation + * @access public + */ + public function escape_value($val,&$params) { + if (is_object($val) && get_class($val) == 'Expression_Database') + return $val->value.' '; + $params[] = $val; + return '? '; + } + + /** + * Builds a query and fills the $params array with parameter values + * + * @return array An array with a prepared query string and an array of parameters + * @access public + */ + public function query() { + + $query = ''; + $params = array(); + if ($this->_type == 'insert') { + $query.= "INSERT INTO `{$this->_table}`"; + $columns = ''; + $values = ''; + $first = true; + foreach($this->_data as $key => $val) { + if (!$first) { + $values.= ','; + $columns.= ','; + }else { + $first=false; + } + $columns.= "`{$key}` "; + $values.=$this->escape_value($val,$params); + } + $query.= "({$columns}) VALUES({$values})"; + }else{ + if ($this->_type == 'select'){ + $query.= "SELECT "; + if($this->_fields==null){ + $query.= "* "; + }else{ + $first = true; + foreach($this->_fields as $f) { + if (!$first) { + $query.=", "; + }else { + $first = false; + } + $query.="{$this->escape_field($f)} "; + } + } + $query.= "FROM `{$this->_table}` "; + } + if ($this->_type == 'count') { + $query.= "SELECT COUNT(*) as `count` FROM `{$this->_table}` "; + } + if($this->_type=='delete') + $query.= "DELETE FROM `{$this->_table}` "; + if($this->_type=='update'){ + $query.= "UPDATE `{$this->_table}` SET "; + $first = true; + foreach($this->_data as $key=>$val){ + if (!$first) { + $query.=','; + }else { + $first=false; + } + $query.= "`{$key}`=".$this->escape_value($val,$params); + } + } + + foreach($this->_joins as $join) { + $table = $join[0]; + if (is_array($table)){ + $table = "`{$table[0]}` as `{$table[1]}`"; + }else { + $table="`{$table}`"; + } + $query.= strtoupper($join[1])." JOIN {$table} ON ".$this->get_condition_query($join[2],$params,true,true); + } + + if (!empty($this->_conditions)) { + $query.="WHERE ".$this->get_condition_query($this->_conditions,$params,true); + } + if (($this->_type == 'select' || $this->_type == 'count') && $this->_groupby!=null) { + $query.="GROUP BY ".$this->escape_field($this->_groupby); + } + if (($this->_type == 'select' || $this->_type == 'count') && !empty($this->_having)) { + $query.="HAVING ".$this->get_condition_query($this->_having,$params,true); + } + + if ($this->_type == 'select' && !empty($this->_orderby)) { + $query.="ORDER BY "; + $first = true; + foreach($this->_orderby as $order) { + if (!$first) { + $query.=','; + }else { + $first=false; + } + $query.= $this->escape_field($order[0]); + if (isset($order[1])) { + $dir = strtoupper($order[1]); + $query.=$dir." "; + } + } + } + if($this->_type != 'count'){ + if ($this->_limit != null) + $query.= "LIMIT {$this->_limit} "; + if ($this->_offset != null) + $query.= "OFFSET {$this->_offset} "; + } + + } + + return array($query,$params); + } + + /** + * Recursively parses conditions array into a query string + * + * @param array $p Element of the array of conditions + * @param array &$params Reference to parameters array + * @param boolean $skip_first_operator Flag to skip the first logical operator in a query + * to prevent AND or OR to be at the beginning of the query + * @param boolean $value_is_field Flag if the the value in the logical operations should + * be treated as a field. E.g. for joins where the fields are + * compared between themselves and not with actual values + * @return string String representation of the conditions + * @access public + * @throws Exception If condition cannot be parsed + */ + public function get_condition_query($p,&$params,$skip_first_operator,$value_is_field=false) { + if (isset($p['field'])) { + if ($value_is_field){ + $param = $this->escape_field($p['value']); + }else { + $param = $this->escape_value($p['value'],$params); + } + return $this->escape_field($p['field']).' '.$p['operator'].' '.$param; + } + if (isset($p['logic'])) { + return ($skip_first_operator?'':strtoupper($p['logic'])).' ' + .$this->get_condition_query($p['conditions'],$params,false,$value_is_field); + } + + $conds = ''; + $skip=$skip_first_operator||(count($p) > 1); + foreach($p as $q) { + $conds.=$this->get_condition_query($q,$params,$skip,$value_is_field); + $skip=false; + } + if (count($p) > 1 && !$skip_first_operator) + return "( ".$conds." ) "; + return $conds; + + throw new Exception("Cannot parse condition:\n".var_export($p,true)); + } + + + +}
\ No newline at end of file diff --git a/modules/database/classes/driver/mysql/result.php b/modules/database/classes/driver/mysql/result.php new file mode 100644 index 0000000..0fbb645 --- /dev/null +++ b/modules/database/classes/driver/mysql/result.php @@ -0,0 +1,47 @@ +<?php + +/** + * Database result implementation for Mysqli + */ +class Result_Mysql_Driver extends Result_Database { + + /** + * Initializes new result object + * + * @param mysqli_result $result Mysqli Result + * @return void + * @access public + * @link http://php.net/manual/en/class.mysqli-result.php + */ + public function __construct($result) { + $this->_result = $result; + $this->_row=$this->_result->fetch_object(); + } + + /** + * Throws exception if rewind is attempted. + * + * @return void + * @access public + * @throws Exception If rewind is attempted + */ + public function rewind() { + if($this->_position!=0) + throw new Exception('Mysqli result cannot be rewound for unbuffered queries.'); + } + + /** + * Iterates to the next row in the result set + * + * @return void + * @access public + */ + public function next() { + + $this->_position++; + $this->_row=$this->_result->fetch_object(); + if ($this->_row == null) + $this->_result->free(); + } + +}
\ No newline at end of file diff --git a/modules/database/classes/driver/pdo/db.php b/modules/database/classes/driver/pdo/db.php new file mode 100644 index 0000000..89c7165 --- /dev/null +++ b/modules/database/classes/driver/pdo/db.php @@ -0,0 +1,70 @@ +<?php + +/** + * PDO Database implementation. + */ +class DB_PDO_Driver extends DB{ + + /** + * Connection object + * @var PDO + * @access public + * @link http://php.net/manual/en/class.pdo.php + */ + public $conn; + + /** + * Initializes database connection + * + * @param string $config Name of the connection to initialize + * @return void + * @access public + */ + public function __construct($config) { + $this->conn = new PDO( + Config::get("database.{$config}.connection"), + Config::get("database.{$config}.user",''), + Config::get("database.{$config}.password",'') + ); + } + + /** + * Builds a new Query implementation + * + * @param string $type Query type. Available types: select,update,insert,delete,count + * @return Query_PDO_Driver Returns a PDO implementation of a Query. + * @access public + * @see Query_Database + */ + public function build_query($type) { + return new Query_PDO_Driver($this,$type); + } + + /** + * Gets the id of the last inserted row. + * + * @return mixed Row id + * @access public + */ + public function get_insert_id() { + return $this->conn->lastInsertId(); + } + + /** + * Executes a prepared statement query + * + * @param string $query A prepared statement query + * @param array $params Parameters for the query + * @return Result_PDO_Driver PDO implementation of a database result + * @access public + * @throws Exception If the query resulted in an error + * @see Database_Result + */ + public function execute($query, $params = array()) { + $cursor = $this->conn->prepare($query); + if(!$cursor->execute($params)) + throw new Exception("Database error: ".implode(' ',$this->conn->errorInfo())." \n in query:\n{$query}"); + + return new Result_PDO_Driver($cursor); + } +}
\ No newline at end of file diff --git a/modules/database/classes/driver/pdo/query.php b/modules/database/classes/driver/pdo/query.php new file mode 100644 index 0000000..4a3d7c2 --- /dev/null +++ b/modules/database/classes/driver/pdo/query.php @@ -0,0 +1,8 @@ +<?php + +/** + * PDO Database Query implementation. Mimics Mysql implementation. + */ +class Query_PDO_Driver extends Query_Mysql_Driver{ + +}
\ No newline at end of file diff --git a/modules/database/classes/driver/pdo/result.php b/modules/database/classes/driver/pdo/result.php new file mode 100644 index 0000000..3270231 --- /dev/null +++ b/modules/database/classes/driver/pdo/result.php @@ -0,0 +1,47 @@ +<?php + +/** + * Database result implementation for PDO + */ +class Result_PDO_Driver extends Result_Database { + + /** + * Initializes new result object + * + * @param PDOStatement $stmt PDO Statement + * @return void + * @access public + * @link http://php.net/manual/en/class.pdostatement.php + */ + public function __construct($stmt) { + $this->_result = $stmt; + $this->_row=$this->_result->fetchObject(); + } + + /** + * Throws exception if rewind is attempted. + * + * @return void + * @access public + * @throws Exception If rewind is attempted + */ + public function rewind() { + if($this->_position!=0) + throw new Exception('PDO statement cannot be rewound for unbuffered queries'); + } + + /** + * Iterates to the next row in the result set + * + * @return void + * @access public + */ + public function next() { + + $this->_position++; + $this->_row=$this->_result->fetchObject(); + if ($this->_row == false) + $this->_result->closeCursor(); + } + +}
\ No newline at end of file diff --git a/modules/orm/classes/orm.php b/modules/orm/classes/orm.php new file mode 100644 index 0000000..cd09c3b --- /dev/null +++ b/modules/orm/classes/orm.php @@ -0,0 +1,579 @@ +<?php + +/** + * ORM allows you to access database items and their relationships in an OOP manner, + * it is easy to setup and makes a lot of use of naming convention. + * + * @method mixed limit(int $limit = null) Set number of rows to return. + * Without arguments returns current limit, returns self otherwise. + * + * @method mixed offset(int $offset = null) Set the offset for the first row in result. + * Without arguments returns current offset, returns self otherwise. + * + * @method mixed orderby(string $column, string $dir) Adds a column to ordering parameters + * + * @method mixed where(mixed $key, mixed $operator = null, mixed $val = null) behaves just like Query_Database::where() + * + * @see Query_Database::where() + */ +class ORM { + + /** + * Specifies which table the model will use, can be overridden + * @var string + * @access public + */ + public $table = null; + + /** + * Specifies which connection the model will use, can be overridden + * but a model can have relatioships only with models utilizing the same connection + * @var string + * @access public + */ + public $connection = 'default'; + + /** + * Specifies which column is treated as PRIMARY KEY + * @var string + * @access public + */ + public $id_field='id'; + + /** + * You can define 'Belongs to' relationships buy changing this array + * @var array + * @access protected + */ + protected $belongs_to=array(); + + /** + * You can define 'Has one' relationships buy changing this array + * @var array + * @access protected + */ + protected $has_one = array(); + + /** + * You can define 'Has many' relationships buy changing this array + * @var array + * @access protected + */ + protected $has_many = array(); + + /** + * An instance of the database connection + * @var DB + * @access private + */ + private $db; + + /** + * Current row returned by the database + * @var array + * @access protected + */ + protected $_row = array(); + + /** + * Associated query builder + * @var Query_Database + * @access public + */ + public $query; + + /** + * A flag whether the row was loaded from the database + * @var boolean + * @access private + */ + private $_loaded = false; + + /** + * The name of the model + * @var string + * @access public + */ + public $model_name; + + /** + * Cached properties + * @var array + * @access private + */ + private $_cached = array(); + + /** + * Constructs the model. To use ORM it is enough to + * just create a model like this: + * <code> + * class Fairy_Model extends ORM { } + * </code> + * By default it will assume that the name of your table + * is the plural form of the models' name, the PRIMARY KEY is id, + * and will use the 'default' connection. This behaiour is easy to be + * changed by overriding $table, $id and $db properties. + * + * @return void + * @access public + * @ see $table + * @ see $id + * @ see $db + */ + public function __construct() { + $this->query = DB::instance($this->connection)->query('select'); + $this->model_name=strtolower(preg_replace('#_Model$#i', '', get_class($this))); + if ($this->table == null) + $this->table = ORM::plural($this->model_name); + $this->query->table($this->table); + + foreach(array('belongs_to', 'has_one', 'has_many') as $rels) { + $normalized=array(); + foreach($this->$rels as $key => $rel) { + if (!is_array($rel)) { + $key = $rel; + $rel=array(); + } + $normalized[$key]=$rel; + if (!isset($rel['model'])) { + $rel['model']=$normalized[$key]['model']=$rels=='has_many'?ORM::singular($key):$key; + } + + $normalized[$key]['type']=$rels; + if (!isset($rel['key'])) + $normalized[$key]['key'] = $rels != 'belongs_to'?($this->model_name.'_id'):$rel['model'].'_id'; + + if ($rels == 'has_many' && isset($rel['through'])) + if (!isset($rel['foreign_key'])) + $normalized[$key]['foreign_key']=$rel['model'].'_id'; + } + $this->$rels=$normalized; + + } + + } + + /** + * Magic method for call Query_Database methods + * + * @param string $method Method to call + * @param array $arguments Arguments passed to the method + * @return mixed Returns self if parameters were passed. If no parameters weere passed returns + * current value for the associated parameter + * @throws Exception If method doesn't exist + * @access public + */ + public function __call($method, $arguments) { + if (!in_array($method, array('limit', 'offset', 'orderby', 'where'))) + throw new Exception("Method '{$method}' doesn't exist on .".get_class($this)); + $res = call_user_func_array(array($this->query, $method), $arguments); + if(is_subclass_of($res,'Query_Database')) + return $this; + return $res; + } + + /** + * Finds all rows that meet set criteria. + * + * @return ORMResult Returns ORMResult that you can use in a 'foreach' loop. + * @access public + */ + public function find_all() { + return new ORMResult(get_class($this), $res=$this->query->execute()); + } + + /** + * Searches for the first row that meets set criteria. If no rows match it still returns an ORM object + * but with its loaded() flag being False. calling save() on such an object will insert a new row. + * + * @return ORM Found item or new object of the current model but with loaded() flag being False + * @access public + */ + public function find() { + $res=new ORMResult(get_class($this), $res=$this->query->limit(1)->execute()); + return $res->current(); + } + + /** + * Counts all rows that meet set criteria. Ignores limit and offset. + * + * @return int Number of rows + * @access public + */ + public function count_all() { + $query = clone $this->query; + $query->type('count'); + return $query->execute(); + + } + + /** + * Checks if the item is considered to be loaded from the database + * + * @return boolean Returns True if the item was loaded + * @access public + */ + public function loaded() { + return $this->_loaded; + } + + /** + * Returns the row associated with current ORM item as an associative array + * + * @return array Associative array representing the row + * @access public + */ + public function as_array() { + return $this->_row; + } + + /** + * Returns a clone of query builder that is being used to set conditions. + * It is useful for example if you let ORM manage bulding a complex query using it's relationship + * system, then you get the clone of that query and alter it to your liking, + * so there is no need to writing relationship joins yourself. + * + * @return Query_Database A clone of the current query builder + * @access public + */ + public function query() { + return clone $this->query; + } + + /** + * You can override this method to return additional properties that you would like to use + * in your model. One advantage for using this instead of just overriding __get() is that + * in this way the properties also get cached. + * + * @param string $property The name of the property to get + * @return void + * @access public + */ + public function get($property) { + + } + + /** + * Magic method that allows accessing row columns as properties and also facilitates + * access to relationships and custom properties defined in get() method. + * If a relatioship is being accessed, it will return an ORM model of the related table + * and automatically alter its query so that all your previously set conditions will remain + + * @param string $column Name of the column, propert or relationship to get + * @return mixed + * @access public + * @throws Exception If neither property nor a relationship with such name is found + */ + public function __get($column) { + if (array_key_exists($column,$this->_row)) + return $this->_row[$column]; + if (array_key_exists($column,$this->_cached)) + return $this->_cached[$column]; + if (($val = $this->get($column))!==null) { + $this->_cached[$column] = $val; + return $val; + } + $relations = array_merge($this->has_one, $this->has_many, $this->belongs_to); + + if ($target = Misc::arr($relations, $column, false)) { + $model = ORM::factory($target['model']); + $model->query = clone $this->query; + if ($this->loaded()) + $model->query->where($this->id_field,$this->_row[$this->id_field]); + if ($target['type']=='has_many'&&isset($target['through'])) { + $lastAlias = $model->query->lastAlias(); + $throughAlias=$model->query->addAlias(); + $newAlias = $model->query->addAlias(); + $model->query->join(array($target['through'], $throughAlias), array( + $lastAlias.'.'.$this->id_field, + $throughAlias.'.'.$target['key'], + ),'inner'); + $model->query->join(array($model->table, $newAlias), array( + $throughAlias.'.'.$target['foreign_key'], + $newAlias.'.'.$model->id_field, + ),'inner'); + }else{ + $lastAlias = $model->query->lastAlias(); + $newAlias = $model->query->addAlias(); + if ($target['type'] == 'belongs_to') { + $model->query->join(array($model->table, $newAlias), array( + $lastAlias.'.'.$target['key'], + $newAlias.'.'.$model->id_field, + ),'inner'); + }else { + $model->query->join(array($model->table, $newAlias), array( + $lastAlias.'.'.$this->id_field, + $newAlias.'.'.$target['key'], + ), 'inner'); + } + } + $model->query->fields(array("$newAlias.*")); + if ($target['type'] != 'has_many' && $model->loaded() ) { + $model = $model->find(); + $this->_cached[$column]=$model; + } + return $model; + } + + throw new Exception("Property {$column} not found on {$this->model_name} model."); + } + + /** + * Magic method to update record values when set as properties or to add an ORM item to + * a relation. By assigning an ORM object to a relationship a relationship is created between the + * current item and the passed one Using properties this way is a shortcut to the add() method. + * + * @param string $column Column or relationship name + * @param mixed $val Column value or an ORM item to be added to a relation + * @return void + * @access public + * @see add() + */ + public function __set($column, $val) { + $relations = array_merge($this->has_one, $this->has_many, $this->belongs_to); + if (array_key_exists($column,$relations)){ + $this->add($column, $val); + }else{ + $this->_row[$column] = $val; + } + $this->_cached=array(); + } + + /** + * Create a relationship between current item and an other one + * + * @param string $relation Name of the relationship + * @param ORM $model ORM item to create a relationship with + * @return void + * @access public + * @throws Exception Exception If realtionship is not defined + * @throws Exception Exception If current item is not in the database yet (isn't considered loaded()) + * @throws Exception Exception If passed item is not in the database yet (isn't considered loaded()) + */ + public function add($relation, $model) { + + if (!$this->loaded()) + throw new Exception("Model must be loaded before you try adding relationships to it. Probably you haven't saved it."); + if (!$model->loaded()) + throw new Exception("Model must be loaded before added to a relationship. Probably you haven't saved it."); + + $rels = array_merge($this->has_one, $this->has_many,$this->belongs_to); + $rel = Misc::arr($rels, $relation, false); + if (!$rel) + throw new Exception("Model doesn't have a '{$relation}' relation defined"); + + if ($rel['type']=='belongs_to') { + $key=$rel['key']; + $this->$key = $model->_row[$this->id_field]; + $this->save(); + }elseif (isset($rel['through'])) { + $exists = DB::instance($this->connection)->query('count') + ->table($rel['through']) + ->where(array( + array($rel['key'],$this->_row[$this->id_field]), + array($rel['foreign_key'],$model->_row[$model->id_field]) + )) + ->execute(); + if(!$exists) + DB::instance($this->connection)->query('insert') + ->table($rel['through']) + ->data(array( + $rel['key'] => $this->_row[$this->id_field], + $rel['foreign_key'] =>$model->_row[$model->id_field] + )) + ->execute(); + }else { + $key=$rel['key']; + $model->$key = $this->_row[$this->id_field]; + $model->save(); + } + $this->_cached=array(); + } + + /** + * Removes a relationship between current item and the passed one + * + * @param string $relation Name of the relationship + * @param ORM $model ORM item to remove relationship with. Can be omitted for 'belongs_to' relationships + * @return void + * @access public + * @throws Exception Exception If realtionship is not defined + * @throws Exception Exception If current item is not in the database yet (isn't considered loaded()) + * @throws Exception Exception If passed item is not in the database yet (isn't considered loaded()) + */ + public function remove($relation, $model=false) { + + if (!$this->loaded()) + throw new Exception("Model must be loaded before you try removing relationships from it."); + + $rels = array_merge($this->has_one, $this->has_many,$this->belongs_to); + $rel = Misc::arr($rels, $relation, false); + if (!$rel) + throw new Exception("Model doesn't have a '{$relation}' relation defined"); + + if ($rel['type']!='belongs_to'&&(!$model||!$model->loaded())) + throw new Exception("Model must be loaded before being removed from a has_one or has_many relationship."); + if ($rel['type']=='belongs_to') { + $key=$rel['key']; + $this->$key = null; + $this->save(); + }elseif (isset($rel['through'])) { + $exists = DB::instance($this->connection)->query('delete') + ->table($rel['through']) + ->where(array( + array($rel['key'],$this->_row[$this->id_field]), + array($rel['foreign_key'],$model->_row[$model->id_field]) + )) + ->execute(); + }else { + $key=$rel['key']; + $model->$key = null; + $model->save(); + } + $this->_cached=array(); + } + + /** + * Deletes current item from the database + * + * @return void + * @access public + * @throws Exception If the item is not in the database, e.g. is not loaded() + */ + public function delete() { + if (!$this->loaded()) + throw new Exception("Cannot delete an item that wasn't selected from database"); + DB::instance($this->connection)->query('delete') + ->table($this->table) + ->where($this->id_field, $this->_row[$this->id_field]) + ->execute(); + $this->_cached=array(); + } + + /** + * Deletes all items that meet set conditions. Use in the same way + * as you would a find_all() method. + * + * @return ORM Returns self + * @access public + */ + public function delete_all() { + $query = clone $this->query; + $query->type('delete'); + $query->execute(); + return $this; + } + + /** + * Saves the item back to the database. If item is loaded() it will result + * in an update, otherwise a new row will be inserted + * + * @return ORM Returns self + * @access public + */ + public function save() { + if (isset($this->_row[$this->id_field])) { + $query = DB::instance($this->connection)->query('update') + ->table($this->table) + ->where($this->id_field,$this->_row[$this->id_field]); + }else { + $query = DB::instance($this->connection)->query('insert') + ->table($this->table); + } + $query->data($this->_row); + $query->execute(); + + if (isset($this->_row[$this->id_field])) { + $id=$this->_row[$this->id_field]; + }else { + $id=DB::instance($this->connection)->get_insert_id(); + } + $row =(array) DB::instance($this->connection)->query('select') + ->table($this->table) + ->where($this->id_field, $id)->execute()->current(); + $this->values($row,true); + return $this; + } + + + /** + * Batch updates item columns using an associative array + * + * @param array $row Associative array of key => value pairs + * @param boolean $set_loaded Flag to consider the ORM item loaded. Useful if you selected + * the row from the database and want to wrap it in ORM + * @return ORM Returns self + * @access public + */ + public function values($row, $set_loaded = false) { + $this->_row = array_merge($this->_row, $row); + if ($set_loaded) + $this->_loaded = true; + $this->_cached=array(); + return $this; + } + + /** + * Initializes ORM model by name, and optionally fetches an item by id + * + * @param string $name Model name + * @param mixed $id If set ORM will try to load the item with this id from the database + * @return ORM ORM model, either empty or preloaded + * @access public + * @static + */ + public static function factory($name,$id=null){ + $model = $name.'_Model'; + $model=new $model; + if ($id != null) + return $model->where($model->id_field, $id)->find() + ->data(array($model->id_field,$id)); + return $model; + } + + /** + * Gets plural form of a noun + * + * @param string $str Noun to get a plural form of + * @return string Plural form + * @access private + * @static + */ + private static function plural($str){ + $regexes=array( + '/^(.*?[sxz])$/i' => '\\1es', + '/^(.*?[^aeioudgkprt]h)$/i' => '\\1es', + '/^(.*?[^aeiou])y$/i'=>'\\1ies', + ); + foreach($regexes as $key=>$val){ + $str = preg_replace($key, $val, $str,-1, $count); + if ($count) + return $str; + } + return $str.'s'; + } + + /** + * Gets singular form of a noun + * + * @param string $str Noun to get singular form of + * @return string Singular form of the noun + * @access private + * @static + */ + private static function singular($str){ + $regexes=array( + '/^(.*?us)$/i' => '\\1', + '/^(.*?[sxz])es$/i' => '\\1', + '/^(.*?[^aeioudgkprt]h)es$/i' => '\\1', + '/^(.*?[^aeiou])ies$/i' => '\\1y', + '/^(.*?)s$/'=>'\\1' + ); + foreach($regexes as $key=>$val){ + $str = preg_replace($key, $val, $str,-1, $count); + if ($count) + return $str; + } + return $str; + } +}
\ No newline at end of file diff --git a/modules/orm/classes/ormresult.php b/modules/orm/classes/ormresult.php new file mode 100644 index 0000000..5dcada0 --- /dev/null +++ b/modules/orm/classes/ormresult.php @@ -0,0 +1,113 @@ +<?php + +/** + * Allows iterating over ORM objects inside loops lie 'foreach', + * while preserving performanceby working with only a single row + * at a time. It wraps conveniently wraps around Database_Result class + * returning ORM object instead of just data object. + * + * @see Database_Result + */ +class ORMResult implements Iterator { + + /** + * Name of the model that the rows belong to + * @var string + * @access private + */ + private $_model; + + /** + * Database result + * @var Result_Database + * @access private + */ + private $_dbresult; + + /** + * Initialized an ORMResult with which model to use and which result to + * iterate over + * + * @param string $model Model name + * @param Result_Database $dbresult Database result + * @return void + * @access public + */ + public function __construct($model,$dbresult){ + $this->_model=$model; + $this->_dbresult = $dbresult; + } + + /** + * Rewinds database cursor to the first row + * + * @return void + * @access public + */ + function rewind() { + $this->_dbresult->rewind(); + } + + /** + * Gets an ORM Model of the current row + * + * @return ORM Model of the current row of the result set + * @access public + */ + function current() { + $model = new $this->_model; + if (!$this->_dbresult->valid()) + return $model; + $model->values((array)$this->_dbresult->current(),true); + return $model; + } + + /** + * Gets current rows' index number + * + * @return int Row number + * @access public + */ + function key() { + return $this->_dbresult->key(); + } + + /** + * Iterates to the next row in the result + * + * @return void + * @access public + */ + function next() { + $this->_dbresult->next(); + } + + /** + * Checks if current row is valid. + * + * @return bool returns false if we reach the end of the result set. + * @access public + */ + function valid() { + return $this->_dbresult->valid(); + } + + /** + * Returns an array of all rows as ORM objects if $rows is False, + * or just an array of result rows with each row being a standart object, + * this can be useful for functions like json_encode. + * + * @param boolean $rows Whether to return just rows and not ORM objects + * @return array Array of ORM objects or standart objects representing rows + * @access public + */ + public function as_array($rows = false) { + if ($rows) + return $this->_dbresult->as_array(); + $arr = array(); + foreach($this as $row) + $arr[] = $row; + return $arr; + } + +}
\ No newline at end of file diff --git a/system/bootstrap.php b/system/bootstrap.php new file mode 100644 index 0000000..2561d4e --- /dev/null +++ b/system/bootstrap.php @@ -0,0 +1,72 @@ +<?php + +/** + * Bootstraps the system + */ +class Bootstrap{ + + /** + * Autload function. Searches for the class file and includes it. + * + * @param unknown $class Class name + * @return void + * @access public + * @throws Exception If the class is not found + * @static + */ + public static function autoload($class) { + $file = Misc::find_file('class', $class); + if ($file == false) + throw new Exception("Class {$class} not found."); + require_once($file); + } + + /** + * Runs the application + * + * @return void + * @access public + * @static + */ + public static function run() { + + /** + * Application folder + */ + define('APPDIR', ROOTDIR.'/application/'); + + /** + * Modules folder + */ + define('MODDIR', ROOTDIR.'/modules/'); + + /** + * System folder + */ + define('SYSDIR', ROOTDIR.'/system/'); + + /** + * Web folder + */ + define('WEBDIR', ROOTDIR.'/web/'); + /** + * Helper functions + */ + require_once('classes/misc.php'); + + /** + * Configuration handler + */ + require_once('classes/config.php'); + + /** + * Applications configuration file + */ + require_once('application/config.php'); + + spl_autoload_register('Bootstrap::autoload'); + Debug::init(); + foreach(Config::get('routes') as $route) + Route::add($route[0],$route[1],$route[2]); + } +}
\ No newline at end of file diff --git a/system/classes/config.php b/system/classes/config.php new file mode 100644 index 0000000..b0550b4 --- /dev/null +++ b/system/classes/config.php @@ -0,0 +1,51 @@ +<?php + +/** + * Handles retrieving of the configuration options. + * You can add any configuration values to your /application/config.php file + * as associative array and get those values using the get() method. + */ +class Config { + + /** + * Array of configuration options + * @var array + * @access public + * @static + */ + public static $data=array(); + + /** + * Retrieves a configuration value. You can use a dot notation + * to access properties in nested arrays like this: + * <code> + * Config::get('database.default.user'); + * </code> + * + * @param string $key Configuration key to retrieve. + * @param string $default Default value to return if the key is not found. + * @return mixed Configuration value + * @access public + * @throws Exception If default value is not specified and the key is not found + * @static + */ + public static function get() { + $p = func_get_args(); + $keys = explode('.', $p[0]); + $group=Config::$data; + for ($i = 0; $i < count($keys); $i++) { + if ($i == count($keys) - 1) { + if (isset($group[$keys[$i]])) + return $group[$keys[$i]]; + break; + } + $group = Misc::arr($group, $keys[$i], null); + if (!is_array($group)) + break; + } + if (isset($p[1])) + return $p[1]; + throw new Exception("Configuration not set for {$p[0]}."); + } + +}
\ No newline at end of file diff --git a/system/classes/controller.php b/system/classes/controller.php new file mode 100644 index 0000000..e543942 --- /dev/null +++ b/system/classes/controller.php @@ -0,0 +1,84 @@ +<?php + +/** + * Base Controller class. Controlers contain the logic of your website, + * each action representing a reply to a prticular request, e.g. a single page. + */ +class Controller { + + /** + * Request for this controller. Holds all input data. + * @var Request + * @access public + */ + public $request; + + /** + * Response for this controller. It will be updated with headers and + * response body during controller execution + * @var Response + * @access public + */ + public $response; + + /** + * If set to False stops controller execution + * @var boolean + * @access public + */ + public $execute=true; + + /** + * This method is called before the action. + * You can override it if you need to, + * it doesn't do anything by default. + * + * @return void + * @access public + */ + public function before() {} + + /** + * This method is called after the action. + * You can override it if you need to, + * it doesn't do anything by default. + * + * @return void + * @access public + */ + public function after() { } + + /** + * Creates new Controller + * + * @return void + * @access public + */ + public function __construct() { + $this->response=new Response; + } + + /** + * Runs the appropriate action. + * It will axecute the before() method before the action + * and after() method after the action finishes. + * + * @param string $action Name of the action to execute. + * @return void + * @access public + * @throws Exception If the specified action doesn't exist + */ + public function run($action) { + $action = 'action_'.$action; + if (!method_exists($this, $action)) + throw new Exception("Method {$action} doesn't exist in ".get_class($this)); + $this->execute=true; + $this->before(); + if($this->execute) + $this->$action(); + if($this->execute) + $this->after(); + } + + +}
\ No newline at end of file diff --git a/system/classes/debug.php b/system/classes/debug.php new file mode 100644 index 0000000..85a8956 --- /dev/null +++ b/system/classes/debug.php @@ -0,0 +1,122 @@ +<?php + +/** + * Handles error reporting and debugging + */ +class Debug { + + /** + * Caught exception + * @var Exception + * @access public + */ + public $exception; + + /** + * An array of logged items + * @var array + * @access public + * @static + */ + public static $logged=array(); + + /** + * Displays the error page + * + * @return void + * @access public + */ + public function render() { + ob_end_clean(); + $view = View::get('debug'); + $view->exception = $this->exception; + $view->log = Debug::$logged; + header($_SERVER["SERVER_PROTOCOL"]." 404 Not Found"); + header("Status: 404 Not Found"); + echo $view->render(); + } + + /** + * Catches errors and exceptions and processes them + * + * @param Exception $exception Caught exception + * @return void + * @access public + * @static + */ + public static function onError($exception) { + set_exception_handler(array('Debug', 'internalException')); + set_error_handler ( array('Debug', 'internalError'), E_ALL); + $error = new Debug(); + $error->exception = $exception; + $error->render(); + } + + /** + * Converts PHP Errors to Exceptions + * + * @param string $errno Error number + * @param string $errstr Error message + * @param string $errfile File in which the error occured + * @param string $errline Line at which the error occured + * @return void + * @access public + * @throws ErrorException Throws converted exception to be immediately caught + * @static + */ + public static function errorHandler($errno, $errstr, $errfile, $errline) { + throw new ErrorException($errstr, $errno, 0, $errfile, $errline); + } + + /** + * Handles exceptions that occured while inside the error handler. Prevents recursion. + * + * @param Exception $exception Caught exception + * @return void + * @access public + * @static + */ + public static function internalException($exception) { + echo $exception->getMessage().' in '.$exception->getFile().' on line '.$exception->getLine(); + } + + /** + * Handles errors that occured while inside the error handler. Prevents recursion. + * + * @param string $errno Error number + * @param string $errstr Error message + * @param string $errfile File in which the error occured + * @param string $errline Line at which the error occured + * @return void + * @access public + * @static + */ + public static function internalError($errno, $errstr, $errfile, $errline) { + echo $errstr.' in '.$errfile.' on line '.$errline; + } + + /** + * Initializes the error handler + * + * @return void + * @access public + * @static + */ + public static function init(){ + set_exception_handler(array('Debug', 'onError')); + set_error_handler ( array('Debug', 'errorHandler'), E_ALL); + } + + /** + * Adds an item to the log. + * + * @param mixed $val Item to be logged + * @return void + * @access public + * @static + */ + public static function log($val){ + array_unshift(Debug::$logged,$val); + } + +}
\ No newline at end of file diff --git a/system/classes/error.php b/system/classes/error.php new file mode 100644 index 0000000..6f54784 --- /dev/null +++ b/system/classes/error.php @@ -0,0 +1,99 @@ +<?php + +/** + * Short description for class + */ +class Error { + + /** + * Description for public + * @var unknown + * @access public + */ + public $exception; + + /** + * Short description for function + * + * @return void + * @access public + */ + public function render() { + ob_end_clean(); + $view = View::get('error'); + $view->exception = $this->exception; + echo $view->render(); + } + + /** + * Short description for function + * + * @param unknown $exception Parameter description (if any) ... + * @return void + * @access public + * @static + */ + public static function onError($exception) { + set_exception_handler(array('Error', 'internalException')); + set_error_handler ( array('Error', 'internalError'), E_ALL); + $error = new Error(); + $error->exception = $exception; + $error->render(); + } + + /** + * Short description for function + * + * @param unknown $errno Parameter description (if any) ... + * @param unknown $errstr Parameter description (if any) ... + * @param unknown $errfile Parameter description (if any) ... + * @param unknown $errline Parameter description (if any) ... + * @return void + * @access public + * @throws ErrorException Exception description (if any) ... + * @static + */ + public static function errorHandler($errno, $errstr, $errfile, $errline) { + throw new ErrorException($errstr, $errno, 0, $errfile, $errline); + } + + /** + * Short description for function + * + * @param mixed $exception Parameter description (if any) ... + * @return void + * @access public + * @static + */ + public static function internalException($exception) { + echo $exception->getMessage().' in '.$exception->getFile().' on line '.$exception->getLine(); + } + + /** + * Short description for function + * + * @param unknown $errno Parameter description (if any) ... + * @param string $errstr Parameter description (if any) ... + * @param string $errfile Parameter description (if any) ... + * @param string $errline Parameter description (if any) ... + * @return void + * @access public + * @static + */ + public static function internalError($errno, $errstr, $errfile, $errline) { + echo $errstr.' in '.$errfile.' on line '.$errline; + } + + /** + * Short description for function + * + * @return void + * @access public + * @static + */ + public static function init(){ + set_exception_handler(array('Error', 'onError')); + set_error_handler ( array('Error', 'errorHandler'), E_ALL); + } + +}
\ No newline at end of file diff --git a/system/classes/misc.php b/system/classes/misc.php new file mode 100644 index 0000000..ab04b11 --- /dev/null +++ b/system/classes/misc.php @@ -0,0 +1,61 @@ +<?php + +/** + * Miscellaneous useful functions + */ +class Misc{ + + /** + * Retrieve value from array by key, with default value support. + * + * @param array $array Input array + * @param string $key Key to retrieve from the array + * @param mixed $default Default value to return if the key is not found + * @return mixed An array value if it was found or default value if it is not + * @access public + * @static + */ + public static function arr($array,$key,$default=null){ + if (isset($array[$key])) + return $array[$key]; + return $default; + } + + /** + * Find full path to either a class or view by name. + * It will search in the /system folder first, then the /application folder + * and then in all enabled modules. + * + * @param string $type Type of the file to find. Either 'class' or 'view' + * @param string $name Name of the file to find + * @return boolean Return Full path to the file or False if it is not found + * @access public + * @static + */ + public static function find_file($type, $name) { + $folders = array(SYSDIR, APPDIR); + foreach(Config::get('modules') as $module) + $folders[] = MODDIR.$module.'/'; + + if($type=='class'){ + $subfolder = 'classes/'; + $dirs = array_reverse(explode('_', strtolower($name))); + $fname = array_pop($dirs); + $subfolder.=implode('/',$dirs).'/'; + } + + if ($type == 'view') { + $subfolder = 'views/'; + $fname=$name; + } + + foreach($folders as $folder) { + $file = $folder.$subfolder.$fname.'.php'; + + if (file_exists($file)) { + return($file); + } + } + return false; + } +}
\ No newline at end of file diff --git a/system/classes/request.php b/system/classes/request.php new file mode 100644 index 0000000..da7cade --- /dev/null +++ b/system/classes/request.php @@ -0,0 +1,112 @@ +<?php + +/** + * Handles client request. + */ +class Request { + + /** + * Stores POST data + * @var array + * @access private + */ + private $_post; + + /** + * Stores GET data + * @var array + * @access private + */ + private $_get; + + /** + * Current Route + * @var Route + * @access public + */ + public $route; + + /** + * Request method + * @var string + * @access public + */ + public $method; + + /** + * Retrieves a GET parameter + * + * @param string $key Parameter key + * @param mixed $default Default value + * @return mixed Returns a value if a key is specified, + * or an array of GET parameters if it isn't. + * @access public + */ + public function get($key = null, $default = null) { + if ($key == null) + return $this->_get; + return Misc::arr($this->_get,$key,$default); + } + + /** + * Retrieves a POST parameter + * + * @param string $key Parameter key + * @param mixed $default Default value + * @return mixed Returns a value if a key is specified, + * or an array of POST parameters if it isn't. + * @access public + */ + public function post($key = null, $default = null) { + if ($key == null) + return $this->_post; + return Misc::arr($this->_post,$key,$default); + } + + /** + * Retrieves a Route parameter + * + * @param string $key Parameter key + * @param mixed $default Default value + * @return mixed Returns a value if a key is specified, + * or an array of Route parameters if it isn't. + * @access public + */ + public function param($key = null, $default = null) { + if ($key == null) + return $this->route->params; + return Misc::arr($this->route->params,$key,$default); + } + + /** + * Initializes the routed Controller and executes specified action + * + * @return Response A Response object with the body and headers set + * @access public + */ + public function execute() { + $controller=$this->param('controller').'_Controller'; + $controller = new $controller; + $controller->request = $this; + $controller->run($this->param('action')); + return $controller->response; + } + + /** + * Initializes the Request and process the URI into a Route + * + * @return object Request + * @access public + * @static + */ + public static function create(){ + $request = new Request(); + $request->_post = $_POST; + $request->_get = $_GET; + $url_parts = parse_url($_SERVER['REQUEST_URI']); + $request->route = Route::match($url_parts['path']); + $request->method=$_SERVER['REQUEST_METHOD']; + return $request; + } + +}
\ No newline at end of file diff --git a/system/classes/response.php b/system/classes/response.php new file mode 100644 index 0000000..da8a80a --- /dev/null +++ b/system/classes/response.php @@ -0,0 +1,69 @@ +<?php + +/** + * Handles the response that is sent back to the client. + */ +class Response { + + /** + * Headers for the response + * @var array + * @access public + */ + public $headers = array( + 'Content-Type: text/html; charset=utf-8' + ); + + /** + * Response body + * @var string + * @access public + */ + public $body; + + /** + * Add header to the response + * + * @param string $header Header content + * @return void + * @access public + */ + public function add_header($header){ + $this->headers[]=$header; + } + + /** + * Add redirection header + * + * @param string $url URL to redirect the client to + * @return void + * @access public + */ + public function redirect($url){ + $this->add_header("Location: $url"); + } + + /** + * Sends headers to the client + * + * @return Response Same Response object, for method chaining + * @access public + */ + public function send_headers(){ + foreach($this->headers as $header) + header($header); + return $this; + } + + /** + * Send response body to the client + * + * @return object Same Response object, for method chaining + * @access public + */ + public function send_body(){ + echo $this->body; + return $this; + } + +}
\ No newline at end of file diff --git a/system/classes/route.php b/system/classes/route.php new file mode 100644 index 0000000..62f53fe --- /dev/null +++ b/system/classes/route.php @@ -0,0 +1,126 @@ +<?php + +/** + * Routing class to extract and parse request parameters from the URL + */ +class Route { + + /** + * Name of the route. + * @var string + * @access public + */ + public $name; + + /** + * Extracted parameters + * @var array + * @access public + */ + public $params=array(); + + /** + * Associative array of routes. + * @var array + * @access private + * @static + */ + private static $rules=array(); + + /** + * Ads a route + * + * @param string $name Name of the route. Routes with the same name will override one another. + * @param mixed $rule Either an expression to match URI against or a function that will + * be passed the URI and must return either an associative array of + * extracted parameters (if it matches) or False. + * @param array $defaults An associated array of default values. + * @return void + * @access public + * @static + */ + public static function add($name, $rule, $defaults = array()) { + Route::$rules[$name]=array( + 'rule'=>$rule, + 'defaults'=>$defaults + ); + } + + /** + * Gets route by name + * + * @param string $name Route name + * @return Route + * @access public + * @throws Exception If specified route doesn't exist + * @static + */ + public static function get($name) { + if (!isset(Route::$rules[$name])) + throw new Exception("Route {$name} not found."); + $route = new Route(); + $route-> name = $name; + return $route; + } + + /** + * Matches the URI against available routes to find the correct one. + * + * @param string $uri Request URI + * @return Route + * @access public + * @throws Exception If no route matches the URI + * @throws Exception If route matched but no Controller was defined for it + * @throws Exception If route matched but no action was defined for it + * @static + */ + public static function match($uri) { + $matched = false; + foreach(Route::$rules as $name=>$rule) { + $rule=$rule['rule']; + if (is_callable($rule)) { + if (($data = $rule($uri)) !== FALSE) { + $matched = $name; + break; + } + }else { + $pattern = is_array($rule)?$rule[0]:$rule; + $pattern = str_replace(')', ')?', $pattern); + + $pattern=preg_replace_callback('/<.*?>/', + function($str) use ($rule){ + $str=$str[0]; + $regexp='[a-zA-Z0-9\-\.]+'; + if(is_array($rule)) + $regexp=Misc::arr($rule[1],str_replace(array('<','>'),'',$str),$regexp); + return '(?P'.$str.$regexp.')'; + },$pattern); + + preg_match('#^'.$pattern.'/?$#',$uri,$match); + if(!empty($match[0])){ + $matched=$name; + $data=array(); + foreach($match as $k=>$v) + if(!is_numeric($k)) + $data[$k]=$v; + break; + } + } + } + if($matched==false) + throw new Exception('No route matched your request'); + $rule=Route::$rules[$matched]; + + $params=array_merge($rule['defaults'],$data); + + if(!isset($params['controller'])) + throw new Exception("Route {$matched} matched, but no controller was defined for this route"); + if(!isset($params['action'])) + throw new Exception("Route {$matched} matched with controller {$params['controller']}, but no action was defined for this route"); + + $route=Route::get($matched); + $route->params=$params; + return $route; + } + +}
\ No newline at end of file diff --git a/system/classes/session.php b/system/classes/session.php new file mode 100644 index 0000000..963aa9c --- /dev/null +++ b/system/classes/session.php @@ -0,0 +1,57 @@ +<?php + +/** + * Simple class for accessing session data + */ +class Session{ + + /** + * Flag to check if the session was already intialized + * @var boolean + * @access private + * @static + */ + private static $initialized=false; + + /** + * Makes sure the session is initialized + * + * @return void + * @access private + * @static + */ + private static function check(){ + if(!Session::$initialized){ + session_start(); + Session::$initialized=true; + } + } + + /** + * Gets a session variable + * + * @param string $key Variable name + * @param mixed $default Default value + * @return mixed Session value + * @access public + * @static + */ + public static function get($key, $default = null) { + Session::check(); + return Misc::arr($_SESSION,$key,$default); + } + + /** + * Sets a session variable + * + * @param string $key Variable name + * @param mixed $val Variable value + * @return void + * @access public + * @static + */ + public static function set($key, $val) { + Session::check(); + $_SESSION[$key]=$val; + } +}
\ No newline at end of file diff --git a/system/classes/view.php b/system/classes/view.php new file mode 100644 index 0000000..5b7b6e7 --- /dev/null +++ b/system/classes/view.php @@ -0,0 +1,95 @@ +<?php + +/** + * Manages passing variables to templates and rendering them + */ +class View{ + + /** + * Full path to template file + * @var string + * @access private + */ + private $path; + + /** + * The name of the view. + * @var string + * @access public + */ + public $name; + + /** + * Stores all the variables passed to the view + * @var array + * @access private + */ + private $_data = array(); + + /** + * Manages storing the data passed to the view as properties + * + * @param string $key Property name + * @param string $val Property value + * @return void + * @access public + */ + public function __set($key, $val) { + $this->_data[$key]=$val; + } + + /** + * Manages accessing passed data as properties + * + * @param string $key Property name + * @return mixed Property value + * @access public + * @throws Exception If the property is not found + */ + public function __get($key){ + if (isset($this->_data[$key])) + return $this->_data[$key]; + throw new Exception("Value {$key} not set for view {$name}"); + } + + /** + * Renders the template, all dynamically set properties + * will be available inside the view file as variables. + * Example: + * <code> + * $view = View::get('frontpage'); + * $view->title = "Page title"; + * echo $view->render(); + * </code> + * + * @return string Rendered template + * @access public + */ + public function render() { + extract($this->_data); + ob_start(); + include($this->path); + $out = ob_get_contents(); + ob_end_clean(); + return $out; + } + + /** + * Constructs the view + * + * @param string $name The name of the template to use + * @return View + * @access public + * @throws Exception If specified template is not found + * @static + */ + public static function get($name){ + $view = new View(); + $view->name = $name; + $file = Misc::find_file('view', $name); + if ($file == false) + throw new Exception("View {$name} not found."); + $view->path=$file; + return $view; + } +}
\ No newline at end of file diff --git a/system/views/debug.php b/system/views/debug.php new file mode 100644 index 0000000..0d78601 --- /dev/null +++ b/system/views/debug.php @@ -0,0 +1,126 @@ +<!DOCTYPE html> +<html> + <head> + <title>Error</title> + <style> + html{ + width:100%; + min-height:100%; + font-family:'Verdana'; + font-size:14px; + } + body{ + + min-height:100%; + background: #a90329; /* Old browsers */ + background: -moz-radial-gradient(center, ellipse cover, #a90329 0%, #6d0019 100%); /* FF3.6+ */ + background: -webkit-radial-gradient(center, ellipse cover, #a90329 0%,#6d0019 100%); /* Chrome10+,Safari5.1+ */ + } + #content{ + width:1000px; + margin:auto; + padding:10px 0px; + background:#eee; + } + .file{ + font-weight:bold; + } + .block{ + border-bottom:1px solid #000; + margin:10px; + } + .code{ + + padding:10px; + } + .highlight{ + background:#efecd0; + } + #exception{ + font-size:25px; + font-weight:bold; + padding:10px; + } + #debug{ + border-bottom: 1px solid black; + margin: 10px; + } + #log{ + font-size:15px; + font-weight:bold; + padding:5px; + } + .log{ + padding:10px; + border-bottom: 1px solid black; + } + .log.odd{ + + } + pre{ + margin:0px; + } + .thick{ + border-width:2px; + } + </style> + </head> + <body> + <?php + $rawblocks=array_merge(array(array( + 'file'=>$exception->getFile(), + 'line'=>$exception->getLine() + )), $exception->getTrace()); + $blocks = array(); + foreach($rawblocks as $block){ + if(!isset($block['file'])) + continue; + //avoid duplicates + if(count($blocks)>0){ + $last=$blocks[count($blocks)-1]; + if($block['file']==$last['file'] && $block['line']==$last['line']) + continue; + } + $blocks[]=$block; + } + + + ?> + <div id="content"> + <div id="exception"><?php echo str_replace("\n",'<br/>',$exception->getMessage()); ?></div> + <div id="blocks"> + <?php foreach($blocks as $bkey=>$block): ?> + <div class="block <?php echo (!empty($log)&&$bkey==0)?'thick':''; ?>"> + <div class="file"><?php echo $block['file'];?></div> + <div class="code"> + <?php + $line=$block['line']-1; + $code = explode("\n", file_get_contents($block['file'])); + $start = $line - 3; + if ($start < 0) $start = 0; + $end = $line + 3; + if($end>=count($code)) $end=count($code)-1; + $code=array_slice($code,$start,$end-$start,true); + ?> + + <?php foreach($code as $n=>$text):?> + <pre class="line <?php echo $n==$line?'highlight':''; ?>"><?php echo ($n+1).' '.htmlspecialchars($text); ?></pre> + <?php endforeach;?> + </div> + </div> + <?php if($bkey==0&&!empty($log)):?> + <div id="debug"> + <div id="log">Logged values:</div> + <?php foreach($log as $key=>$val):?> + <div class="log <?php echo $key%2?'odd':''; ?>"> + <pre><?php var_export($val);?></pre> + </div> + <?php endforeach;?> + </div> + <div id="log">Call stack:</div> + <?php endif;?> + <?php endforeach;?> + </div> + </div> + </body> +</html>
\ No newline at end of file diff --git a/system/views/error.php b/system/views/error.php new file mode 100644 index 0000000..6204f35 --- /dev/null +++ b/system/views/error.php @@ -0,0 +1,94 @@ +<!DOCTYPE html> +<html> + <head> + <title>Error</title> + <style> + html{ + width:100%; + min-height:100%; + font-family:'Verdana'; + font-size:14px; + } + body{ + + min-height:100%; + background: #a90329; /* Old browsers */ + background: -moz-radial-gradient(center, ellipse cover, #a90329 0%, #6d0019 100%); /* FF3.6+ */ + background: -webkit-radial-gradient(center, ellipse cover, #a90329 0%,#6d0019 100%); /* Chrome10+,Safari5.1+ */ + } + #content{ + width:1000px; + margin:auto; + padding:10px 0px; + background:#eee; + } + .file{ + font-weight:bold; + } + .block{ + border-bottom:1px solid #000; + margin:10px; + } + .code{ + + padding:10px; + } + .highlight{ + background:#efecd0; + } + #exception{ + font-size:25px; + font-weight:bold; + padding:10px; + } + + </style> + </head> + <body> + <?php + $rawblocks=array_merge(array(array( + 'file'=>$exception->getFile(), + 'line'=>$exception->getLine() + )), $exception->getTrace()); + $blocks = array(); + foreach($rawblocks as $block){ + if(!isset($block['file'])) + continue; + //avoid duplicates + if(count($blocks)>0){ + $last=$blocks[count($blocks)-1]; + if($block['file']==$last['file'] && $block['line']==$last['line']) + continue; + } + $blocks[]=$block; + } + + + ?> + <div id="content"> + <div id="exception"><?php echo str_replace("\n",'<br/>',$exception->getMessage()); ?></div> + <div id="blocks"> + <?php foreach($blocks as $block): ?> + <div class="block"> + <div class="file"><?php echo $block['file'];?></div> + <div class="code"> + <?php + $line=$block['line']-1; + $code = explode("\n", file_get_contents($block['file'])); + $start = $line - 3; + if ($start < 0) $start = 0; + $end = $line + 3; + if($end>=count($code)) $end=count($code)-1; + $code=array_slice($code,$start,$end-$start,true); + ?> + + <?php foreach($code as $n=>$text):?> + <pre class="line <?php echo $n==$line?'highlight':''; ?>"><?php echo ($n+1).' '.htmlspecialchars($text); ?></pre> + <?php endforeach;?> + </div> + </div> + <?php endforeach;?> + </div> + </div> + </body> +</html>
\ No newline at end of file |