Max YO %!s(int64=6) %!d(string=hai) anos
pai
achega
3f1b7a7528

+ 2 - 2
composer.json

@@ -12,8 +12,8 @@
         "php": ">=5.4.0"
     },
     "autoload": {
-        "psr-0": {
-            "KarmaFW": "src/"
+        "psr-4": {
+            "KarmaFW\\": "src/"
         }
     }
 }

+ 94 - 0
helpers/helpers_array.php

@@ -0,0 +1,94 @@
+<?php
+
+
+if (! function_exists('arrayReduceToOneColumn')) {
+	function arrayReduceToOneColumn($array, $column_key) {
+		return array_map(function($row) use ($column_key) {
+			if (is_callable($column_key)) {
+				return $column_key($row);
+
+			} else {
+				return $row[$column_key];
+			}
+		}, $array);
+	}
+}
+
+if (! function_exists('arrayAddKeyFromColumn')) {
+	function arrayAddKeyFromColumn($array, $column_key) {
+		$results = array();
+		foreach ($array as $row) {
+			if (is_callable($column_key)) {
+				$key = $column_key($row);
+			} else if (is_array($column_key)) {
+				$key_parts = [];
+				foreach ($column_key as $column_key_item) {
+					$key_parts[] = $row[$column_key_item];
+				}
+				$key = implode('-', $key_parts);
+			}else{
+				$key = $row[$column_key];
+			}
+			$results[$key] = $row;
+		}
+		if (empty($results)) {
+			//return new stdClass();
+		}
+		return $results;
+	}
+}
+
+if (! function_exists('arrayGroupByColumn')) {
+	function arrayGroupByColumn($array, $column_key) {
+		$results = array();
+		foreach ($array as $k => $v) {
+			if (is_callable($column_key)) {
+				$key_value = $column_key($v);
+			} else {
+				$key_value = $v[$column_key];
+				
+			}
+			if (! isset($results[$key_value])) {
+				$results[$key_value] = array();
+			}
+			$results[$key_value][$k] = $v;
+		}
+		return $results;
+	}
+}
+
+
+if (! function_exists('get_csv')) {
+	function get_csv($arr, $fields=array(), $sep=";") {
+		$str = '';
+		if (! empty($arr)) {
+
+			if (empty($fields)) {
+				$fields = array_keys($arr[0]);
+			}
+
+			$line = array();
+			foreach ($fields as $k => $v) {
+				if (! is_numeric($k)) {
+					$line[] = $k;
+
+				} else {
+					$line[] = $v;
+				}
+			}
+			$str .= implode($sep, $line) . PHP_EOL;
+
+			foreach ($arr as $row) {
+				$line = array();
+				foreach ($fields as $field) {
+					$line[] = $row[$field];
+				}
+				//$str .= implode($sep, $line) . PHP_EOL;
+				//$str .= '"' . implode('"' . $sep . '"', str_replace('"', '\\"', $line)) . '"' . PHP_EOL;
+				$str .= '"' . implode('"' . $sep . '"', str_replace('"', '""', $line)) . '"' . PHP_EOL;
+			}
+		}
+		return $str;
+	}
+}
+

+ 60 - 0
helpers/helpers_default.php

@@ -0,0 +1,60 @@
+<?php
+
+
+if (! function_exists('pre')) {
+	function pre($var, $exit = false, $prefix = '') {
+		echo "<pre>";
+		if (!empty($prefix)) {
+			echo $prefix;
+		}
+		print_r($var);
+		echo "</pre>";
+
+		if ($exit) {
+			exit;
+		}
+	}
+}
+
+
+if (! function_exists('errorHttp')) {
+	function errorHttp($error_code, $message='An error has occured', $title='Error') {
+		header("HTTP/1.0 " . $error_code . " " . $title);
+		echo '<h1>' . $title . '</h1>';
+		echo '<p>' . $message . '</p>';
+		exit;
+	}
+}
+
+
+if (! function_exists('redirect')) {
+	function redirect($url, $http_code=302) {
+		header('Location: ' . $url, true, $http_code);
+		exit;
+	}
+}
+
+
+if (! function_exists('get')) {
+	function get($key, $default_value=null) {
+		return isset($_GET[$key]) ? $_GET[$key] : $default_value;
+	}
+}
+
+if (! function_exists('post')) {
+	function post($key, $default_value=null) {
+		return isset($_POST[$key]) ? $_POST[$key] : $default_value;
+	}
+}
+
+if (! function_exists('session')) {
+	function session($key, $default_value=null) {
+		return isset($_SESSION[$key]) ? $_SESSION[$key] : $default_value;
+	}
+}
+
+if (! function_exists('cookie')) {
+	function cookie($key, $default_value=null) {
+		return isset($_COOKIE[$key]) ? $_COOKIE[$key] : $default_value;
+	}
+}

+ 114 - 1
src/App.php

@@ -2,12 +2,125 @@
 
 namespace KarmaFW;
 
+use KarmaFW\Routing\Router;
+use KarmaFW\Hooks\HooksManager;
+use KarmaFW\Database\Sql\SqlDb;
+use \KarmaFW\Database\Sql\SqlOrmModel;
+use KarmaFW\Templates\Templater;
+
+
+define('FW_SRC_DIR', __DIR__);
+define('FW_DIR', __DIR__ . "/..");
+
+if (! defined('APP_DIR')) {
+	echo "ERROR: Please, define APP_DIR" . PHP_EOL;
+	exit(1);
+}
+
 
 class App
 {
+	protected static $booted = false;
+
 	public static function boot()
 	{
-		echo "BOOTED";
+		HooksManager::applyHook('fw_app_boot__before', []);
+
+		// include helpers
+		self::loadHelpers(FW_SRC_DIR . "/../helpers");
+
+		self::$booted = true;
+		HooksManager::applyHook('fw_app_boot__after', []);
 	}	
 
+
+	protected static function loadHelpers($dir)
+	{
+		$helpers = glob($dir . '/helpers_*.php');
+
+		foreach ($helpers as $helper) {
+			require $helper;
+		}
+	}
+
+
+	public static function route()
+	{
+		if (! self::$booted) {
+			self::boot();
+		}
+
+		// routing: parse l'url puis transfert au controller
+
+		$route = Router::routeByUrl( $_SERVER['REQUEST_METHOD'], $_SERVER['REQUEST_URI'], false );
+
+		if ($route) {
+			//echo "success: route ok";
+			exit(0);
+
+		} else if ($route === null) {
+			// route found but callback is not callable
+			HooksManager::applyHook('fw_bootstrap_404', []);
+			errorHttp(404, 'Warning: route callback is not callable', '404 Not Found');
+			exit(1);
+
+		} else if ($route === true) {
+			// route found but no callback defined
+			HooksManager::applyHook('fw_bootstrap_404', []);
+			errorHttp(404, "Warning: route found but no callback defined", '404 Not Found');
+			exit(1);
+
+		} else if ($route === false) {
+			// no matching route
+			HooksManager::applyHook('fw_bootstrap_404', []);
+			errorHttp(404, "Warning: no matching route", '404 Not Found');
+			exit(1);
+
+		} else {
+			exit(1);
+		}
+
+	}
+
+
+	public static function getDb($instance_name=null, $dsn=null)
+	{
+		/*
+		$dsn = 'mysql://user:pass@localhost/my_app';
+		*/
+		static $instances = [];
+		static $last_instance_name = null;
+
+		if (empty($instance_name)) {
+			$instance_name = 'default';
+
+			//if (! empty($last_instance_name)) {
+			//	$instance_name = $last_instance_name;
+			//}
+		}
+
+		$last_instance_name = $instance_name;
+
+		if (empty($instances[$instance_name])) {
+			if (empty($dsn) && defined('DB_DSN')) {
+				$dsn = DB_DSN;
+			}
+			$instances[$instance_name] = new SqlDb($dsn);
+		}
+
+		return $instances[$instance_name];
+	}
+
+
+	public static function createTemplate()
+	{
+		return new Templater();
+	}
+
+
+	public static function createOrmModel($db, $table_name, $primary_key_values=[])
+	{
+		return new SqlOrmModel($db, $table_name, $primary_key_values);
+	}
+
 }

