瀏覽代碼

dev 2019-11

Max YO 6 年之前
父節點
當前提交
9ceae776c5

+ 33 - 0
helpers/helpers_array.php

@@ -1,5 +1,7 @@
 <?php
 
+use \KarmaFW\App;
+
 
 if (! function_exists('arrayReduceToOneColumn')) {
 	function arrayReduceToOneColumn($array, $column_key) {
@@ -57,6 +59,19 @@ if (! function_exists('arrayGroupByColumn')) {
 	}
 }
 
+if (! function_exists('arrayToList')) {
+	function arrayToList($array) {
+		$results = array();
+		$db = App::getDb();
+
+		foreach ($array as $k => $v) {
+			$results[] = $db->escape($v);
+		}
+
+		return implode(', ', $results);
+	}
+}
+
 
 if (! function_exists('get_csv')) {
 	function get_csv($arr, $fields=array(), $sep=";") {
@@ -92,3 +107,21 @@ if (! function_exists('get_csv')) {
 	}
 }
 
+
+if (! function_exists('exportToCsvFile')) {
+	function exportToCsvFile($rows, $export_filename=null, $fields=null) {
+		if (! empty($export_filename)) {
+			// download file
+			header('Content-Type: text/csv');
+			header('Content-Disposition: attachment;filename=' . basename($export_filename));
+			header("Pragma: no-cache");
+			header("Expires: 0");
+		} else {
+			// show in browser
+			header('Content-Type: text/plain');
+		}
+
+		echo get_csv($rows, $fields);
+		exit;
+	}
+}

+ 93 - 0
helpers/helpers_default.php

@@ -1,5 +1,7 @@
 <?php
 
+use \KarmaFW\Routing\Router;
+
 
 if (! function_exists('pre')) {
 	function pre($var, $exit = false, $prefix = '') {
@@ -17,6 +19,14 @@ if (! function_exists('pre')) {
 }
 
 
+if (! function_exists('ifEmpty')) {
+	function ifEmpty($val, $default_value='-1') {
+		return empty($val) ? $default_value : $val;
+	}
+}
+
+
+
 if (! function_exists('errorHttp')) {
 	function errorHttp($error_code, $message='An error has occured', $title='Error') {
 		header("HTTP/1.0 " . $error_code . " " . $title);
@@ -58,3 +68,86 @@ if (! function_exists('cookie')) {
 		return isset($_COOKIE[$key]) ? $_COOKIE[$key] : $default_value;
 	}
 }
+
+
+if (! function_exists('slugify')) {
+	function slugify($text, $max_length=null) {
+	    // https://stackoverflow.com/questions/2955251/php-function-to-make-slug-url-string
+
+	    // replace non letter or digits by -
+	    $text = preg_replace('~[^\pL\d]+~u', '-', $text);
+
+	    // transliterate
+	    $text = iconv('utf-8', 'us-ascii//TRANSLIT', $text);
+
+	    // remove unwanted characters
+	    $text = preg_replace('~[^-\w]+~', '', $text);
+
+	    // trim
+	    $text = trim($text, '-');
+
+	    // remove duplicate -
+	    $text = preg_replace('~-+~', '-', $text);
+
+	    // lowercase
+	    $text = strtolower($text);
+
+	    if (empty($text)) {
+	    	return 'n-a';
+	    }
+
+	    if (! empty($max_length) && strlen($text) > $max_length) {
+	    	$text = substr(0, $max_length);
+	    }
+
+	    return $text;
+	}
+}
+
+
+if (! function_exists('geneateGuid')) {
+	function geneateGuid() {
+	    
+	    if (function_exists('com_create_guid')) {
+	        return trim(com_create_guid(), '{}');
+	    }
+
+	    if (function_exists('openssl_random_pseudo_bytes') === true) {
+	        $data = openssl_random_pseudo_bytes(16);
+	        $data[6] = chr(ord($data[6]) & 0x0f | 0x40);    // set version to 0100
+	        $data[8] = chr(ord($data[8]) & 0x3f | 0x80);    // set bits 6-7 to 10
+	        return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4));
+	    }
+
+	    mt_srand((double)microtime()*10000);
+	    
+	    $charid = strtoupper(md5(uniqid(rand(), true)));
+	    $uuid = sprintf(
+	                "%s-%s-%s-%s-%s",
+	                substr($charid, 0, 8),
+	                substr($charid, 8, 4),
+	                substr($charid,12, 4),
+	                substr($charid,16, 4),
+	                substr($charid,20,12)
+	             );
+	    return strtolower($uuid);
+	}
+}
+
+
+
+if (! function_exists('generatePassword')) {
+	function generatePassword($nb_chars = 8) {
+	    $ref = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"; // 62 caractères au total
+	    $ref = $ref . $ref . $ref; // permet d'avoir jusqu'à 3 fois le meme caractere dans le mot de passe
+	    $ref = str_shuffle($ref);
+	    return substr($ref, 0, $nb_chars);
+	}
+}
+
+
+if (! function_exists('getRouteUrl')) {
+	function getRouteUrl($route_name, $urls_args=[]) {
+		return Router::getRouteUrl($route_name, $urls_args);
+	}
+}

+ 28 - 5
src/App.php

@@ -6,7 +6,7 @@ use KarmaFW\Routing\Router;
 use KarmaFW\Hooks\HooksManager;
 use KarmaFW\Database\Sql\SqlDb;
 use \KarmaFW\Database\Sql\SqlOrmModel;
-use KarmaFW\Templates\Templater;
+use KarmaFW\Templates\PhpTemplate;
 
 
 define('FW_SRC_DIR', __DIR__);
@@ -21,19 +21,31 @@ if (! defined('APP_DIR')) {
 class App
 {
 	protected static $booted = false;
+	protected static $session_user = false; // user connected with a session
+	protected static $helpers_dirs = [FW_SRC_DIR . "/../helpers"];
 
 	public static function boot()
 	{
 		HooksManager::applyHook('app_boot__before', []);
 
+		// start session
+		session_start();
+
 		// include helpers
-		self::loadHelpers(FW_SRC_DIR . "/../helpers");
+		foreach (self::$helpers_dirs as $helpers_dir) {
+			self::loadHelpers($helpers_dir);
+		}
 
 		self::$booted = true;
 		HooksManager::applyHook('app_boot__after', []);
 	}	
 
 
+	public static function registerHelpersDir($dir)
+	{
+		self::$helpers_dirs[] = $dir;
+	}
+
 	protected static function loadHelpers($dir)
 	{
 		$helpers = glob($dir . '/helpers_*.php');
@@ -121,13 +133,24 @@ class App
 
 	public static function createTemplate()
 	{
-		return new Templater();
+		return new PhpTemplate();
 	}
 
 
-	public static function createOrmModel($db, $table_name, $primary_key_values=[])
+	public static function createOrmItem($table_name, $primary_key_values=[], $db=null)
+	{
+		return new SqlOrmModel($table_name, $primary_key_values, $db);
+	}
+	
+
+	public static function getUser()
+	{
+		return self::$session_user;
+	}
+
+	public static function setUser($user)
 	{
-		return new SqlOrmModel($db, $table_name, $primary_key_values);
+		self::$session_user = $user;
 	}
 
 }

+ 17 - 1
src/Database/Sql/Drivers/Mysqli/MySqliDriver.php

@@ -2,8 +2,10 @@
 
 namespace KarmaFW\Database\Sql\Drivers\Mysqli;
 
+use \KarmaFW\Database\Sql\SqlQuery;
 use \KarmaFW\Database\Sql\SqlDriver;
 use \KarmaFW\Database\Sql\SqlDriverInterface;
+use \KarmaFW\Database\Sql\SqlResultSetError;
 
 
 class MySqliDriver extends SqlDriver implements SqlDriverInterface
@@ -47,7 +49,21 @@ class MySqliDriver extends SqlDriver implements SqlDriverInterface
 		}
 
 		$rs = mysqli_query($this->conn, $query);
-		return new MysqliResultset($rs);
+
+		$error_code = $this->getConn()->errno;
+		if ($error_code) {
+			$error_msg = $this->getConn()->error;
+			return new SqlResultSetError($query, $error_code, $error_msg);
+		}
+
+		if (strpos($query, " SQL_CALC_FOUND_ROWS ")) {
+			$tmp_query = new SqlQuery($this->db);
+	        $found_rows = $tmp_query->execute('SELECT FOUND_ROWS() AS found_rows')->fetchColumn('found_rows');
+		} else {
+			$found_rows = null;
+		}
+
+		return new MysqliResultset($rs, $found_rows);
 	}
 
 

+ 4 - 2
src/Database/Sql/Drivers/Mysqli/MysqliResultset.php

@@ -39,10 +39,12 @@ class MysqliResultset extends SqlResultset implements SqlResultsetInterface
 		$rows = parent::fetchAll();
 
 		if (! is_null($this->found_rows)) {
+			/*
 			$rows = array(
-				'FOUND_ROWS' => $this->found_rows,
-				'ROWS' => $rows,
+				'found_rows' => $this->found_rows,
+				'data' => $rows,
 			);
+			*/
 		}
 
 		return $rows;

+ 15 - 3
src/Database/Sql/SqlDb.php

@@ -9,7 +9,7 @@ class SqlDb
 	protected $last_query = null;
 	protected $schema = null;
 	protected $tools = null;
-	public $throwOnSqlError = false;
+	public $throwOnSqlError = 1;
 	public $throwOnConnectionError = true;
 
 
@@ -82,7 +82,13 @@ class SqlDb
 
 	public function getTable($table_name) /* : SqlTable */
 	{
-		return new SqlTable($this, $table_name);
+		return new SqlTable($table_name, $this);
+	}
+
+
+	public function createOrmItem($table_name, $primary_key_values=[]) /* : SqlOrmModel */
+	{
+		return new SqlOrmModel($table_name, $primary_key_values, $this);
 	}
 
 
@@ -101,6 +107,12 @@ class SqlDb
 	}
 
 
+	public function isConnected()
+	{
+		return $this->getDriver()->isConnected();
+	}
+
+
 	/* #### */
 
 
@@ -140,7 +152,7 @@ class SqlDb
 	{
 		return $this->createQuery()->executeUpdate($sql, $params);
 	}
-	
+
 	public function executeDelete($query, $params=[])
 	{
 		return $this->createQuery()->executeDelete($sql, $params);

+ 20 - 0
src/Database/Sql/SqlExpr.php

@@ -0,0 +1,20 @@
+<?php
+
+namespace KarmaFW\Database\Sql;
+
+
+class SqlExpr
+{
+	protected $expr = null;
+
+    public function __construct($expr)
+    {
+        $this->expr = $expr;
+    }
+
+    public function __toString()
+    {
+        return $this->expr;
+    }
+
+}

+ 20 - 0
src/Database/Sql/SqlLike.php

@@ -0,0 +1,20 @@
+<?php
+
+namespace KarmaFW\Database\Sql;
+
+
+class SqlLike
+{
+	protected $expr = null;
+
+    public function __construct($expr)
+    {
+        $this->expr = $expr;
+    }
+
+    public function __toString()
+    {
+        return $this->expr;
+    }
+
+}

+ 18 - 2
src/Database/Sql/SqlOrmModel.php

@@ -2,6 +2,8 @@
 
 namespace KarmaFW\Database\Sql;
 
+use \KarmaFW\App;
+
 
 class SqlOrmModel
 {
@@ -14,11 +16,18 @@ class SqlOrmModel
 	public $autosave = false;
 
 
-	public function __construct($db, $table_name=null, $primary_key_values=[])
+	public function __construct($table_name=null, $primary_key_values=[], $db=null)
 	{
+		if (is_null($db)) {
+			$db = App::getDb();
+		}
 		$this->db = $db;
 		$this->table_name = $table_name;
 		$this->primary_key_values = $primary_key_values;
+
+		if (! empty($primary_key_values)) {
+			$this->load($primary_key_values);
+		}
 	}
 
 
@@ -138,7 +147,14 @@ class SqlOrmModel
 	{
 		$this->primary_key_values = $primary_key_values;
 
-		$data = $this->db->createQuery()->tableSelect($this->table_name, $this->primary_key_values, ['limit' => 1])->fetchOne();
+		$data = $this->db->createQuery()->tableSelectOne($this->table_name, $this->primary_key_values, ['limit' => 1]);
+		
+		if (is_null($data)) {
+			$this->primary_key_values = [];
+			$this->table_row = [];
+			return null;
+		}
+
 		$result = $this->loadFromArray($data, $primary_key_values);
 
 		return $result;

+ 22 - 19
src/Database/Sql/SqlQuery.php

@@ -2,6 +2,8 @@
 
 namespace KarmaFW\Database\Sql;
 
+use KarmaFW\Database\Sql\SqlResultSetError;
+
 
 class SqlQuery
 {
@@ -42,7 +44,7 @@ class SqlQuery
         ];
     }
 
-
+    /*
 	public function fetchColumn($column_name)
 	{
 		if ($this->status == 'ready') {
@@ -68,6 +70,7 @@ class SqlQuery
 		}
 		return $this->recordset->fetchAll();
 	}
+	*/
 
 
 	public function getQuery()
@@ -84,6 +87,10 @@ class SqlQuery
 
 	public function execute($query=null, $params=[])
 	{
+		if (! $this->db->isConnected()) {
+			$this->db->connect();
+		}
+
 		if (empty($query)) {
 			$query = $this->query;
 		}
@@ -114,6 +121,7 @@ class SqlQuery
 
 		//echo $query . "<hr />";
 		$rs = $this->db->getDriver()->execute($query);
+		//pre($query);
 		//pre($rs);
 
 		$ts_end = microtime(true);
@@ -122,11 +130,10 @@ class SqlQuery
 		$this->recordset = $rs;
 		$this->db->setLastQuery($this);
 
-		$error_code = $this->db->getDriver()->getConn()->errno;
-	
-		if ($error_code !== 0) {
+		if ($rs instanceOf SqlResultSetError) {
 			// query error
-			$error_msg = $this->db->getDriver()->getConn()->error;
+			$error_code = $rs->getErrorCode();
+			$error_msg = $rs->getErrorMessage();
 			$this->error = $error_msg;
 			$this->status = 'error';
 
@@ -142,14 +149,8 @@ class SqlQuery
 		$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;
+		//return $this;
+		return $rs;
 	}
 
 	
@@ -206,12 +207,12 @@ class SqlQuery
 	public function tableSelect($table_name, $where=[], $options=[])
 	{
 		// Alias of tableSelectAll
-		return $this->tableSelectAll($table_name, $where, $options)->all();
+		return $this->tableSelectAll($table_name, $where, $options);
 	}
 
 	public function tableSelectAll($table_name, $where=[], $options=[])
 	{
-		$table = new SqlTable($this->db, $table_name);
+		$table = new SqlTable($table_name, $this->db);
 		$query = $table->buildQuery($where, $options);
 		return $this->executeSelectAll($query);
 	}
@@ -223,7 +224,9 @@ class SqlQuery
 		}
 		$options['limit'] = 1;
 
-		return $this->tableSelect($table_name, $where, $options)->one();
+		$table = new SqlTable($table_name, $this->db);
+		$query = $table->buildQuery($where, $options);
+		return $this->executeSelectOne($query);
 	}
 
 
@@ -239,21 +242,21 @@ class SqlQuery
 
 	public function tableInsertAll($table_name, $inserts=[], $options=[])
 	{
-		$table = new SqlTable($this->db, $table_name);
+		$table = new SqlTable($table_name, $this->db);
 		return $table->insertAll($inserts, $options);		
 	}
 
 
 	public function tableUpdate($table_name, $updates=[], $where=[], $options=[])
 	{
-		$table = new SqlTable($this->db, $table_name);
+		$table = new SqlTable($table_name, $this->db);
 		return $table->update($updates, $where, $options);
 	}
 
 
 	public function tableDelete($table_name, $where=[], $options=[])
 	{
-		$table = new SqlTable($this->db, $table_name);
+		$table = new SqlTable($table_name, $this->db);
 		return $table->delete($where, $options);
 	}
 

+ 55 - 0
src/Database/Sql/SqlResultSetError.php

@@ -0,0 +1,55 @@
+<?php
+
+namespace KarmaFW\Database\Sql;
+
+
+class SqlResultSetError
+{
+	protected $query;
+	protected $error_code;
+	protected $error_msg;
+
+	public function __construct($query, $error_code, $error_msg)
+	{
+		$this->query = $query;
+		$this->error_code = $error_code;
+		$this->error_msg = $error_msg;
+
+	}
+
+	public function getErrorCode()
+	{
+		return $this->error_code;
+	}
+	
+	public function getErrorMessage()
+	{
+		return $this->error_msg;
+	}
+
+	public function getRowsCount()
+	{
+		return 0;
+	}
+
+	public function getfoundRowsCount()
+	{
+		return 0;
+	}
+
+	public function fetchAll()
+	{
+		return [];
+	}
+
+	public function fetchOne()
+	{
+		return null;
+	}
+
+	public function fetchColumn($column_name)
+	{
+		return null;
+	}
+	
+}

+ 25 - 6
src/Database/Sql/SqlTable.php

@@ -2,6 +2,8 @@
 
 namespace KarmaFW\Database\Sql;
 
+use \KarmaFW\App;
+
 
 class SqlTable
 {
@@ -10,10 +12,13 @@ class SqlTable
 	protected $columns = null;
 
 
-	public function __construct($db, $table_name)
+	public function __construct($table_name, $db=null)
 	{
-		$this->db = $db;
+		if (is_null($db)) {
+			$db = App::getDb();
+		}
 		$this->table_name = $table_name;
+		$this->db = $db;
 	}
 
 
@@ -110,12 +115,19 @@ class SqlTable
 
 	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 getAllWithFoundRows($where=null, $options=[]) /* : array */
+	{
+		$query = $this->buildQuery($where, $options);
+		$rs = $this->db->createQuery()->execute($query);
+		$data = $rs->fetchAll();
+		$found_rows = $rs->getfoundRowsCount();
+		return ['found_rows' => $found_rows, 'data' => $data];
+	}
+
 
 	public function selectOne($where=null, $options=[])
 	{
@@ -126,12 +138,19 @@ class SqlTable
 	public function getOne($where=null, $options=[]) /* : array */
 	{
 		$options['limit'] = 1;
-		return $this->getAll($where, $options)->fetchOne();
+		//return $this->getAll($where, $options)->fetchOne();
+		$query = $this->buildQuery($where, $options);
+		return $this->db->createQuery()->executeSelectOne($query);
 	}
 
 
 	public function buildQuery($where=null, $options=[]) /* : string */
 	{
+		if (empty($options['order_by']) && ! empty($options['order by'])) {
+			// alias "order by" to "order_by"
+			$options['order_by'] = $options['order by'];
+		}
+
 		$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;
@@ -159,7 +178,7 @@ class SqlTable
 					" . $limit_sql;
 
 		if (! empty($options['debug'])) {
-			echo "<pre>" .preg_replace('/\s+/', '', $query) . "</pre>";
+			echo "<pre>" .preg_replace('/\s+/', ' ', $query) . "</pre>";
 		}
 
 		return $query;

+ 10 - 7
src/Database/Sql/SqlTools.php

@@ -128,13 +128,15 @@ class SqlTools
 
                 }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) {
+                    
+                }else if ($value instanceof SqlLike) {
+                    $where_sql[] = $key . ' like ' . (string) $this->escape($value);
+                    
+                }else if ($value instanceof SqlExpr) {
                     $where_sql[] = $key . ' = ' . (string) $value;
-*/
+                    
+                }else if ($value instanceof SqlWhere) {
+                    $where_sql[] = (string) $value;
                     
                 }else{
                     $where_sql[] = $key . ' = ' . $this->escape($value);
@@ -142,7 +144,8 @@ class SqlTools
                 }
             }
         }
-        //print_r($where_sql);
+        //pre($where_sql, 1);
+
         return implode(" and ", $where_sql);
 
 	}

+ 20 - 0
src/Database/Sql/SqlWhere.php

@@ -0,0 +1,20 @@
+<?php
+
+namespace KarmaFW\Database\Sql;
+
+
+class SqlWhere
+{
+	protected $expr = null;
+
+    public function __construct($expr)
+    {
+        $this->expr = $expr;
+    }
+
+    public function __toString()
+    {
+        return $this->expr;
+    }
+
+}

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

@@ -2,6 +2,8 @@
 
 namespace KarmaFW\Routing\Controllers;
 
+use \KarmaFW\Hooks\HooksManager;
+
 
 class WebController
 {
@@ -17,6 +19,24 @@ class WebController
 		$this->request_uri = $request_uri;
 		
 		//echo "DEBUG " . __CLASS__ . ": controller instanced<hr />" . PHP_EOL;
+
+		HooksManager::applyHook('webcontroller__init', [$this]);
+	}
+
+
+	public function getRoute()
+	{
+		return $this->route;
+	}
+
+	public function getRequestMethod()
+	{
+		return $this->request_method;
+	}
+
+	public function getRequestUri()
+	{
+		return $this->request_uri;
 	}
 
 }

+ 37 - 0
src/Routing/Router.php

@@ -115,5 +115,42 @@ class Router
 		return null;
 	}
 
