diff --git a/Action.class.php b/Action.class.php new file mode 100644 index 0000000..69e2465 --- /dev/null +++ b/Action.class.php @@ -0,0 +1,101 @@ +class = get_class($this); + $this->template = substr($this->class, 0, -strlen(ACTION_POSTFIX)); + + if (CACHE_ENABLE && ($cache_name = $this->getCacheKey()) !== false) { + $cache = new Cache($this->class.'_'.$cache_name, $this->getCacheTime()); + if ($cache->isCached()) { + $this->restore($cache->load()); + } else { + $this->init(); + $cache->save($this->store()); + } + } else { + $this->init(); + } + } + + /** + * Выдает результат действия. + * + * @return string + */ + public function display() + { + $this->templater = Load::templater(ACTION_TPL_PATH.'/'.$this->template_dir); + $this->prepare(); + return $this->templater->fetch($this->template.'.tpl'); + } + + /** + * Инициализация данных действия. + * Тут должна быть ВСЯ работа с установкой данных, + * дабы потом они нормально закешировались. + * Этот метод НЕ исполняется при срабатывании кеша. + * + */ + abstract protected function init(); + + /** + * Подготовка данных для шаблона. + * Этот метод выполняется ВСЕГДА + */ + protected function prepare() {} + + /** + * Возвращает имя файла для кеширования + * Переопределяется в доченрих классах. + * + * @return false/string - имя файла кеша либо false если кеш запрещен + */ + protected function getCacheKey() + { + return false; + } + + /** + * Возвращает время жизни кеша в секундах + * При отрицательном значении кеш вечен. + * Переопределяется в доченрих классах. + * + * @return false/integer - время жизни кеша либо false если кеш запрещен + */ + protected function getCacheTime() + { + return 0; + } + + /** + * Выдает строку для кеширования. + */ + protected function store() + { + return serialize(get_object_vars($this)); + } + + /** + * Разбирает строку кеша, созданную методом store + */ + protected function restore($data) + { + foreach(unserialize($data) as $key => $val) { + $this->$key = $val; + } + } +} +?> \ No newline at end of file diff --git a/Cache.class.php b/Cache.class.php new file mode 100644 index 0000000..399926c --- /dev/null +++ b/Cache.class.php @@ -0,0 +1,58 @@ +cache_time = (int) $cache_time; + $this->cache_file = CACHE_PATH.'/'.$cache_name; + } + + /** + * Сохраняет кэш в файл + * + * @return boolean - сохранил или нет + */ + function save($data) + { + if ($this->cache_time != 0) { + return (bool) file_put_contents($this->cache_file, $data); + } + return false; + } + + /** + * Достает кэш из файла + * + * @return string - содержимое кеша + */ + function load() + { + return file_get_contents($this->cache_file); + } + + /** + * Проверяет, закешированы ли данные с таким именем + * + * @return boolean - закеширован или нет + */ + function isCached() + { + if (! file_exists($this->cache_file)) { + return false; + } + if ($this->cache_time > 0 && $this->cache_time + filemtime($this->cache_file) < TIME_NOW) { + return false; + } + return true; + } +} + +?> \ No newline at end of file diff --git a/DBConnector.class.php b/DBConnector.class.php new file mode 100644 index 0000000..bbfb4a4 --- /dev/null +++ b/DBConnector.class.php @@ -0,0 +1,40 @@ + \ No newline at end of file diff --git a/Decorator.class.php b/Decorator.class.php new file mode 100644 index 0000000..0fa8675 --- /dev/null +++ b/Decorator.class.php @@ -0,0 +1,55 @@ +layout) { + throw new MJException('$layout not set in '.get_class($this)); + } + $this->templater = Load::templater(); + } + + /** + * Основной метод вывода + * + */ + function display(Action $action) + { + $this->action = $action; + $this->templater->assign($this->action_name, $this->action->display(), $this->layout); + + $this->exec(); + + $this->templater->setPath(WRAPPERS_TPL_PATH); + return $this->templater->fetch($this->layout); + } + + /** + * Добавить данные на вывод + * + * @param string $var_name - имя переменной, в которую будет осуществлен вывод + * @param Action $action - действие + * @param Decorator $decorator - декоратор, в котором должно выполнится действие. + */ + function addOutput($var_name, Action $action, Decorator $decorator = null) + { + $this->templater->assign($var_name, $decorator ? $decorator->display($action) : $action->display(), $this->layout); + } + + /** + * Основной метод для дочерних декораторов + * + */ + abstract function exec(); +} +?> \ No newline at end of file diff --git a/Env.class.php b/Env.class.php new file mode 100644 index 0000000..c2a7636 --- /dev/null +++ b/Env.class.php @@ -0,0 +1,64 @@ + \ No newline at end of file diff --git a/Load.class.php b/Load.class.php new file mode 100644 index 0000000..07e584f --- /dev/null +++ b/Load.class.php @@ -0,0 +1,35 @@ +setPath($path); + } + return self::$templater; + } + return self::$templater = new Sublimer($path); + } + + static function router() + { + return self::$router ? self::$router : self::$router = new Router; + } +} +?> \ No newline at end of file diff --git a/MJException.class.php b/MJException.class.php new file mode 100644 index 0000000..6db0916 --- /dev/null +++ b/MJException.class.php @@ -0,0 +1,57 @@ +MJ Error: "; + $return .= str_replace("\n", "
\n", $this->getMessage())."
\n"; + + $trace = $this->getTrace(); + + $file = reset($trace); + + //смещение в трейсе, указаное при вызове эксепшена (2й параметр). Нужно для более инофрмативного вывода + if ($shift = abs($this->getCode())) { + while($shift--) { + $file = next($trace); + } + } + + if ($fp = fopen($file['file'], 'r')) { + $error_line = $file['line']; + $start = $error_line - $this->line_range; + $end = $error_line + $this->line_range; + $i = 1; + $return .= "
";
+            while ($line = fgets($fp, 4096) and $i<=$end) {
+                $line = htmlspecialchars($line);
+                if ($i >= $start && $i <= $end) {
+                    if ($i == $error_line) $return .= '
'.$i.' '.$line.'
'; + else $return .= $i.' '.$line; + } + $i++; + } + $return .= "
"; + fclose($fp); + } + + $return .= ''; + $return .= "\n'; + foreach($trace as $row) { + if (isset($row['file'])) { //throwing exception from __call method will not return file and line + $return .= "\n'; + } + } + return $return . '
Backtrace
".$this->getFile().''.$this->getLine().'
".$row['file'].''.$row['line'].'
'; + } +} +?> diff --git a/Model.class.php b/Model.class.php new file mode 100644 index 0000000..b5da5bf --- /dev/null +++ b/Model.class.php @@ -0,0 +1,186 @@ +handler = DBConnector::getConnect(Env::getParam('db_settings')); + } + + /** + * Выполняет запрос и возвращает сырой результат + * + * @param string $sql + * @return resource + */ + function exec($sql) + { + $time = microtime(true); + $res = mysqli_query($this->handler, $sql); + if (mysqli_errno($this->handler)) { + throw new MJException("Query Error:\n".$sql."\nError:\n".mysqli_error($this->handler), 1); + } + + if (DEBUG_ENABLE) { + DBConnector::$queries[] = $sql.'; ('.round((microtime(true)-$time)*1000, 1).'ms)'; + } + + return $res; + } + + /** + * Выполняет запрос и возвращает объект результата + * + * @param string $sql + * @return object + */ + function query($sql) + { + $res = $this->exec($sql); + + switch (strtolower(substr($sql, 0, 6))) { + case 'select': + return new ModelSelectResult($res); + case 'insert': + return new ModelInsertResult($this->handler); + default: + return new ModelChangeResult($this->handler); + } + } + + /** + * Экранирует строку + * + * @param string $data - строка для экранирования + * @return string + */ + function escape($data) + { + return mysqli_real_escape_string($this->handler, $data); + } + +////////////////////////// + function update($id, $data) + { + $sql = ''; + foreach ($data as $key => $val) { + $sql .= $key."='".$this->escape($val)."', "; + } + return $this->query('UPDATE '.$this->table.' SET '.rtrim($sql, ', ').' WHERE '.$this->primary_key.'='.(int) $id); + } + + function insert($data, $postfix = '') + { + $sql = ''; + foreach ($data as $key => $val) { + $sql .= $key.'="'.$this->escape($val).'", '; + } + return $this->query('INSERT '.$this->table.' SET '.rtrim($sql, ', ').' '.$postfix); + } + + function delete($id) + { + return $this->query('DELETE FROM '.$this->table.' WHERE '.$this->primary_key.'='.(int) $id); + } + + function get($id) + { + return $this->query('SELECT * FROM '.$this->table.' WHERE '.$this->primary_key.'='.(int) $id); + } + + function getList($limit = false, $sort = 'ASC') + { + return $this->query('SELECT * FROM '.$this->table.' ORDER BY '.$this->primary_key.' '.($sort == 'ASC' ? 'ASC' : 'DESC').($limit === false ? ' LIMIT '.(int) $limit : '')); + } +} + +class ModelResult +{ + function __call($name, $args) + { + throw new MJException('Call undeclared method "'.$name.'" in "'.get_class($this).'" class', -1); + } +} + +class ModelSelectResult extends ModelResult +{ + public $result; + + function __construct($res) + { + $this->result = $res; + } + + function fetch() + { + return mysqli_fetch_object($this->result); + } + + function fetchField($field, $default = false) + { + $row = $this->fetch(); + return isset($row->$field) ? $row->$field : $default; + } + + function fetchAll() + { + $array = array(); + while ($row = mysqli_fetch_object($this->result)) { + $array[] = $row; + } + return $array; + } + + function count() + { + return mysqli_num_rows($this->result); + } + + function free() + { + mysqli_free_result($this->result); + } + + function __destruct() { + $this->free(); + } +} + +class ModelChangeResult extends ModelResult +{ + public $affected; + + function __construct($resource) + { + $this->affected = mysql_affected_rows($resource); + } + + function count() + { + return $this->affected; + } +} + +class ModelInsertResult extends ModelResult +{ + public $id; + + function __construct($resource) + { + $this->id = mysqli_insert_id($resource); + } + + function getId() + { + return $this->id; + } +} +?> \ No newline at end of file diff --git a/PageController.class.php b/PageController.class.php new file mode 100644 index 0000000..caa88e6 --- /dev/null +++ b/PageController.class.php @@ -0,0 +1,27 @@ +route = Load::router()->proccess(MJ_PATH); + $decorator = new $this->route->decorator; + return $decorator->display(new $this->route->action); + } catch (MJException $e) { + return $e->terminate(); + } catch (Exception $e) { + $decorator_name = DEFAULT_DECORATOR; + $action_name = DEFAULT_ACTION; + + $decorator = new $decorator_name; + return $decorator->display(new $action_name($e->getMessage())); + } + } +} +?> \ No newline at end of file diff --git a/Router.class.php b/Router.class.php new file mode 100644 index 0000000..c8d6373 --- /dev/null +++ b/Router.class.php @@ -0,0 +1,106 @@ +routes[$name] = new Route($path, $action, $params); + } + + /** + * Установить декоратор для роута (действия), отличный от стандартного + * + * @param string $name - имя роута (действия) + * @param string $decorator - имя декоратора + */ + function setDecorator($name, $decorator) + { + if (isset($this->routes[$name])) { + $this->routes[$name]->decorator = $decorator.DECORATOR_POSTFIX; + } + } + + /** + * Найти роутер соответствующий заданному пути + * + * @param stirng $path - путь + * @return Route - роутер + */ + function proccess($path) + { + $path = explode('/', $path); + + foreach ($this->routes as $name => $route) { + if ($route->match($path)) { + $route->action .= ACTION_POSTFIX; + Env::setParams($route->params); + return $route; + } + } + throw new Exception(E_404); + } +} + +/** + * Роутер + * + */ +final class Route +{ + const URL_VARIABLE = '&'; + + protected $path; + + public $decorator = DEFAULT_DECORATOR; + public $action; + public $params; + + function __construct($path, $action, $params=array()) + { + $this->path = $path; + $this->action = $action; + $this->params = $params; + } + + /** + * Проверяет соответствие роутера и пути + * + * @param string $path - путь для сравнения + * @return boolean - соответствует или нет + */ + function match($path_arr) + { + $parts = explode('/', $this->path); + $cnt = count($parts); + if (end($parts) == self::URL_VARIABLE) { + $cnt--; + } elseif ($cnt != count($path_arr)) { + return false; + } + + for ($i=0; $i<$cnt; $i++) { + if (substr($parts[$i], 0, 1) == self::URL_VARIABLE) { + $this->params[substr($parts[$i], 1)] = $path_arr[$i]; + } elseif ($parts[$i] != $path_arr[$i]) { + return false; + } + } + + return true; + } +} +?> \ No newline at end of file diff --git a/Sublimer.class.php b/Sublimer.class.php new file mode 100644 index 0000000..f79fb01 --- /dev/null +++ b/Sublimer.class.php @@ -0,0 +1,93 @@ +setPath($path); + $this->vars[self::GLOBAL_KEY] = array(); + } + + /** + * Присвоить переменную только + * + * @param string $name - имя переменной + * @param mixed $value - значение переменной + * @param string $template - шаблон, если не указан, то переменная глобальная + */ + public function assign($name, $value, $template = self::GLOBAL_KEY) + { + $this->vars[$template][$name] = $value; + } + + /** + * Очистить стек переменных + * @param boolean $with_local - включая локальные переменные + */ + public function clear($template = self::GLOBAL_KEY) + { + $this->vars[$template] = array(); + } + + /** + * Обработать шаблон + * + * @param string $template - относительный путь до шаблона + * @return string - обработанное содержимое шаблона + */ + public function fetch($template) + { + $this->template = $template; //дабы экстракт не перезатер нам переменную. Это важно! $this то он не перезатрет :) + extract($this->vars[self::GLOBAL_KEY]); + if (isset($this->vars[$this->template])) { + extract($this->vars[$this->template], EXTR_OVERWRITE); + } + ob_start(); + if (!(include $this->path.'/'.$this->template) == 'OK') { + throw new MJException('Template '.$this->path.'/'.$this->template.' not found'); + } + return ob_get_clean(); + } + + /** + * Установать путь до шаблонов + * + * @param string $path - путь до шаблонов + */ + public function setPath($path) + { + $this->path = $path; + } + +//Функции для вызова из шаблонов. + + protected function addHead($str) + { + $this->head_array[] = $str; + } + + protected function getHead() + { + return $this->head_array; + } +} + +?> \ No newline at end of file diff --git a/User.class.php b/User.class.php new file mode 100644 index 0000000..3267274 --- /dev/null +++ b/User.class.php @@ -0,0 +1,90 @@ +password != md5($password)) { + return false; + } + + self::setSession(); + return true; + } + + static function logout() + { + Env::setCookie(session_name(), '', 0); + Env::setCookie('login', '', 0); + Env::setCookie('login_hash', '', 0); + if (session_id()) { + session_destroy(); + } + } + + static function process() + { + if (Env::getCookie(session_name())) { //есть сессия + session_start(); + } elseif (Env::getCookie('login') && Env::getCookie('login_hash')) { + self::remember(); + } + } + + static function setSession() + { + Env::setCookie('login', self::$user->login, TIME_NOW + LOGIN_COOKIE_TTL); + Env::setCookie('login_hash', self::getHash(), TIME_NOW + LOGIN_COOKIE_TTL); + + session_start(); + + $_SESSION['user'] = self::$user; + } + + static function remember() + { + if (! self::$user = self::getByLogin(Env::getCookie('login'))) { + self::logout(); + } + + if (Env::getCookie('login_hash') == self::getHash()) { + self::setSession(); + } else { + self::logout(); + } + } + + static function getHash() + { + return md5(self::$user->id.'hckrz'.self::$user->login.'mst'.self::$user->password.'dai'); + } + + static function getInfo() + { + return Env::Session('user', self::$user); + } + + static function isGuest() + { + return ! (bool) Env::Session('user'); + } + + static function getByLogin($login) + { + return Load::model('UserData')->getByLogin($login); + } +} +?> \ No newline at end of file