+ 66 - 0
src/Database/Sql/Drivers/Mysqli/MySqliDriver.php

@@ -0,0 +1,66 @@
+<?php
+
+namespace KarmaFW\Database\Sql\Drivers\Mysqli;
+
+use \KarmaFW\Database\Sql\SqlDriver;
+use \KarmaFW\Database\Sql\SqlDriverInterface;
+
+
+class MySqliDriver extends SqlDriver implements SqlDriverInterface
+{
+
+	public function connect()
+	{
+		extract($this->credentials);
+
+		$this->conn = \mysqli_init();
+
+		if (@\mysqli_real_connect($this->conn, $host, $user, $passwd, $db, $port) && ! mysqli_connect_errno()) {
+			$this->connected = true;
+
+		} else {
+			$this->connected = false;
+
+			if ($this->db->throwOnConnectionError) {
+				throw new \Exception("Cannot connect to the database. " . mysqli_connect_error(), 1);
+			}
+		}
+
+		return $this->connected;
+	}
+
+
+	public function disconnect()
+	{
+		mysqli_close($this->conn);
+		parent::disconnect();
+	}
+
+
+	public function execute($query)
+	{
+		if (! $this->connected) {
+			if ($this->db->throwOnConnectionError) {
+				throw new \Exception("Cannot execute query (reason: Not connected to the database)", 1);
+			}
+			return null;
+		}
+
+		$rs = mysqli_query($this->conn, $query);
+		return new MysqliResultset($rs);
+	}
+
+
+	public function getInsertId()
+	{
+		return $this->getConn()->insert_id;
+	}
+
+
+	public function getAffectedRowsCount()
+	{
+		return $this->getConn()->affected_rows;
+	}
+
+
+}

+ 70 - 0
src/Database/Sql/Drivers/Mysqli/MysqliResultset.php

@@ -0,0 +1,70 @@
+<?php
+
+namespace KarmaFW\Database\Sql\Drivers\Mysqli;
+
+use \KarmaFW\Database\Sql\SqlResultset;
+use \KarmaFW\Database\Sql\SqlResultsetInterface;
+
+
+class MysqliResultset extends SqlResultset implements SqlResultsetInterface
+{
+	protected $found_rows = null;
+
+
+	function __construct($rs, $found_rows=null)
+	{
+		$this->rs = $rs;
+		$this->found_rows = $found_rows;
+	}
+
+
+	public function fetchColumn($column_name)
+	{
+		$row = $this->fetchOne();
+		return isset($row[$column_name]) ? $row[$column_name] : null;
+	}
+
+
+	public function fetchOne()
+	{
+		if (empty($this->rs)) {
+			return [];
+		}
+		return mysqli_fetch_assoc($this->rs);
+	}
+
+
+	public function fetchAll()
+	{
+		$rows = parent::fetchAll();
+
+		if (! is_null($this->found_rows)) {
+			$rows = array(
+				'FOUND_ROWS' => $this->found_rows,
+				'ROWS' => $rows,
+			);
+		}
+
+		return $rows;
+	}
+
+
+	public function getRowsCount()
+	{
+		if (empty($this->rs)) {
+			return null;
+		}
+		if (is_bool($this->rs)) {
+			return null;
+		}
+		return $this->rs->num_rows;
+	}
+
+
+	public function getfoundRowsCount()
+	{
+		return $this->found_rows;
+	}
+
+}
+

+ 67 - 0
src/Database/Sql/Readme.md

@@ -0,0 +1,67 @@
+
+// DROP DATABASE
+```
+$db->dropDatabase('test', true);
+```
+// returns boolean
+
+
+// CREATE DATABASE
+```
+$db->createDatabase('test', true);
+```
+// returns boolean
+
+// USE DATABASE
+```
+$db->use('test');
+```
+// returns boolean
+
+
+// CREATE TABLE
+```
+$db->createTable('TEST', ['id' => 'int(11) not null auto_increment', 'my_int' => 'int(11) null', 'my_text' => "varchar(32) not null default ''"], ['primary key (id)'], true);
+```
+// returns boolean
+
+
+// INSERT ROW FROM OBJECT
+```
+$test = new \StdClass;
+$test->my_int = '111';
+$test->my_text = 'ok';
+$db->getTable('TEST')->insert($test);
+```
+// returns insert_id
+
+// INSERT ROW FROM ARRAY
+```
+$test = [
+	'my_int' => '111',
+	'my_text' => 'ok',
+];
+$db->getTable('TEST')->insert($test);
+```
+// returns insert_id
+
+
+// UPDATE ROW
+```
+$db->getTable('TEST')->update(['my_int' => '11111', 'my_text' => 'ok ok'], ['id' => 1]);
+```
+// return affected_rows
+
+
+// GET ROWS
+```
+$test = $db->getTable('TEST')->getAll();
+```
+// returns array (2 dimensions)
+
+// GET ROW
+```
+$test = $db->getTable('TEST')->getOne();
+```
+// returns array
+

+ 215 - 0
src/Database/Sql/SqlDb.php