+	
+	public static function getRouteUrl($route_name, $urls_args=[])
+	{
+		if (empty($urls_args)) {
+			$urls_args = array();
+		}
+
+		if (! is_array($urls_args)) {
+			$urls_args = array($urls_args);
+		}
+
+		$route = Router::findRouteByName($route_name);
+		if (empty($route) || $route === true) {
+			return null;
+		}
+
+		$link = $route->getMatchUrl();
+		//echo "<pre>"; var_dump($route); exit;
+		$link = rtrim($link, '$');
+		$link = ltrim($link, '^');
+
+		if (! empty($urls_args)) {
+			foreach ($urls_args as $arg_value) {
+				$pos1 = strpos($link, '(');
+				if ($pos1 === false) {
+					break;
+				}
+				$pos2 = strpos($link, ')', $pos1);
+				if ($pos2 === false) {
+					break;
+				}
+				$link = substr($link, 0, $pos1) . $arg_value . substr($link, $pos2+1);
+			}
+		}
+
+		return $link;
+	}
 
 }

+ 53 - 9
src/Templates/PhpTemplate.php

@@ -8,6 +8,7 @@ class PhpTemplate
 	public $tpl_dir = APP_DIR . '/templates';
 	protected $vars = [];
 	protected $plugins = [];
+	protected $layout = null;
 
 	function __construct($tpl_dir=null, $default_vars=[])
 	{
@@ -21,17 +22,37 @@ class PhpTemplate
 
 		$this->assign($default_vars);
 
+		$template = $this;
+		$this->addPlugin('layout', function ($param) use ($template) {
+			$template->layout = $param;
+			return '';
+		});
+		$this->addPlugin('\/\/', function ($param) {
+			return '';
+		});
 		$this->addPlugin('tr', function ($param) {
 			return gettext($param);
 		});
+		$this->addPlugin('include', function ($param) use ($template) {
+			$template = new PhpTemplate($template->tpl_dir, $template->vars);
+			$templatechild_content = $template->fetch($param);
+			return $templatechild_content;
+		});
+		$this->addPlugin('routeUrl', function ($param) use ($template) {
+			$params = explode(' ', $param);
+			$route_name = array_shift($params);
+			$url_args = $params;
+			$url = getRouteUrl($route_name, $url_args);
+			return $url;
+		});
 	}
 
 	public static function createTemplate($tpl_dir=null, $default_vars=[])
 	{
-		return new Templater($tpl_dir, $default_vars);
+		return new PhpTemplate($tpl_dir, $default_vars);
 	}
 
