summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDracony <draconyster@gmail.com>2012-12-26 15:13:57 +0200
committerDracony <draconyster@gmail.com>2012-12-26 15:13:57 +0200
commitdb5bd4e2a84cfbbd662a7dbd9c6fc93edfb376d1 (patch)
tree77335fecf9b3e0995323df3c9887b593cb79aedf
downloadPHPixie-db5bd4e2a84cfbbd662a7dbd9c6fc93edfb376d1.zip
PHPixie-db5bd4e2a84cfbbd662a7dbd9c6fc93edfb376d1.tar.gz
PHPixie-db5bd4e2a84cfbbd662a7dbd9c6fc93edfb376d1.tar.bz2
Moving to github
-rw-r--r--.htaccess11
-rw-r--r--index.php14
-rw-r--r--modules/database/classes/database/expression.php31
-rw-r--r--modules/database/classes/database/query.php380
-rw-r--r--modules/database/classes/database/result.php74
-rw-r--r--modules/database/classes/db.php127
-rw-r--r--modules/database/classes/driver/mysql/db.php88
-rw-r--r--modules/database/classes/driver/mysql/query.php199
-rw-r--r--modules/database/classes/driver/mysql/result.php47
-rw-r--r--modules/database/classes/driver/pdo/db.php70
-rw-r--r--modules/database/classes/driver/pdo/query.php8
-rw-r--r--modules/database/classes/driver/pdo/result.php47
-rw-r--r--modules/orm/classes/orm.php579
-rw-r--r--modules/orm/classes/ormresult.php113
-rw-r--r--system/bootstrap.php72
-rw-r--r--system/classes/config.php51
-rw-r--r--system/classes/controller.php84
-rw-r--r--system/classes/debug.php122
-rw-r--r--system/classes/error.php99
-rw-r--r--system/classes/misc.php61
-rw-r--r--system/classes/request.php112
-rw-r--r--system/classes/response.php69
-rw-r--r--system/classes/route.php126
-rw-r--r--system/classes/session.php57
-rw-r--r--system/classes/view.php95
-rw-r--r--system/views/debug.php126
-rw-r--r--system/views/error.php94
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