@@ -0,0 +1,215 @@
+<?php
+
+namespace KarmaFW\Database\Sql;
+
+
+class SqlDb
+{
+	protected $driver = null;
+	protected $last_query = null;
+	protected $schema = null;
+	protected $tools = null;
+	public $throwOnSqlError = false;
+	public $throwOnConnectionError = true;
+
+
+	public function __construct($dsn=null, $driver_class=null)
+	{
+		$this->schema = new SqlSchema($this);
+		$this->tools = new SqlTools($this);
+
+		$credentials = $this->parseDSN($dsn);
+
+		if (empty($driver_class)) {
+			$driver_name = $credentials['driver'];
+			$driver_class = $this->getDriverClass($driver_name);
+		}
+
+		$this->driver = new $driver_class($this, $credentials);
+
+	}
+
+
+    public function __debugInfo() {
+        return [
+            'driver:protected' => get_class($this->driver) ." Object",
+        ];
+    }
+
+
+	/* #### */
+
+
+	protected function getDriverClass($driver_name)
+	{
+		$available_drivers = [
+			'mysql' => \KarmaFW\Database\Sql\Drivers\Mysqli\MySqliDriver::class,
+			'sqlite' => \KarmaFW\Database\Sql\Drivers\Mysqli\SqliteDriver::class,
+		];
+		return isset($available_drivers[$driver_name]) ? $available_drivers[$driver_name] : null;
+	}
+
+
+	public function getDriver()
+	{
+		return $this->driver;
+	}
+
+
+	public function setLastQuery(SqlQuery $last_query)
+	{
+		return $this->last_query = $last_query;
+	}
+
+	public function getLastQuery()
+	{
+		return $this->last_query;
+	}
+
+
+
+	public function createQuery($query=null)
+	{
+		return new SqlQuery($this, $query);
+	}
+
+
+
+	public function getTable($table_name) : SqlTable
+	{
+		return new SqlTable($this, $table_name);
+	}
+
+
+	/* CONNECTION */
+
+
+	public function connect()
+	{
+		return $this->getDriver()->connect();
+	}
+
+
+	public function disconnect()
+	{
+		return $this->getDriver()->disconnect();
+	}
+
+
+	/* #### */
+
+
+	public function execute($sql, $params=[])
+	{
+		return $this->createQuery()->execute($sql, $params);
+	}
+
+
+	public function getInsertId()
+	{
+		return $this->getDriver()->getInsertId();
+	}
+
+
+	public function getAffectedRowsCount()
+	{
+		return $this->getDriver()->getAffectedRowsCount();
+	}
+
+
+	/* SCHEMA */
+
+	// DATABASE
+
+	public function use($database_name)
+	{
+		return $this->schema->useDatabase($database_name);
+	}
+
+	public function dropDatabase($database_name, $if_exists=false)
+	{
+		return $this->schema->dropDatabase($database_name, $if_exists);
+	}
+
+	public function createDatabase($database_name, $if_not_exists=false)
+	{
+		return $this->schema->createDatabase($database_name, $if_not_exists);
+	}
+
+	public function listDatabases($database_name=null)
+	{
+		return $this->schema->listDatabases($database_name);
+	}
+
+	// TABLE
+
+	public function createTable($table_name, array $columns, array $indexes=[], $if_not_exists=false)
+	{
+		return $this->schema->createTable($table_name, $columns, $indexes, $if_not_exists);
+	}
+
+	public function dropTable($table_name, $if_exists=false)
+	{
+		return $this->schema->dropTable($table_name, $if_exists);
+	}
+
+	public function listTables($table_name=null, $database_name=null)
+	{
+		return $this->schema->listTables($table_name, $database_name);
+	}
+
+	/*
+	public function getTable($table_name)
+	{
+		return $this->schema->getTable($table_name);
+	}
+	*/
+
+
+	// COLUMN
+
+	public function listTableColumns($table_name, $column_name=null)
+	{
+		return $this->schema->listTableColumns($table_name, $column_name);
+	}
+
+	public function listTableIndexes($table_name)
+	{
+		return $this->schema->listTableIndexes($table_name);
+	}
+
+
+
+	/* TOOLS */
+
+
+	public function escape($var)
+	{
+		return $this->tools->escape($var);
+	}
+
+
+	public function buildSqlWhere($var)
+	{
+		return $this->tools->buildSqlWhere($var);
+	}
+
+
+	public function buildSqlUpdateValues($var)
+	{
+		return $this->tools->buildSqlUpdateValues($var);
+	}
+
+
+	public function buildSqlInsertValues($var)
+	{
+		return $this->tools->buildSqlInsertValues($var);
+	}
+
+
+	public function parseDSN($var)
+	{
+		return $this->tools->parseDSN($var);
+	}
+
+}

+ 89 - 0
src/Database/Sql/SqlDriver.php

@@ -0,0 +1,89 @@
+<?php
+
+namespace KarmaFW\Database\Sql;
+
+
+abstract class SqlDriver implements SqlDriverInterface
+{
+	protected $db = null;
+	protected $credentials = [];
+	protected $connected = false;
+	protected $conn = null;
+
+
+	public function __construct($db, $credentials=null)
+	{
+		$this->db = $db;
+
+		if (! empty($credentials)) {
+			$this->credentials = $credentials;
+		}
+	}
+
+
+    public function __debugInfo() {
+        return [
+            'driver_name:protected' => $this->credentials['driver'],
+            'conn:protected' => get_class($this->conn) ." Object",
+        ];
+    }
+
+
+	/* #### */
+
+
+	public function connect()
+	{
+		// EXTENDS ME
+		$this->conn = new \stdClass;
+		$this->connected = true;
+	}
+
+	public function disconnect()
+	{
+		// EXTENDS ME
+		$this->conn = null;
+		$this->connected = false;
+	}
+
+
+	public function getInsertId()
+	{
+		// EXTENDS ME
+		return null;
+	}
+
+
+	public function getAffectedRowsCount()
+	{
+		// EXTENDS ME
+		return null;
+	}
+
+
+	/* #### */
+
+
+	public function isConnected()
+	{
+		return $this->connected;
+	}
+
+	public function getConn()
+	{
+		return $this->conn;
+	}
+
+
+	/* #### */
+
+
+	public function execute($query)
+	{
+		// EXTENDS ME
+		return new SqlResultset(null);
+	}
+
+
+}
+

+ 22 - 0
src/Database/Sql/SqlDriverInterface.php

@@ -0,0 +1,22 @@
+<?php
+
+namespace KarmaFW\Database\Sql;
+
+
+interface SqlDriverInterface
+{
+
+	public function connect();
+
+	public function disconnect();
+
+	public function isConnected();
+
+	public function execute($query);
+
+	public function getInsertId();
+	
+	public function getAffectedRowsCount();
+
+}
+

+ 202 - 0
src/Database/Sql/SqlOrmModel.php

