Browse Source

Merge branch 'master' of ssh://gogs.karmas.fr/KarmaSolutions/KarmaFW

Max F 5 years ago
parent
commit
94c71913d4

+ 29 - 0
Readme.md

@@ -344,3 +344,32 @@ nano templates/homepage2.tpl.php
 </pre>
 
 ```
+
+
+
+## Good practives
+
+## Configuration
+fichiers de conf (config.php & routes.php)
+page 404
+erreurs et exceptions (page 500)
+
+### SEO
+url rewriting
+title, meta desc, h1, canonical
+liens
+robots.txt
+sitemap.xml
+
+### Performance
+opcache, memcache/redis
+cache de templates
+minimify/combine css & js
+gzip
+etag + not_modified_304 + expire
+cdn
+
+### Stats
+traffic logguer
+google analytics
+google webmaster tools

+ 2 - 2
example/templates/layout_base.tpl.php

@@ -12,7 +12,7 @@
     <link rel="stylesheet" type="text/css" href="/assets/vendor/alertify-1.11.1/css/alertify.css">
     <link rel="stylesheet" type="text/css" href="/assets/vendor/tui.chart-3.11.2/tui-chart.min.css">
     <link rel="stylesheet" type="text/css" href="/assets/css/app.css">
-    {$block_extra_css}
+    {yield block_css}
 
     <script type="text/javascript">var onload_actions = []; function registerOnloadAction(func) { onload_actions.push(func); }</script>
   </head>
@@ -28,7 +28,7 @@
     <script type="text/javascript" src="/assets/vendor/tui.chart-3.11.2/tui-chart.min.js"></script>
     <script type="text/javascript" src="/assets/vendor/jQuery-Autocomplete-1.4.11/jquery.autocomplete.min.js"></script>
     <script type="text/javascript" src="/assets/js/app.js"></script>
-    {$block_extra_js}
+    {yield block_js}
 
     <script type="text/javascript">executeOnloadActions();</script>
   </body>

+ 16 - 12
src/App.php

@@ -3,7 +3,7 @@
 namespace KarmaFW;
 
 use \KarmaFW\Lib\Hooks\HooksManager;
-use \KarmaFW\Database\Sql\SqlDb;
+//use \KarmaFW\Database\Sql\SqlDb;
 //use \KarmaFW\Database\Sql\SqlOrmModel;
 
 class App
@@ -77,16 +77,16 @@ class App
 
 		// define class aliases
 		class_alias('\\KarmaFW\\App', 'App');
-		class_alias('\\KarmaFW\\Database\\Sql\\SqlDb', 'SqlDb');
-		class_alias('\\KarmaFW\\Database\\Sql\\SqlSchema', 'SqlSchema');
-		class_alias('\\KarmaFW\\Database\\Sql\\SqlTable', 'SqlTable');
-		class_alias('\\KarmaFW\\Database\\Sql\\SqlOrmModel', 'SqlOrmModel');
-		class_alias('\\KarmaFW\\Database\\Sql\\SqlQuery', 'SqlQuery');
-		class_alias('\\KarmaFW\\Database\\Sql\\SqlWhere', 'SqlWhere');
-		class_alias('\\KarmaFW\\Database\\Sql\\SqlExpr', 'SqlExpr');
-		class_alias('\\KarmaFW\\Database\\Sql\\SqlLike', 'SqlLike');
-		class_alias('\\KarmaFW\\Database\\Sql\\SqlIn', 'SqlIn');
-		class_alias('\\KarmaFW\\Database\\Sql\\SqlTools', 'SqlTools');
+		//class_alias('\\KarmaFW\\Database\\Sql\\SqlDb', 'SqlDb');
+		//class_alias('\\KarmaFW\\Database\\Sql\\SqlSchema', 'SqlSchema');
+		//class_alias('\\KarmaFW\\Database\\Sql\\SqlTable', 'SqlTable');
+		//class_alias('\\KarmaFW\\Database\\Sql\\SqlOrmModel', 'SqlOrmModel');
+		//class_alias('\\KarmaFW\\Database\\Sql\\SqlQuery', 'SqlQuery');
+		//class_alias('\\KarmaFW\\Database\\Sql\\SqlWhere', 'SqlWhere');
+		//class_alias('\\KarmaFW\\Database\\Sql\\SqlExpr', 'SqlExpr');
+		//class_alias('\\KarmaFW\\Database\\Sql\\SqlLike', 'SqlLike');
+		//class_alias('\\KarmaFW\\Database\\Sql\\SqlIn', 'SqlIn');
+		//class_alias('\\KarmaFW\\Database\\Sql\\SqlTools', 'SqlTools');
 		
 
 		if (defined('DB_DSN')) {
@@ -200,7 +200,11 @@ class App
 			if (empty($dsn) && defined('DB_DSN')) {
 				$dsn = DB_DSN;
 			}
-			$instances[$instance_name] = new SqlDb($dsn);
+
+			//$instances[$instance_name] = new SqlDb($dsn);
+
+			$db = App::getData('app')->get('db');
+			$instances[$instance_name] = $db($dsn);
 		}
 
 		return $instances[$instance_name];

+ 12 - 3
src/App/Middlewares/CacheHtml.php

@@ -22,11 +22,13 @@ class CacheHtml
     public function __invoke(Request $request, Response $response, callable $next)
     {
     	$request_uri = $request->SERVER['REQUEST_URI'];
+
+        $cacheable = $request->isGet();
     	
     	$cache_key = md5($request_uri);
     	$cache_file = $this->cache_dir . '/' . $cache_key . '.cache.html';
 
-    	if (is_file($cache_file) && filectime($cache_file) > time() - $this->cache_duration ) {
+    	if ($cacheable && is_file($cache_file) && filectime($cache_file) > time() - $this->cache_duration ) {
     		// Get response content from file cache
     		$content = file_get_contents($cache_file);
     		$response->setContent($content);
@@ -34,9 +36,16 @@ class CacheHtml
 
     	} else {
         	$response = $next($request, $response);
+            
+            $cacheable = $response->getAttribute('cacheable', $cacheable);
+
+            if ($cacheable) {
+            	file_put_contents($cache_file, $response->getContent());
+            	$response->addHeader('X-Cache-Html', 'miss');
 
-        	file_put_contents($cache_file, $response->getContent());
-        	$response->addHeader('X-Cache-Html', 'miss');
+            } else {
+                $response->addHeader('X-Cache-Html', 'not cacheable');
+            }
     	}
 
         return $response;

+ 39 - 10
src/App/Middlewares/DebugBar.php

@@ -4,13 +4,16 @@ namespace KarmaFW\App\Middlewares;
 
 use \DebugBar\StandardDebugBar;
 //use \DebugBar\DataCollector\MessagesCollector;
-use \DebugBar\DataCollector\TimeDataCollector;
+use \DebugBar\DataCollector\ConfigCollector;
 
 use \KarmaFW\App;
 use \KarmaFW\Http\Request;
 use \KarmaFW\Http\Response;
+use \KarmaFW\Http\UserAgent;
 use \KarmaFW\App\Middlewares\DebugBar\KarmaFwCollector;
+use \KarmaFW\App\Middlewares\DebugBar\SEOCollector;
 use \KarmaFW\App\Middlewares\DebugBar\SqlDbCollector;
+use \KarmaFW\App\Middlewares\DebugBar\SqlDbTimelineCollector;
 use \KarmaFW\App\Middlewares\DebugBar\KarmaMessagesCollector;
 //use \KarmaFW\App\Middlewares\DebugBar\PhpTemplateCollector;
 
@@ -21,16 +24,19 @@ class DebugBar
 	public function __invoke(Request $request, Response $response, callable $next)
 	{
 		$load_debugbar = ( class_exists('\\DebugBar\\StandardDebugBar') && ((defined('ENV') && ENV == 'dev') || defined('FORCE_DEBUGBAR') && FORCE_DEBUGBAR)  );
+		$load_debugbar = $load_debugbar && $request->isGet() && ! $request->isAjax() && (! isset($_GET['debugbar']) || ! empty($_GET['debugbar']));
 
 		if ($load_debugbar) {
 			$debugbar = new StandardDebugBar();
 			App::setData('debugbar', $debugbar);
 			
+			$debugbar->addCollector(new ConfigCollector);
 			$debugbar->addCollector(new KarmaFwCollector);
 			$debugbar->addCollector(new SqlDbCollector);
-
-			//$debugbar->addCollector(new PhpTemplateCollector); // DO NOT WORK
+			$debugbar->addCollector(new SqlDbTimelineCollector);
 			$debugbar->addCollector(new KarmaMessagesCollector('templates'));
+			$debugbar->addCollector(new ConfigCollector([], 'templates_vars'));
+			$debugbar->addCollector(new SEOCollector);
 
 			$debugbarRenderer = $debugbar->getJavascriptRenderer('/assets/vendor/debugbar'); // symlink to ${APP_DIR}/vendor/maximebf/debugbar/src/DebugBar/Resources
 		}
@@ -39,15 +45,37 @@ class DebugBar
 		$response = $next($request, $response);
 
 		$is_html = (empty($response->getContentType()) || strpos($response->getContentType(), 'text/html') === 0);
-		$show_debugbar = ($load_debugbar && $is_html);
+		$show_debugbar = ($load_debugbar && $is_html && $response->getStatus() == 200);
 
 		if ($show_debugbar) {
-			$data = [
-				'app' => App::getData('app'),
-				'request' => $request,
-				'response' => $response,
-			];
-			$debugbar['KarmaFW']->setData($data);
+
+			// config
+			$constants = get_defined_constants(true);
+			$debugbar['config']->setData($constants['user']);
+
+
+			
+			// KarmaFW
+			if (isset($debugbar['KarmaFW'])) {
+				$ua_infos = UserAgent::analyseUserAgent( $request->getUserAgent() );
+
+				$data = [
+					'app' => App::getData('app'),
+					'request' => $request,
+					'response' => $response,
+					'user agent' => $ua_infos,
+					'client_ip' => $request->getClientIp(),
+				];
+				$debugbar['KarmaFW']->setData($data);
+			}
+
+
+			// SEO
+			if (isset($debugbar['SEO'])) {
+				$seo_data = $debugbar['SEO']->seoParseContent($request, $response);
+				$debugbar['SEO']->setData($seo_data);
+			}
+
 
 			$response->append( $debugbarRenderer->renderHead() );
 			// TODO: $response->injectAppendTo('head', $debugbarRenderer->renderHead())
@@ -66,4 +94,5 @@ class DebugBar
 		return $response;
 	}
 
+
 }

+ 86 - 0
src/App/Middlewares/DebugBar/SEOCollector.php

@@ -0,0 +1,86 @@
+<?php
+
+namespace KarmaFW\App\Middlewares\DebugBar;
+
+use \DebugBar\DataCollector\ConfigCollector;
+
+use \KarmaFW\App;
+use \KarmaFW\Http\Request;
+use \KarmaFW\Http\Response;
+
+
+class SEOCollector extends ConfigCollector
+{
+	protected $data = [
+		'title' => '',
+		'meta description' => '',
+		'h1' => '',
+	];
+
+
+	public function __construct(array $data = array(), $name = 'config')
+	{
+		$data += $this->data;
+		parent::__construct($data, $name);
+	}
+
+
+    public function getName()
+    {
+        return 'SEO';
+    }
+
+    public function collect()
+    {
+        return parent::collect();
+    }
+
+
+	public function seoParseContent(Request $request, Response $response)
+	{
+		$content = $response->getBody();
+
+		$url = $request->getFullUrl();
+
+		preg_match('~<title(.*?)>(.*?)</title>~is', $content, $matches);
+		$title = empty($matches) ? '' : $matches[2];
+
+		preg_match('~<meta +name="description" +content="(.*?)" *>~is', $content, $matches);
+		$meta_desc = empty($matches) ? '' : $matches[1];
+
+		preg_match('~<h1(.*?)>(.*?)</h1>~is', $content, $matches);
+		$h1 = empty($matches) ? '' : $matches[2];
+
+		preg_match_all('/<img /is', $content, $matches);
+		$nb_images = empty($matches) ? 0 : count($matches[0]);
+
+		preg_match_all('/<a /is', $content, $matches);
+		$nb_links = empty($matches) ? 0 : count($matches[0]);
+
+		preg_match_all('/<script ?/is', $content, $matches);
+		$nb_scripts = empty($matches) ? 0 : count($matches[0]);
+
+		preg_match_all('/<script (.*?)src="(.*?)>/is', $content, $matches);
+		$nb_scripts_external = empty($matches) ? 0 : count($matches[0]);
+
+		preg_match_all('/<link (.*?)rel="stylesheet"(.*?)>/is', $content, $matches);
+		$nb_stylesheets = empty($matches) ? 0 : count($matches[0]);
+
+		$data = [
+			'url' => $url,
+			'server ip' => $request->getServerIp(),
+			'title' => $title,
+			'meta description' => $meta_desc,
+			'h1' => $h1,
+			'nb images' => $nb_images,
+			'nb links' => $nb_links,
+			'nb stylesheets' => $nb_stylesheets,
+			'nb scripts' => $nb_scripts . " (" . ($nb_scripts-$nb_scripts_external) . " inline scripts + " . $nb_scripts_external . " external scripts)",
+			'content length' => formatSize(strlen($content)),
+		];
+
+		return $data;
+	}
+
+}
+

+ 37 - 0
src/App/Middlewares/DebugBar/SqlDbTimelineCollector.php

@@ -0,0 +1,37 @@
+<?php
+
+namespace KarmaFW\App\Middlewares\DebugBar;
+
+use \DebugBar\DataCollector\TimeDataCollector;
+use \DebugBar\DataCollector\Renderable;
+
+
+/* USE THIS WITH DEBUGBAR (see http://phpdebugbar.com/ or https://github.com/maximebf/php-debugbar ) */
+
+class SqlDbTimelineCollector extends TimeDataCollector implements Renderable
+{
+	
+    public function getName()
+    {
+        return 'sql_time';
+    }
+
+    public function getWidgets()
+    {
+        return array(
+            "sql_time" => array(
+                "icon" => "clock-o",
+                "tooltip" => "Request Duration",
+                "map" => "sql_time.duration_str",
+                "default" => "'0ms'"
+            ),
+            "SQL timeline" => array(
+                "icon" => "tasks",
+                "widget" => "PhpDebugBar.Widgets.TimelineWidget",
+                "map" => "sql_time",
+                "default" => "{}"
+            )
+        );
+    }
+}
+

+ 16 - 1
src/App/Middlewares/TrafficLogger.php

@@ -2,6 +2,7 @@
 
 namespace KarmaFW\App\Middlewares;
 
+use \KarmaFW\App;
 use \KarmaFW\Http\Request;
 use \KarmaFW\Http\Response;
 
@@ -11,8 +12,22 @@ class TrafficLogger
 	
 	public function __invoke(Request $request, Response $response, callable $next)
 	{
+        if (! isset($request->SERVER['REQUEST_TIME_FLOAT'])) {
+            $request->SERVER['REQUEST_TIME_FLOAT'] = microtime(true);
+        }
 
-		return $next($request, $response);
+		$response = $next($request, $response);
+
+        $ts_end = microtime(true);
+        $duration = $ts_end - $request->SERVER['REQUEST_TIME_FLOAT'];
+
+		$traffic_logger = App::getData('app')->get('traffic_logger');
+
+		if ($traffic_logger) {
+			$traffic_logger($request, $response, $duration);
+		}
+
+		return $response;
 	}
 
 }

+ 7 - 6
src/App/Middlewares/UrlRouter.php

@@ -5,7 +5,7 @@ namespace KarmaFW\App\Middlewares;
 use \KarmaFW\App;
 use \KarmaFW\Http\Request;
 use \KarmaFW\Http\Response;
-use \KarmaFW\Routing\Router;
+//use \KarmaFW\Routing\Router;
 
 
 class UrlRouter
@@ -31,14 +31,15 @@ class UrlRouter
 
 
 		try {
-			$router = new Router;
-
 			ob_start();
 			
-			$route_response = Router::routeRequest($request, $response);
+			//$route_response = Router::routeRequest($request, $response);
+			$app = App::getData('app');
+			$router = $app->get('router');
+			$route_response = $router($request, $response);
 
-			// en principe le contenu de la reponse est dans $response->content
-			// mais si il y a eu des "echo", ils sont capturés par le ob_start puis insérés au début de $response->content
+			// en principe le contenu de la reponse est dans $response->body
+			// mais si il y a eu des "echo", ils sont capturés par le ob_start puis insérés au début de $response->body
 
 			$content = ob_get_contents();
 			ob_end_clean();

+ 2 - 1
src/App/Pipe.php

@@ -3,6 +3,7 @@
 namespace KarmaFW\App;
 
 use \KarmaFW\App;
+use \KarmaFW\App\Tools;
 use \KarmaFW\Http\Request;
 use \KarmaFW\Http\Response;
 
@@ -40,7 +41,7 @@ class Pipe
         $debugbar = App::getData('debugbar');
         if ($debugbar) {
             if (isset($debugbar['time'])) {
-                $debugbar['time']->startMeasure($service_name, $service_name);
+                $debugbar['time']->startMeasure($service_name, [], Tools::getCaller([__FILE__]));
             }
         }
 

+ 129 - 0
src/App/Tools.php

@@ -21,4 +21,133 @@ class Tools
 		return (php_sapi_name() == 'cli');
 	}
 
+
+    public static function getCaller($excludeFiles = [], $formatted = true, $traceOffset = 2)
+    {
+        $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 10);
+        $backtrace = array_slice($backtrace, $traceOffset);
+
+        $excludeFiles[] = VENDOR_DIR . '/karmasolutions/karmafw/src/Database/Sql/SqlTable.php';
+        $excludeFiles[] = VENDOR_DIR . '/karmasolutions/karmafw/src/Database/Sql/SqlTableModel.php';
+        //$excludeFiles[] = VENDOR_DIR . '/karmasolutions/karmafw/src/Database/Sql/SqlQuery.php';
+        //$excludeFiles[] = VENDOR_DIR . '/karmasolutions/karmafw/src/App/Middlewares/DebugBar.php';
+        //$excludeFiles[] = VENDOR_DIR . '/karmasolutions/karmafw/src/Routing/Router.php';
+        //$excludeFiles[] = VENDOR_DIR . '/karmasolutions/karmafw/src/App/Pipe.php';
+
+        foreach ($backtrace as $index => $context) {
+            if (isset($context['file']) && ! in_array($context['file'], $excludeFiles)) {
+                break;
+            }
+        }
+
+        if (!isset($context)) {
+            return null;
+        }
+
+        if ($formatted) {
+            return isset($context) && array_key_exists('file', $context) ? $context['file'] . ':' . $context['line'] : null;
+        }
+
+        return $context;
+    }
+
+
+
+    public static function removeAccents()
+    {
+        $foreign_characters = array(
+            '/ä|æ|ǽ/' => 'ae',
+            '/ö|œ/' => 'oe',
+            '/ü/' => 'ue',
+            '/Ä/' => 'Ae',
+            '/Ü/' => 'Ue',
+            '/Ö/' => 'Oe',
+            '/À|Á|Â|Ã|Ä|Å|Ǻ|Ā|Ă|Ą|Ǎ|Α|Ά|Ả|Ạ|Ầ|Ẫ|Ẩ|Ậ|Ằ|Ắ|Ẵ|Ẳ|Ặ|А/' => 'A',
+            '/à|á|â|ã|å|ǻ|ā|ă|ą|ǎ|ª|α|ά|ả|ạ|ầ|ấ|ẫ|ẩ|ậ|ằ|ắ|ẵ|ẳ|ặ|а/' => 'a',
+            '/Б/' => 'B',
+            '/б/' => 'b',
+            '/Ç|Ć|Ĉ|Ċ|Č/' => 'C',
+            '/ç|ć|ĉ|ċ|č/' => 'c',
+            '/Д/' => 'D',
+            '/д/' => 'd',
+            '/Ð|Ď|Đ|Δ/' => 'Dj',
+            '/ð|ď|đ|δ/' => 'dj',
+            '/È|É|Ê|Ë|Ē|Ĕ|Ė|Ę|Ě|Ε|Έ|Ẽ|Ẻ|Ẹ|Ề|Ế|Ễ|Ể|Ệ|Е|Э/' => 'E',
+            '/è|é|ê|ë|ē|ĕ|ė|ę|ě|έ|ε|ẽ|ẻ|ẹ|ề|ế|ễ|ể|ệ|е|э/' => 'e',
+            '/Ф/' => 'F',
+            '/ф/' => 'f',
+            '/Ĝ|Ğ|Ġ|Ģ|Γ|Г|Ґ/' => 'G',
+            '/ĝ|ğ|ġ|ģ|γ|г|ґ/' => 'g',
+            '/Ĥ|Ħ/' => 'H',
+            '/ĥ|ħ/' => 'h',
+            '/Ì|Í|Î|Ï|Ĩ|Ī|Ĭ|Ǐ|Į|İ|Η|Ή|Ί|Ι|Ϊ|Ỉ|Ị|И|Ы/' => 'I',
+            '/ì|í|î|ï|ĩ|ī|ĭ|ǐ|į|ı|η|ή|ί|ι|ϊ|ỉ|ị|и|ы|ї/' => 'i',
+            '/Ĵ/' => 'J',
+            '/ĵ/' => 'j',
+            '/Ķ|Κ|К/' => 'K',
+            '/ķ|κ|к/' => 'k',
+            '/Ĺ|Ļ|Ľ|Ŀ|Ł|Λ|Л/' => 'L',
+            '/ĺ|ļ|ľ|ŀ|ł|λ|л/' => 'l',
+            '/М/' => 'M',
+            '/м/' => 'm',
+            '/Ñ|Ń|Ņ|Ň|Ν|Н/' => 'N',
+            '/ñ|ń|ņ|ň|ʼn|ν|н/' => 'n',
+            '/Ò|Ó|Ô|Õ|Ō|Ŏ|Ǒ|Ő|Ơ|Ø|Ǿ|Ο|Ό|Ω|Ώ|Ỏ|Ọ|Ồ|Ố|Ỗ|Ổ|Ộ|Ờ|Ớ|Ỡ|Ở|Ợ|О/' => 'O',
+            '/ò|ó|ô|õ|ō|ŏ|ǒ|ő|ơ|ø|ǿ|º|ο|ό|ω|ώ|ỏ|ọ|ồ|ố|ỗ|ổ|ộ|ờ|ớ|ỡ|ở|ợ|о/' => 'o',
+            '/П/' => 'P',
+            '/п/' => 'p',
+            '/Ŕ|Ŗ|Ř|Ρ|Р/' => 'R',
+            '/ŕ|ŗ|ř|ρ|р/' => 'r',
+            '/Ś|Ŝ|Ş|Ș|Š|Σ|С/' => 'S',
+            '/ś|ŝ|ş|ș|š|ſ|σ|ς|с/' => 's',
+            '/Ț|Ţ|Ť|Ŧ|τ|Т/' => 'T',
+            '/ț|ţ|ť|ŧ|т/' => 't',
+            '/Þ|þ/' => 'th',
+            '/Ù|Ú|Û|Ũ|Ū|Ŭ|Ů|Ű|Ų|Ư|Ǔ|Ǖ|Ǘ|Ǚ|Ǜ|Ũ|Ủ|Ụ|Ừ|Ứ|Ữ|Ử|Ự|У/' => 'U',
+            '/ù|ú|û|ũ|ū|ŭ|ů|ű|ų|ư|ǔ|ǖ|ǘ|ǚ|ǜ|υ|ύ|ϋ|ủ|ụ|ừ|ứ|ữ|ử|ự|у/' => 'u',
+            '/Ý|Ÿ|Ŷ|Υ|Ύ|Ϋ|Ỳ|Ỹ|Ỷ|Ỵ|Й/' => 'Y',
+            '/ý|ÿ|ŷ|ỳ|ỹ|ỷ|ỵ|й/' => 'y',
+            '/В/' => 'V',
+            '/в/' => 'v',
+            '/Ŵ/' => 'W',
+            '/ŵ/' => 'w',
+            '/Ź|Ż|Ž|Ζ|З/' => 'Z',
+            '/ź|ż|ž|ζ|з/' => 'z',
+            '/Æ|Ǽ/' => 'AE',
+            '/ß/' => 'ss',
+            '/IJ/' => 'IJ',
+            '/ij/' => 'ij',
+            '/Œ/' => 'OE',
+            '/ƒ/' => 'f',
+            '/ξ/' => 'ks',
+            '/π/' => 'p',
+            '/β/' => 'v',
+            '/μ/' => 'm',
+            '/ψ/' => 'ps',
+            '/Ё/' => 'Yo',
+            '/ё/' => 'yo',
+            '/Є/' => 'Ye',
+            '/є/' => 'ye',
+            '/Ї/' => 'Yi',
+            '/Ж/' => 'Zh',
+            '/ж/' => 'zh',
+            '/Х/' => 'Kh',
+            '/х/' => 'kh',
+            '/Ц/' => 'Ts',
+            '/ц/' => 'ts',
+            '/Ч/' => 'Ch',
+            '/ч/' => 'ch',
+            '/Ш/' => 'Sh',
+            '/ш/' => 'sh',
+            '/Щ/' => 'Shch',
+            '/щ/' => 'shch',
+            '/Ъ|ъ|Ь|ь/' => '',
+            '/Ю/' => 'Yu',
+            '/ю/' => 'yu',
+            '/Я/' => 'Ya',
+            '/я/' => 'ya'
+        );
+
+    }
+
 }

+ 29 - 0
src/Commands/ControllerCommand.php

@@ -0,0 +1,29 @@
+<?php
+
+namespace KarmaFW\Commands;
+
+//use \KarmaFW\App;
+use \KarmaFW\Http\Request;
+use \KarmaFW\Http\Response;
+
+
+class ControllerCommand
+{
+	protected $request;
+	protected $response;
+
+
+	public function __construct(Request $request, Response $response) 
+	{
+		$this->request = $request;
+		$this->response = $response;
+	}
+
+
+	public function execute($arguments=[]) 
+	{
+
+		
+	}
+	
+}

+ 2 - 2
src/Commands/HelpCommand.php

@@ -3,8 +3,8 @@
 namespace KarmaFW\Commands;
 
 //use \KarmaFW\App;
-use \KarmaFW\App\Request;
-use \KarmaFW\App\Response;
+use \KarmaFW\Http\Request;
+use \KarmaFW\Http\Response;
 
 
 class HelpCommand

+ 35 - 0
src/Commands/MigrationCommand.php

@@ -0,0 +1,35 @@
+<?php
+
+namespace KarmaFW\Commands;
+
+//use \KarmaFW\App;
+use \KarmaFW\Http\Request;
+use \KarmaFW\Http\Response;
+
+
+class MigrationCommand
+{
+	protected $request;
+	protected $response;
+
+
+	public function __construct(Request $request, Response $response) 
+	{
+		$this->request = $request;
+		$this->response = $response;
+	}
+
+
+	public function execute($arguments=[]) 
+	{
+		echo "PHP Console script" . PHP_EOL;
+		echo PHP_EOL;
+		echo "Usage: php console.php migration [migration_name]" . PHP_EOL;
+		echo PHP_EOL;
+		echo "Example: php console.php migration add_column_age_into_table_users" . PHP_EOL;
+		echo PHP_EOL;
+
+		
+	}
+	
+}

+ 97 - 0
src/Commands/ModelCommand.php

@@ -0,0 +1,97 @@
+<?php
+
+namespace KarmaFW\Commands;
+
+use \KarmaFW\App;
+use \KarmaFW\Http\Request;
+use \KarmaFW\Http\Response;
+
+
+class ModelCommand
+{
+	protected $request;
+	protected $response;
+
+
+	public function __construct(Request $request, Response $response) 
+	{
+		$this->request = $request;
+		$this->response = $response;
+	}
+
+
+	public function execute($arguments=[]) 
+	{
+		if (empty($arguments)) {
+			echo "PHP Console script" . PHP_EOL;
+			echo PHP_EOL;
+			echo "Usage: php console.php model [model_name]" . PHP_EOL;
+			echo PHP_EOL;
+			echo "Example: php console.php model users" . PHP_EOL;
+			echo PHP_EOL;
+			return;
+		}
+
+		$table_name = $arguments[0];
+
+		$class_name = $table_name;
+		$class_name = strtolower($class_name);
+		$class_name = rtrim($class_name, 's');
+		$class_name = ucwords($class_name, "_ \t\r\n\f\v");
+		$class_name = str_replace('s_', '', $class_name);
+		$class_name = str_replace('_', '', $class_name);
+		//echo $class_name . PHP_EOL; exit;
+
+		$db = App::getDb();
+
+		$columns = $db->listTableColumns($table_name);
+		//print_r($columns);
+		$infos = [];
+		foreach ($columns as $column) {
+			$infos[] = '- ' . $column['Field'] . ' => ' . $column['Type'];
+		}
+
+		$indexes = $db->listTableIndexes($table_name);
+		$primary_key = [];
+		foreach ($indexes as $index) {
+			if ($index['Key_name'] == 'PRIMARY') {
+				$seq = $index['Seq_in_index'];
+				$primary_key[$seq] = $index['Column_name'];
+			}
+		}
+		ksort($primary_key);
+		$primary_key = array_values($primary_key);
+		//print_r($primary_key);
+
+		$tpl = '<' . '?php
+
+namespace App\Models;
+
+use \KarmaFW\Database\Sql\SqlTableModel;
+
+
+/*
+Fields:
+' . implode(PHP_EOL, $infos) . '
+*/
+
+
+class ' . $class_name . ' extends SqlTableModel
+{
+	public static $table_name = "' . $table_name . '";
+	public static $primary_key = ["' . implode('", "', $primary_key) . '"];
+
+}
+';
+
+		echo $tpl;
+
+		if (false) {
+			$model_filepath = APP_DIR . "/src/Models/" . $class_name . ".php";
+			if (! is_file($model_filepath)) {
+				file_put_contents($model_filepath, $tpl);
+			}
+		}
+	}
+	
+}

+ 2 - 2
src/Commands/TestCommand.php

@@ -3,8 +3,8 @@
 namespace KarmaFW\Commands;
 
 //use \KarmaFW\App;
-use \KarmaFW\App\Request;
-use \KarmaFW\App\Response;
+use \KarmaFW\Http\Request;
+use \KarmaFW\Http\Response;
 
 
 class TestCommand

+ 13 - 2
src/Database/Redis/Redis.php

@@ -9,9 +9,10 @@ class Redis
 {
 	protected $dsn = null;
 	protected $client = null;
+	protected $connection_name = null;
 	
 
-	public function __construct($redis_dsn=null)
+	public function __construct($redis_dsn=null, $connection_name='')
 	{
 		if (empty($redis_dsn) && defined('REDIS_DSN')) {
 			$redis_dsn = REDIS_DSN;
@@ -24,28 +25,38 @@ class Redis
 		if (class_exists('\\Predis\\Client')) {
 			$this->setClient(new Predis\Client($redis_dsn));
 			$this->dsn = $redis_dsn;
-		}
+			$this->connection_name = $connection_name;
+
+			if ($connection_name) {
+				$this->getClient()->client('SETNAME', $connection_name);
+			}
 
+		}
 	}
 
+
 	public function getClient()
 	{
 		return $this->client;
 	}
 
+
 	public function setClient($client)
 	{
 		$this->client = $client;
 	}
 
+
 	public function get($key)
 	{
 		return $this->client->get($key);
 	}
 
+
 	public function set($key, $value)
 	{
 		return $this->client->set($key, $value);
 	}
+	
 }
 

+ 111 - 0
src/Database/Redis/RedisQueue.php

@@ -0,0 +1,111 @@
+<?php
+
+namespace KarmaFW\Database\Redis;
+
+
+class RedisQueue
+{
+	protected $redis;
+	protected $queue_name;
+
+
+	function __construct(Redis $redis, $queue_name)
+	{
+		$this->redis = $redis;
+		$this->queue_name = $queue_name;
+	}
+
+
+	function getRedis()
+	{
+		return $this->redis;
+	}
+
+
+	function getClient()
+	{
+		return $this->redis->getClient();
+	}
+
+
+	function push($data)
+	{
+		$data = serialize($data);
+		$client = $this->redis->getClient();
+		$client->rPush($this->queue_name, $data);
+	}
+
+
+	function unshift($data)
+	{
+		$data = serialize($data);
+		$client = $this->redis->getClient();
+		$client->lPush($this->queue_name, $data);
+	}
+
+
+	function pop($timeout=0)
+	{
+		$ts_start = microtime(true);
+		$client = $this->redis->getClient();
+		$data = null;
+
+		while (! $data) {
+			$data_raw = $client->rPop($this->queue_name);
+			if ($data_raw) {
+				$data = unserialize($data_raw);
+				break;
+			}
+
+			if(connection_status() != CONNECTION_NORMAL || connection_aborted()) {
+				break;
+			}
+
+			$ts_end = microtime(true);
+			$duration = $ts_end - $ts_start;
+			$remaining_max = $timeout - $duration;
+
+			if ($remaining_max <= 0) {
+				break;
+			}
+
+			usleep( 1000 * 1000 * min($remaining_max, 0.05) ); // 0.05 second
+		}
+		
+		return $data;
+	}
+
+
+	function shift($timeout=0)
+	{
+		$ts_start = microtime(true);
+		$client = $this->redis->getClient();
+		$data = null;
+
+		while (! $data) {
+			$data_raw = $client->lPop($this->queue_name);
+
+			if ($data_raw) {
+				$data = unserialize($data_raw);
+				break;
+			}
+
+			if(connection_status() != CONNECTION_NORMAL || connection_aborted()) {
+				break;
+			}
+
+			$ts_end = microtime(true);
+			$duration = $ts_end - $ts_start;
+			$remaining_max = $timeout - $duration;
+
+			if ($remaining_max <= 0) {
+				break;
+			}
+
+			usleep( 1000 * 1000 * min($remaining_max, 0.05) ); // 0.05 second
+		}
+
+		return $data;
+	}
+
+}

+ 16 - 7
src/Database/Sql/SqlQuery.php

@@ -3,6 +3,7 @@
 namespace KarmaFW\Database\Sql;
 
 use \KarmaFW\App;
+use \KarmaFW\App\Tools;
 use \KarmaFW\Database\Sql\SqlResultSetError;
 
 
@@ -144,11 +145,23 @@ class SqlQuery
 		$this->recordset = $rs;
 		$this->db->setLastQuery($this);
 
+		/*
+		if (strpos($query, 'from utilisateurs')) {
+			$backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 10);
+			//pre(Tools::getCaller([__FILE__])); exit;
+			pre($backtrace); exit;
+		}
+		*/
 
-		// TODO: voir comment bien injecter cette dependance
+		// debugbar
 		$debugbar = App::getData('debugbar');
 		if ($debugbar) {
 			//$debugbar['sql']->addMessage( preg_replace('/\s+/', ' ', $query) );
+			$query_escaped = preg_replace('/\s+/', ' ', $query);
+
+            if (isset($debugbar['sql_time'])) {
+                $debugbar['sql_time']->startMeasure($query_escaped, [], Tools::getCaller([__FILE__]));
+            }
 			
 			if (isset($debugbar['sql_queries'])) {
 				$error_code = 0;
@@ -160,24 +173,20 @@ class SqlQuery
 					$error_msg = $rs->getErrorMessage();
 				}
 
-
 				$debugbar['sql_queries']->addQuery([
-					'sql' => preg_replace('/\s+/', ' ', $query),
+					'sql' => $query_escaped,
 					'duration' => $this->duration,
 					'duration_str' => formatDuration($this->duration),
 					'row_count' => $rs->getRowsCount(),
-					//'stmt_id' => null,
-					//'prepared_stmt' => null,
 					'params' => $params,
 					'memory' => $memory_used,
-					//'memory_str' => round($memory_used/1000000, 1) . "Mo",
 					'memory_str' => formatSize($memory_used),
 					'end_memory' => $mem_end,
-					//'end_memory_str' => round($mem_end/1000000, 1) . "Mo",
 					'end_memory_str' => formatSize($mem_end),
 					'is_success' => $is_success,
 					'error_code' => $error_code,
 					'error_message' => $error_msg,
+					//'label' => Tools::getCaller([__FILE__]),
 				]);
 			}
 			

+ 1 - 1
src/Database/Sql/SqlTable.php

@@ -314,7 +314,7 @@ class SqlTable
 			'offset' => $offset,
 			'page_rows' => count($data),
 			'total_rows' => $found_rows,
-			'nb_pages' => empty($nb_per_page) ? null : ceil($found_rows / $nb_per_page),
+			'nb_pages' => empty($nb_per_page) ? 1 : ceil($found_rows / $nb_per_page),
 		];
 
 		return [

+ 87 - 0
src/Http/Request.php

@@ -11,6 +11,7 @@ class Request
 	protected $method = null;
 	protected $url = null;
 	protected $protocol = null;
+	protected $attributes = [];
 
 	protected $route = null;
 	protected $client_ip = null;
@@ -32,6 +33,8 @@ class Request
 		$this->protocol = $version;
 		//$this->setHeaders($headers);
 
+		$this->setAttribute('env', ENV);
+
 		//print_r($_SERVER); exit;
 	}
 
@@ -89,6 +92,10 @@ class Request
 			$request->SERVER['SERVER_NAME'] = $request->SERVER['HTTP_X_FORWARDED_HOST'];
 		}
 
+		if (empty($request->SERVER['SERVER_ADDR'])) {
+			$request->SERVER['SERVER_ADDR'] = '127.0.0.1';
+		}
+
 		// Set Client User-Agent
 		$user_agent = isset($request->SERVER['HTTP_USER_AGENT']) ? $request->SERVER['HTTP_USER_AGENT'] : null;
 		$request->setUserAgent($user_agent);
@@ -107,6 +114,12 @@ class Request
 	}
 
 
+	public function getFullUrl()
+	{
+		$scheme = $this->isSecure() ? 'https://' : 'http:';
+		return $scheme . $this->SERVER['SERVER_NAME'] . $this->url;
+	}
+
 	public function getUrl()
 	{
 		return $this->url;
@@ -117,6 +130,11 @@ class Request
 		return $this->method;
 	}
 
+	public function getServerIp()
+	{
+		return $this->SERVER['SERVER_ADDR'];
+	}
+
 	public function getClientIp()
 	{
 		return $this->client_ip;
@@ -147,6 +165,42 @@ class Request
 		$this->route = $route;
 	}
 
+
+	public function isGet()
+	{
+		return ($this->method == 'GET');
+	}
+
+	public function isPost()
+	{
+		return ($this->method == 'POST');
+	}
+
+	public function isHead()
+	{
+		return ($this->method == 'HEAD');
+	}
+
+	public function isOptions()
+	{
+		return ($this->method == 'OPTIONS');
+	}
+
+	public function isPut()
+	{
+		return ($this->method == 'PUT');
+	}
+
+	public function isDelete()
+	{
+		return ($this->method == 'DELETE');
+	}
+
+	public function isPatch()
+	{
+		return ($this->method == 'PATCH');
+	}
+
 	public function isSecure()
 	{
 		return (! empty($this->SERVER['HTTPS']) && $this->SERVER['HTTPS'] == 'On')
@@ -160,6 +214,39 @@ class Request
 		return UserAgent::isBot($this->user_agent);
 	}
 
+	public function isMobile()
+	{
+		return UserAgent::isMobile($this->user_agent);
+	}
+
+	public function isAjax()
+	{
+		return (! empty($this->SERVER['HTTP_X_REQUESTED_WITH']) && $this->SERVER['HTTP_X_REQUESTED_WITH'] == 'XMLHttpRequest');
+	}
+
+
+
+	public function getAttributes()
+	{
+		return $this->attributes;
+	}
+
+	public function setAttributes($attributes)
+	{
+		$this->attributes = $attributes;
+	}
+
+	public function getAttribute($key, $default_value=null)
+	{
+		return isset($this->attributes[$key]) ? $this->attributes[$key] : $default_value;
+	}
+
+	public function setAttribute($key, $value)
+	{
+		$this->attributes[$key] = $value;
+	}
+
+
 	/*
 
 	public function setUrl($url)

+ 27 - 5
src/Http/Response.php

@@ -19,6 +19,7 @@ class Response
 	protected $redirect_url = null;
 	protected $download_file_name = null;
 	protected $download_file_path = null;
+	protected $attributes = [];
 
 
 	/* public */ const http_status_codes = [
@@ -166,7 +167,7 @@ class Response
 		return $this->setBody($body);
 	}
 
-	public function setHtml($body, $status=200, $content_type='text/html')
+	public function setHtml($body, $status=200, $content_type='text/html; charset=utf8')
 	{
 		if (! is_null($status)) {
 			$this->setStatus($status);
@@ -177,7 +178,7 @@ class Response
 		return $this->setBody($body);
 	}
 	
-	public function setJson($body, $status=200, $content_type='application/json')
+	public function setJson($body, $status=200, $content_type='application/json; charset=utf8')
 	{
 		if (! is_string($body)) {
 			$body = json_encode($body);
@@ -187,7 +188,7 @@ class Response
 				->setStatus($status);
 	}
 
-	public function setCsv($body, $download_file_name=null, $status=200, $content_type='text/csv')
+	public function setCsv($body, $download_file_name=null, $status=200, $content_type='text/csv; charset=utf8')
 	{
 		if (is_array($body)) {
 			// transform array to csv
@@ -261,7 +262,7 @@ class Response
 
 		if ($this->status === 200 && empty($this->body)) {
 			// No content
-			$this->setStatus(204);
+			//$this->setStatus(204);
 		}
 
 		if (! empty($this->status)) {
@@ -382,7 +383,7 @@ class Response
 	}
 
 
-	public function error404($body='', $content_type='text/html')
+	public function error404($body='', $content_type='text/html; charset=utf8')
 	{
 		$this->setStatus(404)
 			->setContentType($content_type)
@@ -391,4 +392,25 @@ class Response
 		return $this;
 	}
 
+
+	public function getAttributes()
+	{
+		return $this->attributes;
+	}
+
+	public function setAttributes($attributes)
+	{
+		$this->attributes = $attributes;
+	}
+
+	public function getAttribute($key, $default_value=null)
+	{
+		return isset($this->attributes[$key]) ? $this->attributes[$key] : $default_value;
+	}
+
+	public function setAttribute($key, $value)
+	{
+		$this->attributes[$key] = $value;
+	}
+
 }

+ 259 - 1
src/Http/UserAgent.php

@@ -6,6 +6,211 @@ namespace KarmaFW\Http;
 class UserAgent
 {
 
+	protected static $platforms = array(
+		'windows nt 10.0'	=> 'Windows 10',
+		'windows nt 6.3'	=> 'Windows 8.1',
+		'windows nt 6.2'	=> 'Windows 8',
+		'windows nt 6.1'	=> 'Windows 7',
+		'windows nt 6.0'	=> 'Windows Vista',
+		'windows nt 5.2'	=> 'Windows 2003',
+		'windows nt 5.1'	=> 'Windows XP',
+		'windows nt 5.0'	=> 'Windows 2000',
+		'windows nt 4.0'	=> 'Windows NT 4.0',
+		'winnt4.0'			=> 'Windows NT 4.0',
+		'winnt 4.0'			=> 'Windows NT',
+		'winnt'				=> 'Windows NT',
+		'windows 98'		=> 'Windows 98',
+		'win98'				=> 'Windows 98',
+		'windows 95'		=> 'Windows 95',
+		'win95'				=> 'Windows 95',
+		'windows phone'		=> 'Windows Phone',
+		'windows'			=> 'Unknown Windows OS',
+		'android'			=> 'Android',
+		'blackberry'		=> 'BlackBerry',
+		'iphone'			=> 'iOS',
+		'ipad'				=> 'iOS',
+		'ipod'				=> 'iOS',
+		'os x'				=> 'Mac OS X',
+		'ppc mac'			=> 'Power PC Mac',
+		'freebsd'			=> 'FreeBSD',
+		'ppc'				=> 'Macintosh',
+		'linux'				=> 'Linux',
+		'debian'			=> 'Debian',
+		'sunos'				=> 'Sun Solaris',
+		'beos'				=> 'BeOS',
+		'apachebench'		=> 'ApacheBench',
+		'aix'				=> 'AIX',
+		'irix'				=> 'Irix',
+		'osf'				=> 'DEC OSF',
+		'hp-ux'				=> 'HP-UX',
+		'netbsd'			=> 'NetBSD',
+		'bsdi'				=> 'BSDi',
+		'openbsd'			=> 'OpenBSD',
+		'gnu'				=> 'GNU/Linux',
+		'unix'				=> 'Unknown Unix OS',
+		'symbian' 			=> 'Symbian OS'
+	);
+
+
+	// The order of this array should NOT be changed. Many browsers return
+	// multiple browser types so we want to identify the sub-type first.
+	protected static $browsers = array(
+		'OPR'			=> 'Opera',
+		'Flock'			=> 'Flock',
+		'Edge'			=> 'Spartan',
+		'Chrome'		=> 'Chrome',
+		// Opera 10+ always reports Opera/9.80 and appends Version/<real version> to the user agent string
+		'Opera.*?Version'	=> 'Opera',
+		'Opera'			=> 'Opera',
+		'MSIE'			=> 'Internet Explorer',
+		'Internet Explorer'	=> 'Internet Explorer',
+		'Trident.* rv'	=> 'Internet Explorer',
+		'Shiira'		=> 'Shiira',
+		'Firefox'		=> 'Firefox',
+		'Chimera'		=> 'Chimera',
+		'Phoenix'		=> 'Phoenix',
+		'Firebird'		=> 'Firebird',
+		'Camino'		=> 'Camino',
+		'Netscape'		=> 'Netscape',
+		'OmniWeb'		=> 'OmniWeb',
+		'Safari'		=> 'Safari',
+		'Mozilla'		=> 'Mozilla',
+		'Konqueror'		=> 'Konqueror',
+		'icab'			=> 'iCab',
+		'Lynx'			=> 'Lynx',
+		'Links'			=> 'Links',
+		'hotjava'		=> 'HotJava',
+		'amaya'			=> 'Amaya',
+		'IBrowse'		=> 'IBrowse',
+		'Maxthon'		=> 'Maxthon',
+		'Ubuntu'		=> 'Ubuntu Web Browser'
+	);
+
+	protected static $mobiles = array(
+		// legacy array, old values commented out
+		'mobileexplorer'	=> 'Mobile Explorer',
+	//  'openwave'			=> 'Open Wave',
+	//	'opera mini'		=> 'Opera Mini',
+	//	'operamini'			=> 'Opera Mini',
+	//	'elaine'			=> 'Palm',
+		'palmsource'		=> 'Palm',
+	//	'digital paths'		=> 'Palm',
+	//	'avantgo'			=> 'Avantgo',
+	//	'xiino'				=> 'Xiino',
+		'palmscape'			=> 'Palmscape',
+	//	'nokia'				=> 'Nokia',
+	//	'ericsson'			=> 'Ericsson',
+	//	'blackberry'		=> 'BlackBerry',
+	//	'motorola'			=> 'Motorola'
+
+		// Phones and Manufacturers
+		'motorola'		=> 'Motorola',
+		'nokia'			=> 'Nokia',
+		'palm'			=> 'Palm',
+		'iphone'		=> 'Apple iPhone',
+		'ipad'			=> 'iPad',
+		'ipod'			=> 'Apple iPod Touch',
+		'sony'			=> 'Sony Ericsson',
+		'ericsson'		=> 'Sony Ericsson',
+		'blackberry'	=> 'BlackBerry',
+		'cocoon'		=> 'O2 Cocoon',
+		'blazer'		=> 'Treo',
+		'lg'			=> 'LG',
+		'amoi'			=> 'Amoi',
+		'xda'			=> 'XDA',
+		'mda'			=> 'MDA',
+		'vario'			=> 'Vario',
+		'htc'			=> 'HTC',
+		'samsung'		=> 'Samsung',
+		'sharp'			=> 'Sharp',
+		'sie-'			=> 'Siemens',
+		'alcatel'		=> 'Alcatel',
+		'benq'			=> 'BenQ',
+		'ipaq'			=> 'HP iPaq',
+		'mot-'			=> 'Motorola',
+		'playstation portable'	=> 'PlayStation Portable',
+		'playstation 3'		=> 'PlayStation 3',
+		'playstation vita'  	=> 'PlayStation Vita',
+		'hiptop'		=> 'Danger Hiptop',
+		'nec-'			=> 'NEC',
+		'panasonic'		=> 'Panasonic',
+		'philips'		=> 'Philips',
+		'sagem'			=> 'Sagem',
+		'sanyo'			=> 'Sanyo',
+		'spv'			=> 'SPV',
+		'zte'			=> 'ZTE',
+		'sendo'			=> 'Sendo',
+		'nintendo dsi'	=> 'Nintendo DSi',
+		'nintendo ds'	=> 'Nintendo DS',
+		'nintendo 3ds'	=> 'Nintendo 3DS',
+		'wii'			=> 'Nintendo Wii',
+		'open web'		=> 'Open Web',
+		'openweb'		=> 'OpenWeb',
+
+		// Operating Systems
+		'android'		=> 'Android',
+		'symbian'		=> 'Symbian',
+		'SymbianOS'		=> 'SymbianOS',
+		'elaine'		=> 'Palm',
+		'series60'		=> 'Symbian S60',
+		'windows ce'	=> 'Windows CE',
+
+		// Browsers
+		'obigo'			=> 'Obigo',
+		'netfront'		=> 'Netfront Browser',
+		'openwave'		=> 'Openwave Browser',
+		'mobilexplorer'	=> 'Mobile Explorer',
+		'operamini'		=> 'Opera Mini',
+		'opera mini'	=> 'Opera Mini',
+		'opera mobi'	=> 'Opera Mobile',
+		'fennec'		=> 'Firefox Mobile',
+
+		// Other
+		'digital paths'	=> 'Digital Paths',
+		'avantgo'		=> 'AvantGo',
+		'xiino'			=> 'Xiino',
+		'novarra'		=> 'Novarra Transcoder',
+		'vodafone'		=> 'Vodafone',
+		'docomo'		=> 'NTT DoCoMo',
+		'o2'			=> 'O2',
+
+		// Fallback
+		'mobile'		=> 'Generic Mobile',
+		'wireless'		=> 'Generic Mobile',
+		'j2me'			=> 'Generic Mobile',
+		'midp'			=> 'Generic Mobile',
+		'cldc'			=> 'Generic Mobile',
+		'up.link'		=> 'Generic Mobile',
+		'up.browser'	=> 'Generic Mobile',
+		'smartphone'	=> 'Generic Mobile',
+		'cellphone'		=> 'Generic Mobile'
+	);
+
+	// There are hundreds of bots but these are the most common.
+	protected static $robots = array(
+		'googlebot'		=> 'Googlebot',
+		'msnbot'		=> 'MSNBot',
+		'baiduspider'		=> 'Baiduspider',
+		'bingbot'		=> 'Bing',
+		'slurp'			=> 'Inktomi Slurp',
+		'yahoo'			=> 'Yahoo',
+		'ask jeeves'		=> 'Ask Jeeves',
+		'fastcrawler'		=> 'FastCrawler',
+		'infoseek'		=> 'InfoSeek Robot 1.0',
+		'lycos'			=> 'Lycos',
+		'yandex'		=> 'YandexBot',
+		'mediapartners-google'	=> 'MediaPartners Google',
+		'CRAZYWEBCRAWLER'	=> 'Crazy Webcrawler',
+		'adsbot-google'		=> 'AdsBot Google',
+		'feedfetcher-google'	=> 'Feedfetcher Google',
+		'curious george'	=> 'Curious George',
+		'ia_archiver'		=> 'Alexa Crawler',
+		'MJ12bot'		=> 'Majestic-12',
+		'Uptimebot'		=> 'Uptimebot'
+	);
+
+
+
 	protected static $bots_users_agents = [
 		"Googlebot/2.1 (+http://www.google.com/bot.html)",
 		"Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)",
@@ -932,10 +1137,63 @@ class UserAgent
 		"Mozilla/5.0 (compatible; +http://tweetedtimes.com)",
 	];
 
+
 	public static function isBot($user_agent) 
 	{
-		return in_array($user_agent, self::$bots_users_agents);
+		$ua_infos = self::analyseUserAgent($user_agent);
+		return ! empty($ua_infos['robot']) || in_array($user_agent, self::$bots_users_agents);
 	}
 
 
+	public static function isMobile($user_agent) 
+	{
+		$ua_infos = self::analyseUserAgent($user_agent);
+		return ! empty($ua_infos['mobile']);
+	}
+
+
+	public static function analyseUserAgent($user_agent) 
+	{
+		$ua_platform = null;
+		$ua_browser = null;
+		$ua_mobile = null;
+		$ua_robot = null;
+
+		foreach (self::$platforms as $k => $v) {
+			if (preg_match('/' . $k . '/i', $user_agent)) {
+				$ua_platform = $v;
+				break;
+			}
+		}
+
+		foreach (self::$browsers as $k => $v) {
+			if (preg_match('/' . $k . '/i', $user_agent)) {
+				$ua_browser = $v;
+				break;
+			}
+		}
+
+		foreach (self::$mobiles as $k => $v) {
+			if (preg_match('/' . $k . '/i', $user_agent)) {
+				$ua_mobile = $v;
+				break;
+			}
+		}
+
+		foreach (self::$robots as $k => $v) {
+			if (preg_match('/' . $k . '/i', $user_agent)) {
+				$ua_robot = $v;
+				break;
+			}
+		}
+
+		return [
+			'platform' => $ua_platform,
+			'browser' => $ua_browser,
+			'mobile' => $ua_mobile,
+			'robot' => $ua_robot,
+			'user_agent' => $user_agent,
+		];
+	}
+
 }

+ 59 - 10
src/Kernel.php

@@ -10,10 +10,11 @@ use \KarmaFW\Database\Sql\SqlDb;
 use \KarmaFW\Database\Redis\Redis;
 use \KarmaFW\Http\Request;
 use \KarmaFW\Http\Response;
+use \KarmaFW\Routing\Router;
 
 
 define('FW_SRC_DIR', __DIR__);
-define('FW_DIR', __DIR__ . "/..");
+define('FW_DIR', realpath(__DIR__ . '/..'));
 
 if (! defined('APP_DIR')) {
 	echo "ERROR: Please, define APP_DIR" . PHP_EOL;
@@ -29,8 +30,8 @@ class Kernel
 		FW_SRC_DIR . "/helpers",
 	];
 
-	protected $db = null;
-	protected $redis = null;
+	protected $db = null; // TODO: a deplacer dans $services['db']
+	protected $redis = null; // TODO: a deplacer dans $services['redis']
 
 	protected $middlewares;
 	protected $container;
@@ -45,6 +46,7 @@ class Kernel
 
 		try {
 			$this->configure();
+			$this->init();
 
 		} catch (\Exception $e) {
 			header("HTTP/1.0 500 Internal Server Error");
@@ -88,21 +90,29 @@ class Kernel
 		if (! defined('ERROR_TEMPLATE')) {
 			//define('ERROR_TEMPLATE', "page_error.tpl.php");
 		}
+	}
+
+
+	public function init()
+	{
+		// Load helpers
+		Tools::loadHelpers(APP_DIR . '/src/helpers');
+		Tools::loadHelpers(FW_DIR . '/src/helpers');
+
+
+		// Load services
+		$this->loadServices();
 
 
 		if (defined('DB_DSN')) {
 			//$this->db = static::getDb('default', DB_DSN);
-			$this->db = $this->connectDb('default', DB_DSN);
+			//$this->db = $this->connectDb('default', DB_DSN); // TODO: a deplacer dans $services['db'] ( ou $services['sql'] ? )
 		}
 
 		if (defined('REDIS_DSN')) {
-			$this->redis = new Redis(REDIS_DSN);
+			//$this->redis = new Redis(REDIS_DSN); // TODO: a deplacer dans $services['redis']
 		}
-
-
-		// Load helpers
-		Tools::loadHelpers(APP_DIR . '/src/helpers');
-		Tools::loadHelpers(FW_DIR . '/src/helpers');
+		
 	}
 
 
@@ -167,6 +177,45 @@ class Kernel
 	}
 
     
+    /*
+	public function setService($service_name, $callback)
+	{
+		return $this->set($service_name, $callback);
+	}
+	*/
+
+	public function loadServices()
+	{
+		// TODO: rendre parametrable la liste des services
+
+		$this->set('router', function (Request $request, Response $response) {
+			return Router::routeRequest($request, $response);
+		});
+
+		$this->set('db', function ($dsn=null) {
+			if (empty($dsn) && defined('DB_DSN')) {
+				$dsn = DB_DSN;
+			}
+			return new \KarmaFW\Database\Sql\SqlDb($dsn);
+		});
+
+		$this->set('redis', function ($dsn=null) {
+			if (empty($dsn) && defined('REDIS_DSN')) {
+				$dsn = REDIS_DSN;
+			}
+			return new \KarmaFW\Database\Redis\Redis($dsn);
+		});
+
+		$this->set('template', function ($tpl=null, $data=[]) {
+			//return new \KarmaFW\Templates\PhpTemplate($tpl, $data);
+			return new \KarmaFW\Templates\LightweightTemplate($tpl, $data);
+		});
+
+		$this->set('traffic_logger', function (Request $request, Response $response) {
+			return null; // TODO
+		});
+	}
+
 
     /* CONTAINER */
 

+ 3 - 2
src/Routing/Router.php

@@ -3,6 +3,7 @@
 namespace KarmaFW\Routing;
 
 use \KarmaFW\App;
+use \KarmaFW\App\Tools;
 use \KarmaFW\WebApp;
 use \KarmaFW\App\Pipe;
 use \KarmaFW\Http\Request;
@@ -212,7 +213,7 @@ class Router
         $debugbar = App::getData('debugbar');
         if ($debugbar) {
             if (isset($debugbar['time'])) {
-                $debugbar['time']->startMeasure($service_name, $service_name);
+                $debugbar['time']->startMeasure($service_name, [], Tools::getCaller([__FILE__]));
             }
         }
 
@@ -332,7 +333,7 @@ class Router
         $debugbar = App::getData('debugbar');
         if ($debugbar) {
             if (isset($debugbar['time'])) {
-                $debugbar['time']->startMeasure($service_name, $service_name);
+                $debugbar['time']->startMeasure($service_name, [], Tools::getCaller([__FILE__]));
             }
         }
 

+ 327 - 0
src/Templates/LightweightTemplate.php

@@ -0,0 +1,327 @@
+<?php
+
+namespace KarmaFW\Templates;
+
+use \KarmaFW\App;
+
+
+class LightweightTemplate {
+	// https://codeshack.io/lightweight-template-engine-php/
+
+	static $blocks = array();
+	static $cache_path = TPL_CACHE_DIR; // APP_DIR . '/var/cache/templates';
+	static $tpl_path = TPL_DIR;
+	static $cache_enabled = (ENV == 'prod') || true;
+	static $tpl_last_updated = null;
+
+
+	protected $data = [];
+
+	public function __construct($tpl_path=null, $variables=[], $layout=null) 
+	{
+		$this->data = $variables;
+	}
+
+	public function assign($k, $v=null) 
+	{
+		if (is_array($k)) {
+			$keys = $k;
+			foreach ($keys as $k => $v) {
+				$this->assign($k, $v);
+			}
+
+		} else {
+			$this->data[$k] = $v;
+		}
+	}
+	
+	public function getVariables() 
+	{
+		return $this->data;
+	}
+	
+	public function getVar($var_name, $default_value=null) 
+	{
+		return isset($this->data[$var_name]) ? $this->data[$var_name] : $default_value;
+	}
+
+	public function fetch($tpl=null, $extra_vars=[], $layout=null, $options=[]) 
+	{
+		ob_start();
+		$this->display($tpl, $extra_vars, $layout, $options);
+		$content = ob_get_contents();
+		ob_end_clean();
+		return $content;
+	}
+
+	public function display($tpl=null, $extra_vars=[], $layout=null, $options=[]) 
+	{
+		$tpl_data = $this->data + $extra_vars;
+		self::view($tpl, $tpl_data);
+		return true;
+	}
+
+	
+	public static function view($file, $tpl_data = array()) {
+		$cached_file = self::cache($file);
+	    extract($tpl_data, EXTR_SKIP);
+
+		$debugbar = App::getData('debugbar');
+		if ($debugbar) {
+			if (isset($debugbar['templates_vars'])) {
+				$debugbar['templates_vars']->setData($tpl_data);
+			}
+		}
+		unset($debugbar);
+
+	   	require $cached_file;
+	}
+
+
+	protected static function cache($file) {
+		if (!file_exists(self::$cache_path)) {
+		  	if (! @mkdir(self::$cache_path, 0744)) {
+		  		throw new \Exception("Cannot create templates cache dir " . self::$cache_path, 1);
+		  	}
+		}
+
+	    $cached_file = self::$cache_path . '/' . str_replace(array('/', '.html'), array('_', ''), $file . '.php');
+	    $cached_file_exists = is_file($cached_file);
+
+	    if ($cached_file_exists) {
+		    $cached_file_updated = filemtime($cached_file);
+		    $file_path = strpos($file, '/') === 0 ? $file : (self::$tpl_path . '/' . $file);
+		    self::$tpl_last_updated = filemtime($file_path);
+	    } else {
+	    	$cached_file_updated = null;
+	    }
+
+	    if (ENV == 'dev') {
+	    	// on force le parcours de tous les fichiers inclus pour avoir la vraie valeur de self::$tpl_last_updated
+	    	$code = self::includeFiles($file);
+	    }
+
+	    if (!self::$cache_enabled || ! $cached_file_exists || $cached_file_updated < self::$tpl_last_updated) {
+	    	if (! isset($code)) {
+				$code = self::includeFiles($file);
+	    	}
+			$code = self::compileCode($code);
+	        file_put_contents($cached_file, '<?php class_exists(\'' . __CLASS__ . '\') or exit; ?>' . PHP_EOL . $code);
+
+	    } else {
+	    	//header('X-Template: cached'); // TODO: $response->addHeader(...)
+
+			$debugbar = App::getData('debugbar');
+			if ($debugbar) {
+				if (isset($debugbar['templates'])) {
+					$debugbar_message_idx = $debugbar['templates']->addMessage([
+						'tpl' => $cached_file,
+						'content_length' => filesize($cached_file),
+						'content_length_str' => formatSize(filesize($cached_file)),
+						'cached' => true,
+					]);
+				}
+			}
+
+	    }
+		return $cached_file;
+	}
+
+	public static function clearCache() {
+		foreach(glob(self::$cache_path . '/*') as $file) {
+			unlink($file);
+		}
+	}
+
+	protected static function compileCode($code) {
+		$code = self::compileBlock($code);
+		$code = self::compileYield($code);
+
+		$code = self::compileModules($code);
+		$code = self::compileEscapedEchos($code);
+		$code = self::compileEchos($code);
+		//$code = self::compilePHP($code);
+		return $code;
+	}
+
+	protected static function includeFiles($file, $level=0, $caller_file=null, $parent_file=null) {
+		$file_path = strpos($file, '/') === 0 ? $file : (self::$tpl_path . '/' . $file);
+
+		if (! is_file($file_path)) {
+			throw new \Exception("Template file not found " . $file, 500);
+		}
+		
+		$code = file_get_contents($file_path);
+		$code_init = $code;
+		$layout = null;
+
+		$ts_update_file = filectime($file_path);
+		if (empty(self::$tpl_last_updated) || self::$tpl_last_updated < $ts_update_file) {
+			self::$tpl_last_updated = $ts_update_file;
+		}
+
+		static $tpl_idx = null;
+		if (is_null($tpl_idx) || (empty($caller_file) && empty($parent_file))) {
+			$tpl_idx = 0;
+		}
+		$tpl_idx++;
+
+		$debugbar = App::getData('debugbar');
+		if ($debugbar) {
+			if (isset($debugbar['templates'])) {
+				$debugbar_message_idx = $debugbar['templates']->addMessage([
+					'tpl' => $file,
+				]);
+			}
+		}
+
+		$ts_start = microtime(true);
+
+
+		// Layout (1)
+		preg_match_all('/{layout ?\'?(.*?)\'? ?}/i', $code, $layout_matches, PREG_SET_ORDER);
+		if ($layout_matches) {
+			$value = $layout_matches[0];
+			$layout = $value[1];
+		} else {
+			$layout = null;
+		}
+
+
+		if (defined('ENV') && ENV == 'dev') {
+			$tpl_infos = '';
+			if ($caller_file) {
+				$tpl_infos .= ' layout for ' . $caller_file . '';
+			}
+			if ($parent_file) {
+				$tpl_infos .= ' child of ' . $parent_file . '';
+			}
+			if ($layout) {
+				$tpl_infos .= ' with layout ' . $layout . '';
+			}
+
+			$begin = '<!-- [' . $level . '] BEGIN TEMPLATE #' . $tpl_idx . ' : ' . $file . ' (size: ' . formatSize(strlen($code)) . ' - ' . $tpl_infos . ') -->';
+			$end = '<!-- [' . $level . '] END TEMPLATE #' . $tpl_idx . ' : ' . $file . ' (size: ' . formatSize(strlen($code)) . ' - ' . $tpl_infos . ') -->';
+
+			$code = PHP_EOL . $begin . PHP_EOL . $code . PHP_EOL . $end . PHP_EOL;
+		}
+
+		// Layout (2)
+		if ($layout_matches) {
+			$value = $layout_matches[0];
+			$layout = $value[1];
+
+			$layout_code = self::includeFiles($layout, $level-1, $file);
+			$code = str_replace($value[0], '', $code);
+			
+			$layout_code = str_replace('<' . '?=$child_content?' . '>', '{@content}', $layout_code);
+			$layout_code = str_replace('{$child_content}', '{@content}', $layout_code);
+			$layout_code = str_replace('{@content}', $code, $layout_code);
+
+			$code = $layout_code;
+		}
+
+		// includes
+		preg_match_all('/{include ?\'?(.*?)\'? ?}/i', $code, $matches, PREG_SET_ORDER);
+		foreach ($matches as $value) {
+			$included_code = self::includeFiles($value[1], $level+1, null, $file);
+			$code = str_replace($value[0], $included_code, $code);
+		}
+
+
+		$ts_end = microtime(true);
+		$duration = $ts_end - $ts_start;
+
+		if (isset($debugbar_message_idx) && ! is_null($debugbar_message_idx)) {
+			$debugbar['templates']->updateMessage($debugbar_message_idx, [
+				'tpl' => $file,
+				'layout' => $layout,
+				'source_length' => strlen($code_init),
+				'source_length_str' => formatSize(strlen($code_init)),
+				'content_length' => strlen($code),
+				'content_length_str' => formatSize(strlen($code)),
+				'duration' => round($duration, 6),
+				'duration_str' => formatDuration($duration),
+				'level' => $level,
+			]);
+		}
+
+		return $code;
+	}
+
+	protected static function compilePHP($code) {
+		/* return preg_replace('~\{%\s*(.+?)\s*\%}~is', '<?php $1 ?>', $code); */
+		return $code;
+	}
+
+	protected static function compileModules($code) {
+
+		// url => {url clients_list}
+		$code = preg_replace('/{routeUrl /', '{url ', $code); // for compatibility with old templates
+		preg_match_all('~{url (.*?)}~is', $code, $matches, PREG_SET_ORDER);
+		foreach ($matches as $value) {
+			//pre($matches); exit;
+			$code = str_replace($value[0], getRouteUrl($value[1]), $code);
+		}
+
+		// foreach => {foreach $list as $item}<div>...</div>{/foreach}
+		preg_match_all('~{foreach (.*?) ?}(.*?){/foreach}~is', $code, $matches, PREG_SET_ORDER);
+		foreach ($matches as $value) {
+			$replaced = PHP_EOL . '<' . '?php foreach (' . $value[1] . ') : ?' . '>' . PHP_EOL . $value[2] . PHP_EOL . '<' . '?php endforeach; ?' . '>';
+			$code = str_replace($value[0], $replaced, $code);
+		}
+
+		// if => {if $item}<div>...</div>{/if}
+		preg_match_all('~{if (.*?) ?}(.*?){/if}~is', $code, $matches, PREG_SET_ORDER);
+		foreach ($matches as $value) {
+			
+			$replaced = '<' . '?php elseif ( $1 ) : ?' . '>';
+			$value[2] = preg_replace('/{elseif (.*?) ?}/', $replaced, $value[2]);
+
+			$replaced = PHP_EOL . '<' . '?php if (' . $value[1] . ') : ?' . '>' . PHP_EOL . $value[2] . PHP_EOL . '<' . '?php endif; ?' . '>';
+			$code = str_replace($value[0], $replaced, $code);
+		}
+
+		return $code;
+	}
+
+	protected static function compileEchos($code, $strict=true) {
+		// compile PHP variables (method 1) => {$my_var}
+		if ($strict) {
+			$code = preg_replace('~\{\$(.+?)}~is', '<?php echo \$$1 ?>', $code);
+		} else {
+			$code = preg_replace('~\{\$(.+?)}~is', '<?php echo isset(\$$1) ? (\$$1) : ""; ?>', $code);
+		}
+		// compile PHP variables (method 2) => {{ $my_var }}
+		return preg_replace('~\{{\s*(.+?)\s*\}}~is', '<?php echo $1 ?>', $code);
+	}
+
+	protected static function compileEscapedEchos($code) {
+		// compile PHP escaped variables => {{{ $my_var }}}
+		return preg_replace('~\{{{\s*(.+?)\s*\}}}~is', '<?php echo htmlentities($1, ENT_QUOTES, \'UTF-8\') ?>', $code);
+	}
+
+	protected static function compileBlock($code) {
+		preg_match_all('~{block ?(.*?) ?}(.*?){/block}~is', $code, $matches, PREG_SET_ORDER);
+		foreach ($matches as $value) {
+			if (!array_key_exists($value[1], self::$blocks)) self::$blocks[$value[1]] = '';
+			if (strpos($value[2], '@parent') === false) {
+				self::$blocks[$value[1]] = $value[2];
+			} else {
+				self::$blocks[$value[1]] = str_replace('@parent', self::$blocks[$value[1]], $value[2]);
+			}
+			$code = str_replace($value[0], '', $code);
+		}
+		return $code;
+	}
+
+	protected static function compileYield($code) {
+		// compile yields => {yield my_block}
+		foreach(self::$blocks as $block => $value) {
+			$code = preg_replace('/{yield ' . $block . ' ?}/', $value, $code);
+		}
+		$code = preg_replace('/{yield ?(.*?) ?}/i', '', $code);
+		return $code;
+	}
+
+}

+ 4 - 0
src/Templates/PhpTemplater.php

@@ -25,6 +25,10 @@ class PhpTemplater
 		$this->variables = $variables;
 	}
 
+	public function getVariables()	{
+		return $this->variables;
+	}
+
 	public function setAllVariables($var_name, $var_value)
 	{
 		$this->variables[$var_name] = $var_value;

+ 4 - 1
src/WebApp.php

@@ -53,7 +53,10 @@ class WebApp extends App
 
 	public static function createTemplate($tpl_path=null, $variables=[], $layout=null, $templates_dirs=null)
 	{
-		return new PhpTemplate($tpl_path, $variables, $layout, $templates_dirs);
+		$templater = App::getData('app')->get('template');
+		return $templater($tpl_path, $variables);
+
+		// return new PhpTemplate($tpl_path, $variables, $layout, $templates_dirs);
 	}
 
 

+ 6 - 1
src/helpers/helpers_default.php

@@ -212,7 +212,9 @@ if (! function_exists('date_us_to_fr')) {
 
 if (! function_exists('date_us2_to_fr')) {
 	function date_us2_to_fr($date_us, $include_time=false) {
-		if (empty($date_us)) {
+		//pre($date_us, "date_us2_to_fr: "); exit;
+
+		if (empty($date_us) || strlen($date_us) < 8) {
 			return null;
 		}
 		$time = ($include_time) ? substr($date_us, 10) : "";
@@ -385,6 +387,9 @@ if (! function_exists('formatDuration')) {
 	    if (empty($seconds)) {
 	        return 0 . " s";
 
+	    } else if ($seconds < 1/1000) {
+	        return round($seconds*1000*1000, 4) . " µs";
+
 	    } else if ($seconds < 1) {
 	        return round($seconds*1000, 4) . " ms";