-	public function fetch($tpl, $layout=null, $extra_vars=array())
+	public function fetch($tpl, $layout=null, $extra_vars=[])
 	{
 		$tpl_dirs = [];
 
@@ -62,8 +83,8 @@ class PhpTemplate
 			throw new \Exception("Template not found : " . $tpl, 1);
 		}
 		
-		extract($this->vars);
-		extract($extra_vars);
+		$tpl_vars = array_merge($this->vars, $extra_vars);
+		extract($tpl_vars);
 		
 		if ($tpl_path) {
 			ob_start();
@@ -76,10 +97,11 @@ class PhpTemplate
 		}
 
 
-		// plugins. ex: {tr:English text} ==> "Texte francais"
+		// 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);
+				//preg_match_all('/{' . $prefix . ':([^}]+)}/', $content, $regs, PREG_SET_ORDER);
+				preg_match_all('/{' . $prefix . ' ([^}]+)}/', $content, $regs, PREG_SET_ORDER);
 				foreach($regs as $reg) {
 					$replaced = $callback($reg[1]);
 					$content = str_replace($reg[0], $replaced, $content);
@@ -88,19 +110,41 @@ class PhpTemplate
 			}
 		}
 
+		// variables. ex: {$user_name} ==> John
+		if (true) {
+			preg_match_all('/{\$([a-zA-Z0-9_\[\]\']+)}/', $content, $regs, PREG_SET_ORDER);
+			foreach($regs as $reg) {
+				$var = $reg[1];
+				
+				if (isset(${$var})) {
+					$replaced = ${$var};
+					$content = str_replace($reg[0], $replaced, $content);
+				} else {
+					//$content = str_replace($reg[0], '', $content);
+				}
+			}
+		}
+
+		// si pas de layout defini, on recupere celui eventuel du plugin layout (c'est a dire venant d'un marker {layout xxx} dans le template)
+		if (is_null($layout)) {
+			$layout = $this->layout;
+		}
+		$this->layout = null;
 
 		if (empty($layout)) {
 			return $content;
 
 		} else {
-			$content_layout = $this->fetch($layout, null, array('layout_content' => $content));
+			$extra_vars['child_content'] = $content;
+			//$extra_vars['child_content'] = '{CONTENT OF ' . $tpl . '}';
+			$content_layout = $this->fetch($layout, null, $extra_vars);
 			return $content_layout;
 		}
 	}
 
-	public function display($tpl, $layout=null)
+	public function display($tpl, $layout=null, $extra_vars=[])
 	{
-		echo $this->fetch($tpl, $layout);
+		echo $this->fetch($tpl, $layout, $extra_vars);
 	}
 
 	public function assign($var_name, $var_value=null)