@@ -0,0 +1,202 @@
+<?php
+
+namespace KarmaFW\Database\Sql;
+
+
+class SqlOrmModel
+{
+	protected $db;
+	protected $table_row = [];
+	protected $status = 'ready';
+	protected $primary_keys = null; 		// example:  ['id_product']       ... or ... ['id_product', 'id_category']
+	protected $primary_key_values = null; 	// example:  ['id_product' => 14] ... or ... ['id_product' => 123, 'id_category' => 14]
+	public $table_name;
+	public $autosave = false;
+
+
+	public function __construct($db, $table_name=null, $primary_key_values=[])
+	{
+		$this->db = $db;
+		$this->table_name = $table_name;
+		$this->primary_key_values = $primary_key_values;
+	}
+
+
+
+	public function __set($name, $value)
+	{
+		$this->table_row[$name] = $value;
+		$this->status = 'unsaved';
+
+		if ($this->autosave) {
+			$this->save();
+		}
+	}
+
+	public function __get($name)
+	{
+		if (array_key_exists($name, $this->table_row)) {
+			return $this->table_row[$name];
+		}
+
+		$trace = debug_backtrace();
+		trigger_error(
+			'Propriété non-définie via __get() : ' . $name .
+			' dans ' . $trace[0]['file'] .
+			' à la ligne ' . $trace[0]['line'],
+			E_USER_NOTICE);
+		return null;
+	}
+
+	/**  Depuis PHP 5.1.0  */
+	public function __isset($name)
+	{
+		return isset($this->table_row[$name]);
+	}
+
+	/**  Depuis PHP 5.1.0  */
+	public function __unset($name)
+	{
+		unset($this->table_row[$name]);
+	}
+
+
+
+	public function clear()
+	{
+		return $this->loadFromArray([]);
+	}
+	
+
+	public function asArray()
+	{
+		// Alias of toArray
+		return $this->toArray();	
+	}
+
+	public function getArray()
+	{
+		// Alias of toArray
+		return $this->toArray();	
+	}
+
+	public function toArray()
+	{
+		return $this->table_row;
+	}
+
+	public function toJSON() 
+	{
+		return json_encode($this->table_row);
+	}
+
+
+	public function fetchPrimaryKeys()
+	{
+		if (empty($this->table_name)) {
+			return false;
+		}
+
+		$this->primary_keys = [];
+
+		$columns = $this->db->listTableColumns($this->table_name);
+		foreach ($columns as $column) {
+			if ($column['Key'] == 'PRI') {
+				$this->primary_keys[] = $column['Field'];
+			}
+		}
+
+		return $this->primary_keys;
+	}
+	
+
+	public function setPrimaryKeysValues(array $primary_key_values=null)
+	{
+		$this->primary_key_values = $primary_key_values;
+	}
+
+
+	public function loadFromArray($data, $primary_key_values=null)
+	{
+		$this->primary_key_values = $primary_key_values;
+		$this->table_row = array_slice($data, 0);
+		//pre($this->table_row);
+
+		if (! empty($this->primary_keys)) {
+			foreach ($this->primary_keys as $column_name) {
+				if (isset($this->table_row[$column_name])) {
+					$this->primary_key_values[$column_name] = $this->table_row[$column_name];
+				}
+			}
+		}
+
+		return $this;
+	}
+
+
+	public function load(array $primary_key_values)
+	{
+		$this->primary_key_values = $primary_key_values;
+
+		$data = $this->db->createQuery()->tableSelect($this->table_name, $this->primary_key_values, ['limit' => 1])->fetchOne();
+		$result = $this->loadFromArray($data, $primary_key_values);
+
+		return $result;
+	}
+
+
+	public function save($force = false)
+	{
+		if (empty($this->primary_keys)) {
+			// on recupere les primary_keys depis le schema de ma table sql
+			$this->fetchPrimaryKeys();
+
+			if (empty($this->primary_keys)) {
+				// cannot update beacause no primary key found
+				return false;
+			}
+		}
+
+		if ($this->status == 'saved' && ! $force) {
+			return true;
+		}
+
+		if (empty($this->primary_key_values)) {
+			// INSERT 
+
+			$id = $this->db->createQuery()->tableInsert($this->table_name, $this->table_row);
+
+			if (! empty($id)) {
+				if (count($this->primary_keys) > 1) {
+					// TODO: gerer correctement les index multiples (normalement un seul des champ est en autoincrement. TODO...)
+					$this->primary_key_values = [];
+					foreach ($this->primary_keys as $column_name) {
+						if (! isset($this->table_row[$column_name])) {
+							$this->table_row[$column_name] = null; // TODO: trouver mieux que null
+						}
+						$this->primary_key_values = [ $column_name => $this->table_row[$column_name] ];
+					}
+
+				} else {
+					$column_name = $this->primary_keys[0];
+					$this->primary_key_values = [ $column_name => $id ];
+
+					// on affecte le insert_id a sa variable equivalente en php
+					$this->table_row[$column_name] = $id;
+				}
+			}
+
+		} else {
+			// UPDATE
+
+			$this->db->createQuery()->tableUpdate($this->table_name, $this->table_row, $this->primary_key_values, ['limit' => 1]);
+			
+		}
+
+		$this->status = 'saved';
+
+		return true;
+	}
+
+
+}

+ 260 - 0
src/Database/Sql/SqlQuery.php

@@ -0,0 +1,260 @@
+<?php
+
+namespace KarmaFW\Database\Sql;
+
+
+class SqlQuery
+{
+	protected $db;
+	protected $duration = null;
+	protected $error = null;
+	protected $recordset = null;
+	protected $status = 'ready';
+	protected $query = null;
+	protected $results_rows_count = null;
+	protected $affected_rows_count = null;
+
+	public function __construct($db, $query=null)
+	{
+		$this->db = $db;
+		$this->query = $query;
+	}
+
+
+	public function __toString()
+	{
+		return $this->getQuery();
+	}
+
+
+    public function __debugInfo() {
+        return [
+            //'db:protected' => get_class($this->db) ." Object",
+            'db:protected' => $this->db,
+            'status:protected' => $this->status,
+            'error:protected' => $this->error,
+            'query:protected' => $this->query,
+            'duration:protected' => $this->duration,
+            //'recordset:protected' => get_class($this->recordset) ." Object",
+            'recordset:protected' => $this->recordset,
+            'results_rows_count:protected' => $this->results_rows_count,
+            'affected_rows_count:protected' => $this->affected_rows_count,
+        ];
+    }
+
+
+	public function fetchColumn($column_name)
+	{
+		if ($this->status == 'ready') {
+			$this->execute();
+		}
+		return $this->recordset->fetchColumn($column_name);
+	}
+
+
+	public function fetchOne()
+	{
+		if ($this->status == 'ready') {
+			$this->execute();
+		}
+		return $this->recordset->fetchOne();
+	}
+
+
+	public function fetchAll()
+	{
+		if ($this->status == 'ready') {
+			$this->execute();
+		}
+		return $this->recordset->fetchAll();
+	}
+
+
+	public function getQuery()
+	{
+		return $this->query;
+	}
+
+
+	public function getStatus()
+	{
+		return $this->status;
+	}
+
+
+	public function execute($query=null, $params=[])
+	{
+		if (empty($query)) {
+			$query = $this->query;
+		}
+
+		if (! empty($params)) {
+			$parts = explode('?', $query);
+
+			$query = array_shift($parts);
+			$i = 1;
+			foreach ($params as $param) {
+				if (empty($parts)) {
+					throw new \Exception('SqlQuery::execute() => params error (1) [QUERY: ' . preg_replace('/\s+/', ' ', $query) . ' ]');
+				}
+				$param = $this->db->escape($param);
+				$query .= $param;
+				$query .= array_shift($parts);
+				$i++;
+			}
+			if (! empty($parts)) {
+				throw new \Exception('SqlQuery::execute() => params error (2) [QUERY: ' . preg_replace('/\s+/', ' ', $query) . ' ]');
+			}
+		}
+
+		$this->query = preg_replace('/\s+/', ' ', $query);
+
+		$this->status = 'running';
+		$ts_start = microtime(true);
+
+		//echo $query . "<hr />";
+		$rs = $this->db->getDriver()->execute($query);
+		//pre($rs);
+
+		$ts_end = microtime(true);
+		$this->duration = $ts_end - $ts_start;
+		
+		$this->recordset = $rs;
+		$this->db->setLastQuery($this);
+
+		$error_code = $this->db->getDriver()->getConn()->errno;
+	
+		if ($error_code !== 0) {
+			// query error
+			$error_msg = $this->db->getDriver()->getConn()->error;
+			$this->error = $error_msg;
+			$this->status = 'error';
+
+			if ($this->db->throwOnSqlError) {
+				throw new \Exception('SqlQuery::execute() => DB error [' . $error_code . '] : ' . $error_msg . PHP_EOL . '[QUERY: ' . preg_replace('/\s+/', ' ', $query) . ' ]');
+			}
+			//return null;
+		
+		} else {
+			$this->status = 'success';
+		}
+
+		$this->results_rows_count = $rs->getRowsCount();
+		$this->affected_rows_count = $this->db->getDriver()->getAffectedRowsCount();
+
+		if (strpos($query, "SQL_CALC_FOUND_ROWS")) {
+	        $found_rows = $this->execute('SELECT FOUND_ROWS() AS found_rows')->oneField('found_rows');
+		} else {
+			$found_rows = null;
+		}
+
+
+		return $this;
+	}
+
+	
+
+	public function executeSelect($query, $params=[])
+	{
+		// Alias of executeSelectAll
+		return $this->executeSelectAll($query, $params);
+	}
+
+	public function executeSelectOne($query, $params=[])
+	{
+		return $this->execute($query, $params)->fetchOne();
+	}
+
+	public function executeSelectAll($query, $params=[])
+	{
+		return $this->execute($query, $params)->fetchAll();
+	}
+
+
+	public function executeInsert($query, $params=[])
+	{
+		$this->execute($query, $params);
+		return $this->insert_id();
+	}
+	
+
+	public function executeInsertAll($query, $params=[])
+	{
+		return $this->execute($query, $params);
+	}
+
+
+	public function executeUpdate($query, $params=[])
+	{
+		$this->execute($query, $params);
+		return $this->affected_rows_count;
+	}
+
+
+	public function executeDelete($query, $params=[])
+	{
+		$this->execute($query, $params);
+		return $this->affected_rows_count;
+	}
+
+
+	/* ### */
+
+
+
+
+	public function tableSelect($table_name, $where=[], $options=[])
+	{
+		// Alias of tableSelectAll
+		return $this->tableSelectAll($table_name, $where, $options)->all();
+	}
+
+	public function tableSelectAll($table_name, $where=[], $options=[])
+	{
+		$table = new SqlTable($this->db, $table_name);
+		$query = $table->buildQuery($where, $options);
+		return $this->executeSelectAll($query);
+	}
+
+	public function tableSelectOne($table_name, $where=[], $options=[])
+	{
+		if (empty($options)) {
+			$options = [];
+		}
+		$options['limit'] = 1;
+
+		return $this->tableSelect($table_name, $where, $options)->one();
+	}
+
+
+
+
+
+	public function tableInsert($table_name, $values=[], $options=[])
+	{
+		$this->tableInsertAll($table_name, [$values], $options);
+		return $this->db->getInsertId();
+	}
+
+
+	public function tableInsertAll($table_name, $inserts=[], $options=[])
+	{
+		$table = new SqlTable($this->db, $table_name);
+		return $table->insertAll($inserts, $options);		
+	}
+
+
+	public function tableUpdate($table_name, $updates=[], $where=[], $options=[])
+	{
+		$table = new SqlTable($this->db, $table_name);
+		return $table->update($updates, $where, $options);
+	}
+
+
+	public function tableDelete($table_name, $where=[], $options=[])
+	{
+		$table = new SqlTable($this->db, $table_name);
+		return $table->delete($where, $options);
+	}
+
+}

+ 71 - 0
src/Database/Sql/SqlResultset.php

@@ -0,0 +1,71 @@
+<?php
+
+namespace KarmaFW\Database\Sql;
+
+
+class SqlResultset implements SqlResultsetInterface
+{
+	protected $rs = null;
+
+
+	function __construct($rs)
+	{
+		$this->rs = $rs;
+	}
+
+
+
+    public function __debugInfo() {
+        return [
+            'rs:protected' => is_object($this->rs) ?  (get_class($this->rs) . ' Object') : (gettype($this->rs)),
+        ];
+    }
+
+
+
+	public function fetchColumn($column_name)
+	{
+		$row = $this->fetchOne();
+		if ($row) {
+			return $row[$column_name];
+		}
+		return null;
+	}
+
+	public function one()
+	{
+		// Alias of fetchOne
+		return $this->fetchOne();
+	}
+	
+	public function fetchOne()
+	{
+		// extend me
+		return null;
+	}
+
+
+	public function all()
+	{
+		// Alias of fetchAll
+		return $this->fetchAll();
+	}
+	
+	public function fetchAll()
+	{
+		$rows = [];
+		while ($row = $this->fetchOne()) {
+			$rows[] = $row;
+		}
+
+		return $rows;
+	}
+
+
+	public function getRowsCount()
+	{
+		// extend me
+		return 0;
+	}
+
+}

+ 15 - 0
src/Database/Sql/SqlResultsetInterface.php

@@ -0,0 +1,15 @@
+<?php
+
+namespace KarmaFW\Database\Sql;
+
+
+interface SqlResultsetInterface
+{
+
+	public function fetchOne();
+	
+	public function fetchAll();
+
+	public function getRowsCount();
+
+}

+ 168 - 0
src/Database/Sql/SqlSchema.php

@@ -0,0 +1,168 @@
+<?php
+
+namespace KarmaFW\Database\Sql;
+
+
+class SqlSchema
+{
+	protected $db;
+
+	public function __construct($db)
+	{
+		$this->db = $db;
+	}
+
+
+	/* DATABASES */
+
+	public function useDatabase($database_name) : bool
+	{
+		$sql = "use " . $database_name;
+		$ret = $this->db->createQuery()->execute($sql);
+		return ($ret->getStatus() == 'success');
+	}
+
+
+	public function dropDatabase($database_name, $if_exists=false) : bool
+	{
+		if ($if_exists) {
+			$sql = "drop database if exists " . $database_name;
+
+		} else {
+			$sql = "drop database " . $database_name;
+		}
+		$ret = $this->db->createQuery()->execute($sql);
+		return ($ret->getStatus() == 'success');
+	}
+
+
+	public function createDatabase($database_name, $if_not_exists=false) : bool
+	{
+		if ($if_not_exists) {
+			$sql = "create database if not exists " . $database_name;
+
+		} else {
+			$sql = "create database " . $database_name;
+		}
+		$ret = $this->db->createQuery()->execute($sql);
+		return ($ret->getStatus() == 'success');
+	}
+
+
+	public function listDatabases($database=null) : array
+	{
+		$sql = "show databases";
+
+		if (! empty($database)) {
+			$sql .= " like '%" . str_replace("'", "\\'", $database) . "%'";
+		}
+		
+		$rs = $this->db->createQuery()->execute($sql);
+		$rows = $rs->fetchAll();
+
+		$databases = array_map(function ($row) {return array_values($row)[0];}, $rows);
+
+		return $databases;
+	}
+
+
+	/* TABLES */
+
+	public function createTable($table_name, array $columns, array $indexes=[], $if_not_exists=false) : bool
+	{
+		if ($if_not_exists) {
+			$sql = "create table if not exists " . $table_name . " (" . PHP_EOL;
+
+		} else {
+			$sql = "create table " . $table_name . " (" . PHP_EOL;
+		}
+
+		$sql_table_columns = [];
+		foreach ($columns as $column_name => $column_type) {
+			$sql_table_columns[] = "`" . $column_name . "` " . $column_type;
+		}
+		
+		foreach ($indexes as $index_def) {
+			$sql_table_columns[] = $index_def;
+		}
+
+		$sql .= implode(',' . PHP_EOL, $sql_table_columns) . PHP_EOL;
+
+		$sql .= ")";
+
+		$query = $this->db->createQuery();
+		$ret = $query->execute($sql);
+		//pre($query);
+		return ($ret->getStatus() == 'success');
+
+	}
+
+
+
+	public function dropTable($table_name, $if_exists=false) : bool
+	{
+		if ($if_exists) {
+			$sql = "drop table if exists " . $table_name;
+
+		} else {
+			$sql = "drop table " . $table_name;
+		}
+		$ret = $this->db->createQuery()->execute($sql);
+		return ($ret->getStatus() == 'success');
+	}
+
+
+	public function listTables($table=null, $database=null) : array
+	{
+		$sql = "show tables";
+
+		if (! empty($database)) {
+			$sql .= " from `" . $database . "`";
+		}
+
+		if (! empty($table)) {
+			$sql .= " like '%" . str_replace("'", "\\'", $table) . "%'";
+		}
+		
+		$rs = $this->db->execute($sql);
+		$rows = $rs->fetchAll();
+
+		$tables = array_map(function ($row) {return array_values($row)[0];}, $rows);
+
+		return $tables;
+	}
+
+
+	/* COLUMNS */
+
+	public function listTableColumns($table, $column=null) : array
+	{
+		$sql = "show columns from " . $table;
+
+		if (! empty($column)) {
+			$sql .= " like '%" . str_replace("'", "\\'", $column) . "%'";
+		}
+		
+		$rs = $this->db->execute($sql);
+		$rows = $rs->fetchAll();
+
+		$columns = arrayAddKeyFromColumn($rows, 'Field');
+
+		return $columns;
+	}
+
+
+	public function listTableIndexes($table) : array
+	{
+		$sql = "show indexes from " . $table;
+
+		$rs = $this->db->execute($sql);
+		$rows = $rs->fetchAll();
+
+		//$indexes = arrayAddKeyFromColumn($rows, 'Field');
+
+		return $rows;
+	}
+
+}
+

+ 168 - 0
src/Database/Sql/SqlTable.php

@@ -0,0 +1,168 @@
+<?php
+
+namespace KarmaFW\Database\Sql;
+
+
+class SqlTable
+{
+	protected $db;
+	protected $table_name;
+	protected $columns = null;
+
+
+	public function __construct($db, $table_name)
+	{
+		$this->db = $db;
+		$this->table_name = $table_name;
+	}
+
+
+	public function getTableName()
+	{
+		return $this->table_name;
+	}
+
+
+	public function __toString()
+	{
+		return $this->getTableName();
+	}
+
+
+	public function listColumns() : array
+	{
+		$schema = new SqlSchema($this->db);
+		$this->columns = $schema->listTableColumns($table, $column=null);
+		return $this->columns;
+	}
+
+
+	public function insert($values=[], $options=[]) : int
+	{
+		$this->insertAll([$values], $options);
+		return $this->db->getInsertId();
+	}
+
+
+	public function insertAll($rows=[], $options=[]) 
+	{
+		/*
+		$rows = [
+			['id' => 1, 'name' => 'foo'],
+			['id' => 2, 'name' => 'bar'],
+		];
+		*/
+		$values_array = [];
+		$fields_sql = '';
+		foreach ($rows as $values) {
+			$insert = $this->db->buildSqlInsertValues($values);
+			$values_array[] = "(" . $insert['values'] . ")";
+
+			if (empty($fields_sql)) {
+				$fields_sql = $insert['fields'];
+			}
+		}
+		if (empty($values_array)) {
+			return null;
+		}
+
+		$inserts_sql = implode(', ', $values_array);
+
+		$query = "insert into " . $this->table_name . " (" . $fields_sql . ") values " . $inserts_sql;
+		return $this->db->createQuery()->executeInsertAll($query);
+	}
+
+
+	public function update(array $updates=[], array $where=[], $options=[]) : int
+	{
+		$limit_sql = (isset($options['limit']) && ! is_null($options['limit'])) ? ("limit " . $options['limit']) : "";
+
+		$query = "update " . $this->table_name . "
+					set " . $this->db->buildSqlUpdateValues($updates) . "
+					where " . $this->db->buildSqlWhere($where) . "
+					" . $limit_sql;
+		return $this->db->createQuery()->executeUpdate($query);
+	}
+
+
+	public function delete(array $where=[], $options=[]) : int
+	{
+		$limit_sql = isset($options['limit']) ? ("limit " . $options['limit']) : "";
+
+		$query = "delete from " . $this->table_name . "
+					where " . $this->db->buildSqlWhere($where) . "
+					" . $limit_sql;
+		return $this->db->createQuery()->executeDelete($query);
+	}
+
+
+	public function select($where=null, $options=[])
+	{
+		// Alias of getAll
+		return $this->getAll($where, $options);
+	}
+
+	public function selectAll($where=null, $options=[])
+	{
+		// Alias of getAll
+		return $this->getAll($where, $options);
+	}
+
+	public function getAll($where=null, $options=[]) : array
+	{
+		//return $this->db->createQuery()->tableSelect($this->table_name, $where, $options)->fetchAll();
+
+		$query = $this->buildQuery($where, $options);
+		return $this->db->createQuery()->executeSelectAll($query);
+	}
+
+
+	public function selectOne($where=null, $options=[])
+	{
+		// Alias of getOne
+		return $this->getOne($where, $options);
+	}
+
+	public function getOne($where=null, $options=[]) : array
+	{
+		$options['limit'] = 1;
+		return $this->getAll($where, $options)->fetchOne();
+	}
+
+
+	public function buildQuery($where=null, $options=[]) : string
+	{
+		$limit_sql = isset($options['limit']) ? ("limit " . $options['limit']) : "";
+		$order_by_sql = isset($options['order_by']) ? ("order by " . $options['order_by']) : "";
+		$table_name = isset($options['from']) ? $options['from'] : $this->table_name;
+
+		$select_sql = '*';
+		if (! empty($options['select'])) {
+			$options['select'] = is_array($options['select']) ? $options['select'] : [$options['select']];
+			$select_sql = implode(', ', $options['select']);
+		}
+		if (! empty($options['CALC_FOUND_ROWS'])) {
+			$select_sql = 'SQL_CALC_FOUND_ROWS ' . $select_sql;
+		}
+
+		$joins_sql = '';
+		if (! empty($options['join'])) {
+			$options['join'] = is_array($options['join']) ? $options['join'] : [$options['join']];
+			$joins_sql = implode(' ', $options['join']);
+		}
+
+		$query = "select " . $select_sql . "
+					from " . $this->table_name . "
+					" . $joins_sql . "
+					where " . $this->db->buildSqlWhere($where) . "
+					" . $order_by_sql . "
+					" . $limit_sql;
+
+		if (! empty($options['debug'])) {
+			echo "<pre>" .preg_replace('/\s+/', '', $query) . "</pre>";
+		}
+
+		return $query;
+	}
+
+}

+ 200 - 0
src/Database/Sql/SqlTools.php

@@ -0,0 +1,200 @@
+<?php
+
+namespace KarmaFW\Database\Sql;
+
+
+class SqlTools
+{
+    protected $db;
+
+
+    public function __construct($db)
+    {
+        $this->db = $db;
+    }
+
+
+
+    public function parseDSN($dsn)
+    {
+        // PARSE A DSN LIKE mysql://user:password@host:port/database
+        // AND RETURNS driver,host,port,user,passwd,db
+
+        if (empty($dsn)) {
+            return [
+                'driver' => '',
+                'host' => '',
+                'port' => '',
+                'user' => '',
+                'passwd' => '',
+                'db' => '',
+            ];
+        }
+
+        $parts0 = explode(':', $dsn);
+        $driver = $parts0[0];
+
+        $parts1 = explode('/', $dsn);
+        $user_passwd_host_port = $parts1[2];
+        $db = ! empty($parts1[3]) ? $parts1[3] : null;
+
+        $parts2 = explode('@', $user_passwd_host_port);
+        if (count($parts2) > 1) {
+            // USER (AND OPTIONNALY PASSWORD) IS DEFINED
+            // mysql://user@host/database
+            // mysql://user@host:port/database
+            // mysql://user:password@host/database
+            // mysql://user:password@host:port/database
+            $user_password = $parts2[0];
+            $host_port = $parts2[1];
+        } else {
+            // USER AND PASSWORD ARE NOT DEFINED
+            // mysql://host/database
+            // mysql://host:port/database
+            $user_password = '';
+            $host_port = $parts2[0];
+        }
+
+        $parts3 = explode(':', $host_port);
+        $host = $parts3[0];
+        if (count($parts3) > 1) {
+            // HOST AND PORT ARE DEFINED
+            // mysql://user@host:port/database
+            // mysql://user:password@host:port/database
+            $port = $parts3[1];
+        } else {
+            // HOST IS DEFINED. PORT IS NOT DEFINED
+            // mysql://user@host/database
+            // mysql://user:password@host/database
+            $port = 3306;
+        }
+
+        $parts4 = explode(':', $user_password);
+        $user = $parts4[0];
+        if (count($parts4) > 1) {
+            // USER AND PASSWORD ARE DEFINED
+            // mysql://user:password@host/database
+            // mysql://user:password@host:port/database
+            $passwd = $parts4[1];
+        } else {
+            // USER IS DEFINED. PASSWORD IS NOT DEFINED
+            // mysql://user@host/database
+            // mysql://user@host:port/database
+            $passwd = '';
+        }
+
+        return [
+            'driver' => $driver,
+            'host' => $host,
+            'port' => $port,
+            'user' => $user,
+            'passwd' => $passwd,
+            'db' => $db,
+        ];
+    }
+
+
+	public function escape($var)
+	{
+		if (is_null($var)) {
+			return 'NULL';
+		}
+		if (is_bool($var)) {
+			return intval($var);
+		}
+		if (is_int($var)) {
+			return intval($var);
+		}
+		if (is_float($var)) {
+			return floatval($var);
+		}
+		return "'" . $this->db->getDriver()->getConn()->real_escape_string($var) . "'";
+	}
+
+
+	public function buildSqlWhere($where)
+	{
+        $where_sql = array("1" => "1");
+        if (! empty($where)) {
+            foreach ($where as $key => $value) {
+                if (is_null($value)) {
+                    $where_sql[] = $key . ' is null';
+
+                }else if (is_bool($value) || is_int($value)) {
+                    $where_sql[] = $key . ' = ' . intval($value);
+
+                }else if (is_float($value)) {
+                    $where_sql[] = $key . ' = ' . floatval($value);
+
+                }else if (is_string($value)) {
+                    $where_sql[] = $key . ' = ' . $this->escape($value);
+/*
+                }else if ($value instanceof DB\DbWhere) {
+                    $where_sql[] = (string) $value;
+
+                }else if ($value instanceof DB\DbExpr) {
+                    $where_sql[] = $key . ' = ' . (string) $value;
+*/
+                    
+                }else{
+                    $where_sql[] = $key . ' = ' . $this->escape($value);
+                    //$where_sql[] = $key . ' = ' . (string) $value;
+                }
+            }
+        }
+        //print_r($where_sql);
+        return implode(" and ", $where_sql);
+
+	}
+
+	public function buildSqlUpdateValues($values)
+	{
+        $values_sql = array();
+
+        if (is_object($values)) {
+            $values = get_object_vars($values);
+        }
+
+        foreach ($values as $key => $value) {
+            if (is_null($value)) {
+                $values_sql[] = $key . ' = NULL';
+            }else if (gettype($value) === 'string') {
+                $values_sql[] = $key . ' = ' . $this->escape($value);
+            }else if (gettype($value) === 'boolean') {
+                $values_sql[] = $key . ' = ' . intval($value);
+            }else{
+                $values_sql[] = $key . ' = ' . $value;
+            }
+        }
+        return implode(", ", $values_sql);
+	}
+
+	public function buildSqlInsertValues($values)
+	{
+        $fields_sql = array();
+        $values_sql = array();
+
+        if (is_object($values)) {
+            $values = get_object_vars($values);
+        }
+
+        foreach ($values as $key => $value) {
+            if (is_null($value)) {
+                $values_sql[] = 'NULL';
+            }else if (gettype($value) === 'string') {
+                $values_sql[] = $this->escape($value);
+            }else if (gettype($value) === 'boolean') {
+                $values_sql[] = intval($value);
+            }else{
+                $values_sql[] = $value;
+            }
+            $fields_sql[] = $key;
+        }
+        return array(
+        	'fields' => implode(', ', $fields_sql),
+        	'values' => implode(', ', $values_sql),
+        );
+	}
+
+}
+

+ 11 - 0
src/Database/Sql/testModel.php

@@ -0,0 +1,11 @@
+<?php
+
+namespace KarmaFW\Database\Sql;
+
+
+class testModel extends SqlOrmModel
+{
+	protected $table_name = 'TEST';
+
+}
+

+ 23 - 0
src/Hooks/HooksManager.php

@@ -0,0 +1,23 @@
+<?php
+
+namespace KarmaFW\Hooks;
+
+class HooksManager {
+	// source: https://stackoverflow.com/questions/5931324/what-is-a-hook-in-php
+
+    private static $actions = [];
+
+    public static function applyHook($hook, $args = array()) {
+        if (!empty(self::$actions[$hook])) {
+            foreach (self::$actions[$hook] as $f) {
+                $f($args);
+            }
+        }
+    }
+
+    public static function addHookAction($hook, $function) {
+        self::$actions[$hook][] = $function;
+    }
+
+}
+

+ 22 - 0
src/Routing/Controllers/WebController.php

@@ -0,0 +1,22 @@
+<?php
+
+namespace KarmaFW\Routing\Controllers;
+
+
+class WebController
+{
+	protected $route = null;
+	protected $request_method = null;
+	protected $request_uri = null;
+
+
+	public function __construct($route, $request_method, $request_uri)
+	{
+		$this->route = $route;
+		$this->request_method = $request_method;
+		$this->request_uri = $request_uri;
+		
+		//echo "DEBUG " . __CLASS__ . ": controller instanced<hr />" . PHP_EOL;
+	}
+
+}

+ 135 - 0
src/Routing/Route.php

@@ -0,0 +1,135 @@
+<?php
+
+namespace KarmaFW\Routing;
+
+
+class Route
+{
+	private $name = null;
+	private $methods = [];
+	private $match_url = '';
+	private $match_type = 'exact';
+	private $regex_params = [];
+	private $callback = null;
+
+
+	public function __construct()
+	{
+
+	}
+
+	// Set route method (can be called multiple times for differents methods)
+	public function setMethod($method = null)
+	{
+		if (! is_null($method)) {
+			$this->methods[] = $method;
+		}
+	}
+
+	// Set route match url
+	public function setMatchUrl($match_url)
+	{
+		$this->match_url = $match_url;
+	}
+
+	// Get route match url
+	public function getMatchUrl()
+	{
+		return $this->match_url;
+	}
+
+	// Set route match type (exact, startsWith, endsWith, regex, regexStartsWith, regexEndsWith)
+	public function setMatchType($match_type)
+	{
+		$this->match_type = $match_type;
+	}
+
+	// Set route regex params (WORKS WITH: regex, regexStartsWith, regexEndsWith)
+	public function setRegexParams(array $regex_params)
+	{
+		$this->regex_params = $regex_params;
+	}
+
+	// Get route name
+	public function getName()
+	{
+		return $this->name;
+	}
+
+	// Set route name
+	public function setName($name)
+	{
+		$this->name = $name;
+	}
+
+	// Set route callback
+	public function setCallback($callback)
+	{
+		$this->callback = $callback;
+	}
+
+	// Get route callback
+	public function getCallback()
+	{
+		return $this->callback;
+	}
+
+
+	// Check if route is matching the request_method and request_uri
+	public function match($request_method, $request_uri)
+	{
+		if (empty($this->methods) || in_array($request_method, $this->methods)) {
+
+			$request_uri_short = explode('?', $request_uri)[0];
+			
+			// exact match
+			if ($this->match_type == 'exact') {
+				if ($request_uri_short === $this->match_url) {
+					return [];
+				}
+			}
+
+			// startsWith
+			if ($this->match_type == 'startsWith') {
+				if (substr($request_uri_short, 0, strlen($this->match_url)) === $this->match_url) {
+					return [];
+				}
+			}
+
+			// endsWith
+			if ($this->match_type == 'endsWith') {
+				if (substr($request_uri_short, -1 * strlen($this->match_url)) === $this->match_url) {
+					return [];
+				}
+			}
+
+			// regex / regexStartsWith / regexEndsWith
+			if (in_array($this->match_type, ['regex', 'regexStartsWith', 'regexEndsWith'])) {
+				$match_pattern = '#^' . $this->match_url . '$#';
+	
+				if ($this->match_type == 'regexStartsWith') {
+					$match_pattern = '#^' . $this->match_url . '#';
+				}
+
+				if ($this->match_type == 'regexEndsWith') {
+					$match_pattern = '#' . $this->match_url . '$#';
+				}
+
+				if (preg_match($match_pattern, $request_uri_short, $regs)) {
+					$matched_uri = array_shift($regs); // $matched_uri == $request_uri_short
+					$args = $regs;
+
+					if (! empty($this->regex_params)) {
+						$args = array_combine($this->regex_params, $args);
+					}
+
+					return $args;
+				}
+			}
+
+		}
+
+		return null;
+	}
+
+}

+ 119 - 0
src/Routing/Router.php

@@ -0,0 +1,119 @@
+<?php
+
+namespace KarmaFW\Routing;
+
+
+class Router
+{
+	private static $routes = [];
+
+
+	// Add a route to the router
+	public static function add($methods, $url_match, $callback=null, $type_match='exact', $regex_params=[])
+	{
+		$route = new Route();
+
+		$route->setMatchUrl($url_match);
+		$route->setCallback($callback);
+		$route->setMatchType($type_match	);
+		$route->setRegexParams($regex_params);
+		
+		if (! is_array($methods)) {
+			$methods = [$methods];
+		}
+		foreach ($methods as $method) {
+			$route->setMethod($method);
+		}
+
+		self::$routes[] = $route;
+
+		return $route;
+	}
+
+
+	// Allow whatever method (GET, POST, HEAD, OPTION, DELETE, PUT, ...)
+	public static function all($url_match, $callback=null, $type_match='exact', $regex_params=[])
+	{
+		return self::Add(null, $url_match, $callback, $type_match, $regex_params);
+	}
+
+	// GET method
+	public static function get($url_match, $callback=null, $type_match='exact', $regex_params=[])
+	{
+		return self::Add('GET', $url_match, $callback, $type_match, $regex_params);
+	}
+
+	// POST method
+	public static function post($url_match, $callback=null, $type_match='exact', $regex_params=[])
+	{
+		return self::Add('POST', $url_match, $callback, $type_match, $regex_params);
+	}
+
+
+	// Lookup the first matching route then execute it 
+	public static function routeByUrl($request_method, $request_uri, $debug = false)
+	{
+		foreach (self::$routes as $route) {
+			if ($debug) {
+				pre($route);
+			}
+
+			$match_params = $route->match($request_method, $request_uri);
+
+			if (! is_null($match_params)) {
+				if ($debug) {
+					echo " => MATCH !<br />" . PHP_EOL;
+				}
+
+				$callback = $route->getCallback();
+				if (empty($callback)) {
+					// Do nothing
+					return false;
+
+				} else if (is_callable($callback)) {
+					self::routeRun($route, $callback, $request_method, $request_uri, $match_params);
+
+				} else {
+					// Error: callback not callable
+					return null;
+				}
+
+				return $route;
+			}
+		}
+
+		// No matching route
+		return false;
+	}
+
+	public static function routeRun($route, $callback, $request_method, $request_uri, $match_params)
+	{
+		if (gettype($callback) == 'array') {
+			$class = new $callback[0]($route, $request_method, $request_uri);
+			call_user_func([$class, $callback[1]], $match_params);
+
+		} else {
+			$callback($route, $request_method, $request_uri);
+		}
+
+		return true;
+	}
+
+
+	// Search a route by its name
+	public static function findRouteByName($expected_route_name, $debug = false)
+	{
+		if (empty($expected_route_name)) {
+			return null;
+		}
+		foreach (self::$routes as $route) {
+			$route_name = $route->getName();
+			if (! empty($route_name) && $route_name == $expected_route_name) {
+				return $route;
+			}
+		}
+		return null;
+	}
+
+
+}

+ 121 - 0
src/Templates/Templater.php

@@ -0,0 +1,121 @@
+<?php
+
+namespace KarmaFW\Templates;
+
+
+class Templater
+{
+	public $tpl_dir = APP_DIR . '/templates';
+	protected $vars = [];
+	protected $plugins = [];
+
+	function __construct($tpl_dir=null, $default_vars=[])
+	{
+		if (is_null($tpl_dir) && defined('TPL_DIR')) {
+			$tpl_dir = TPL_DIR;
+		}
+		
+		if (! is_null($tpl_dir)) {
+			$this->tpl_dir = $tpl_dir;
+		}
+
+		$this->assign($default_vars);
+
+		$this->addPlugin('tr', function ($param) {
+			return gettext($param);
+		});
+	}
+
+	public function fetch($tpl, $layout=null, $extra_vars=array())
+	{
+		$tpl_dirs = [];
+
+		if (! is_null($this->tpl_dir) && is_dir($this->tpl_dir)) {
+			$tpl_dirs[] = $this->tpl_dir; // user templates
+		}
+
+		if (is_dir(FW_DIR . '/templates')) {
+			$tpl_dirs[] = FW_DIR . '/templates'; // framework templates
+		}
+
+		if (empty($tpl_dirs)) {
+			throw new \Exception("No Templates dir", 1);
+		}
+
+		$tpl_path = false;
+		foreach ($tpl_dirs as $tpl_dir) {
+			$tpl_path = $tpl_dir . '/' . $tpl;
+
+			if (is_file($tpl_path)) {
+				break;
+			}
+
+			$tpl_path = null;
+		}
+
+		if (is_null($tpl_path)) {
+			throw new \Exception("Template not found : " . $tpl, 1);
+		}
+		
+		extract($this->vars);
+		extract($extra_vars);
+		
+		if ($tpl_path) {
+			ob_start();
+			include($tpl_path);
+			$content = ob_get_contents();
+			ob_end_clean();
+
+		} else {
+			$content = '';
+		}
+
+
+		// plugins. ex: {tr:English text} ==> "Texte francais"
+		if (! empty($this->plugins)) {
+			foreach ($this->plugins as $prefix => $callback) {
+				preg_match_all('/{' . $prefix . ':([^}]+)}/', $content, $regs, PREG_SET_ORDER);
+				foreach($regs as $reg) {
+					$replaced = $callback($reg[1]);
+					$content = str_replace($reg[0], $replaced, $content);
+				}
+
+			}
+		}
+
+
+		if (empty($layout)) {
+			return $content;
+
+		} else {
+			$content_layout = $this->fetch($layout, null, array('layout_content' => $content));
+			return $content_layout;
+		}
+	}
+
+	public function display($tpl, $layout=null)
+	{
+		echo $this->fetch($tpl, $layout);
+	}
+
+	public function assign($var_name, $var_value=null)
+	{
+		if (is_array($var_name)) {
+			foreach ($var_name as $k => $v) {
+				$this->assign($k, $v);
+			}
+			return $this;
+		}
+
+		$this->vars[$var_name] = $var_value;
+
+		return $this;
+	}
+
+
+	public function addPlugin($prefix, $callback)
+	{
+		$this->plugins[$prefix] = $callback;
+	}
+
+}