├── .gitignore ├── _config.yml ├── src ├── RouteUrl.php ├── Criteria.php ├── RouteCli.php ├── View.php ├── Json.php ├── Controller.php ├── Session.php ├── functions.php ├── Database.php ├── Core.php ├── Model.php ├── Services_JSON.php └── Spyc.php ├── composer.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-slate -------------------------------------------------------------------------------- /src/RouteUrl.php: -------------------------------------------------------------------------------- 1 | config['url_param'] ? $this->config['url_param'] : '_url']; 7 | $this->parsePath($path); 8 | } 9 | } -------------------------------------------------------------------------------- /src/Criteria.php: -------------------------------------------------------------------------------- 1 | =5.4.0" 15 | }, 16 | "autoload": { 17 | "files": ["src/Core.php"] 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/RouteCli.php: -------------------------------------------------------------------------------- 1 | parsePath($argv[1]); 8 | 9 | if (count($argv) > 2) { 10 | $gets = array_slice($argv, 2); 11 | 12 | foreach ($gets as $get) { 13 | $p = explode('=', $get, 2); 14 | if (count($p) == 1) { 15 | $_GET[$p[0]] = ""; 16 | } else { 17 | $_GET[$p[0]] = $p[1]; 18 | } 19 | } 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /src/View.php: -------------------------------------------------------------------------------- 1 | render($view, $data); 21 | } 22 | } 23 | } 24 | abstract class Renderer { 25 | 26 | public abstract function render($_____________________view, $data); 27 | } 28 | class HtmlRender extends Renderer { 29 | 30 | public function render($_____________________view, $data) { 31 | extract($data); 32 | $file = AtomCode::$config['view']['dir'] . DIRECTORY_SEPARATOR . $_____________________view . AtomCode::$config['view']['ext']; 33 | if (file_exists($file)) { 34 | include $file; 35 | } 36 | } 37 | } 38 | class JsonRender extends Renderer { 39 | 40 | public function render($_____________________view, $data) { 41 | if ($_REQUEST['jsonp']) { 42 | header('Content-Type: application/jsonp'); 43 | echo $_REQUEST['jsonp'] . '(' . Json::encode($data) . ')'; 44 | } else { 45 | header('Content-Type: application/json'); 46 | echo Json::encode($data); 47 | } 48 | } 49 | } 50 | class YamlRender extends Renderer { 51 | 52 | public function render($_____________________view, $data) { 53 | echo Spyc::YAMLDump($data); 54 | } 55 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | AtomCode 2 | ======== 3 | 欢迎使用 AtomCode,这是一个适合中小型项目的框架。 4 | 5 | 安装 6 | --- 7 | 1. 通过 composer 安装 8 | ```shell 9 | composer require "atomcode/atomcode" 10 | ``` 11 | 12 | 2. 直接下载最新安装包,参考下方的目录结构 13 | 14 | https://github.com/eachcan/atomcode/releases 15 | 16 | ### 目录结构 17 | 18 | 总共需要有三套目录: 框架的目录, 你自己的程序目录,你的入口文件和图片之前Web资源目录。分别如下: 19 | 20 | | atomcode # 框架放这里, 如果是 composer 安装则不需要此目录 21 | | vendor # 这是 composer 的目录,如果是下载安装则没有此目录 22 | | app # 这个名字无所谓 23 | | public # 这个名字更无所谓,作为 Web 服务器的根目录 24 | 25 | atomcode 目录你不需要关心,只需要找个地方放就行了。 26 | 27 | app 目录结构: 28 | ```shell 29 | |- config 30 | |- config.php 31 | |- controller 32 | |- model 33 | |- view 34 | ``` 35 | 36 | public 目录下,需要放置一个入口文件,内容类似于: 37 | ```PHP 38 | $v) { 45 | if ($k !== $c) { 46 | $is_assoc = TRUE; 47 | break; 48 | } 49 | 50 | $c++; 51 | } 52 | 53 | if ($is_assoc) { 54 | $a = array(); 55 | foreach ($arr as $k => $v) { 56 | $a[] = '"' . str_replace(array('\\', '"', "\n", "\r", "\t"), array('\\\\', '\\"', '\n', "\r", "\t"), $k) . '":' . self::encode($v); 57 | } 58 | return '{' . implode(',', $a) . '}'; 59 | } else { 60 | $a = array(); 61 | foreach ($arr as $v) { 62 | $a[] = self::encode($v); 63 | } 64 | return '[' . implode(',', $a) . ']'; 65 | } 66 | } 67 | 68 | public static function decode($str) { 69 | $json = new Services_JSON(SERVICES_JSON_LOOSE_TYPE); 70 | return $json->decode($str); 71 | } 72 | } -------------------------------------------------------------------------------- /src/Controller.php: -------------------------------------------------------------------------------- 1 | __render; 8 | } 9 | 10 | protected function setRender($render) { 11 | $this->__render = $render; 12 | } 13 | 14 | protected function setVar($name, $value) { 15 | $this->__data[$name] = $value; 16 | } 17 | 18 | protected function setVars($arr) { 19 | $this->__data = array_merge($this->__data, $arr); 20 | } 21 | 22 | public function getData() { 23 | return $this->__data; 24 | } 25 | 26 | public function getVar($name) { 27 | return $this->__data[$name]; 28 | } 29 | 30 | protected function setView($view) { 31 | $this->__view = $view; 32 | } 33 | 34 | public function getView() { 35 | return $this->__view; 36 | } 37 | 38 | protected function disableRender() { 39 | $this->__disable_render = true; 40 | } 41 | 42 | public function isDisabledRender() { 43 | return $this->__disable_render; 44 | } 45 | 46 | public function requireRoute($route) { 47 | if ($route == "cli" && !class_exists("RouteCli", false)) { 48 | exit("Command Line Required"); 49 | } elseif ($route == "url" && !class_exists("RouteUrl", false)) { 50 | exit("Access From Web Browswer is Required"); 51 | } 52 | } 53 | 54 | public function requireCli() { 55 | return $this->requireRoute('cli'); 56 | } 57 | 58 | public function requireUrl() { 59 | return $this->requireRoute('url'); 60 | } 61 | 62 | protected function debug() { 63 | error_reporting(E_ALL & ~E_NOTICE); 64 | ini_set("display_errors", 1); 65 | } 66 | 67 | public function _resolve($action) { 68 | return $action; 69 | } 70 | 71 | public function redirect($action) { 72 | $will_replace = strpos(AtomCode::$config['route']['base'], '?') !== false; 73 | if ($will_replace) { 74 | $action = str_replace('?', '&', $action); 75 | } 76 | header('location: ' . rtrim(AtomCode::$config['route']['base'], ' /') . '/' . ltrim($action, '/ ')); 77 | } 78 | } -------------------------------------------------------------------------------- /src/Session.php: -------------------------------------------------------------------------------- 1 | queryArray($sql, array('id' => $session_id)); 42 | if ($results) { 43 | $life_time = AtomCode::$config['session']['lifetime']; 44 | if (!$life_time) $life_time = self::DEFAULT_LIFETIME; 45 | 46 | if ($results[0]['last_activity'] > time() - $life_time) { 47 | $session = $results[0]['user_data']; 48 | self::$exists = true; 49 | } else { 50 | $this->destroy($session_id); 51 | } 52 | } 53 | 54 | return $session; 55 | } 56 | 57 | public function write($session_id, $data) { 58 | $data = str_replace("'", "\\'", $data); 59 | $time = time(); 60 | $db = Database::get(AtomCode::$config['session']['db']); 61 | if (self::$exists) { 62 | $sql = "UPDATE sessions SET user_data= '$data', last_activity= '$time', user_id='" . self::$user_id . "' WHERE session_id = '$session_id'"; 63 | $query = $db->bind($sql, array()); 64 | $db->query($query); 65 | } else { 66 | $sql = "insert sessions(session_id, ip_address, user_agent, last_activity, user_data, user_id) VALUES ('$session_id', '" . get_ip() . "', '" . $_SERVER['HTTP_USER_AGENT'] . "', " . time() . ", '$data', '" . self::$user_id . "')"; 67 | 68 | $result = $db->query($sql); 69 | } 70 | return true; 71 | } 72 | 73 | public function destroy($session_id) { 74 | $sql = "DELETE FROM sessions WHERE session_id = :id"; 75 | 76 | self::$user_id = 0; 77 | 78 | $db = Database::get(AtomCode::$config['session']['db']); 79 | $db->query($sql, array('id' => $session_id)); 80 | return true; 81 | } 82 | 83 | public function gc($lifetime) { 84 | $sql = "DELETE FROM sessions WHERE last_activity < :time"; 85 | $db = Database::get(AtomCode::$config['session']['db']); 86 | 87 | $life_time = AtomCode::$config['session']['lifetime']; 88 | if (!$life_time) $life_time = self::DEFAULT_LIFETIME; 89 | 90 | $db->query($sql, array('time' => time() - $life_time)); 91 | return true; 92 | } 93 | 94 | public function createSid() { 95 | return md5(time() . mt_rand(1,3000000)); 96 | } 97 | } -------------------------------------------------------------------------------- /src/functions.php: -------------------------------------------------------------------------------- 1 | '), array('&', '"', '<', '>'), $string)); 21 | return $string; 22 | } 23 | function log_err($msg = '', $tag = '') { 24 | if (AtomCode::$config['log']) { 25 | if ($tag) $tag .= '-'; 26 | $file = AtomCode::$config['logdir'] . '/' . $tag . date("Y-m-d") . '.log'; 27 | if (!file_exists($file)) { 28 | touch($file); 29 | chmod($file, 0777); 30 | } 31 | file_put_contents($file, '[' . date("Y-m-d H:i:s") . '] [' . AtomCode::$route->getController() . '->' . AtomCode::$route->getAction() . "] " . $msg . "\r\n", FILE_APPEND); 32 | } 33 | } 34 | 35 | function _error_handler($level, $error, $file = '', $line = 0, $context = array()) { 36 | if (in_array($level, array(E_COMPILE_ERROR, E_CORE_ERROR, E_ERROR, E_PARSE, E_RECOVERABLE_ERROR, E_STRICT, E_USER_ERROR))) { 37 | log_err("$error in $file line $line. " . ($context ? print_r($context, true) : "")); 38 | } 39 | } 40 | 41 | /** 42 | * @param Exception $e 43 | */ 44 | function _exception_handler($e) { 45 | log_err($e->getTraceAsString()); 46 | } 47 | 48 | function get_ip() { 49 | if (getenv('HTTP_CLIENT_IP') && strcasecmp(getenv('HTTP_CLIENT_IP'), 'unknown')) { 50 | $onlineip = getenv('HTTP_CLIENT_IP'); 51 | } elseif (getenv('HTTP_X_FORWARDED_FOR') && strcasecmp(getenv('HTTP_X_FORWARDED_FOR'), 'unknown')) { 52 | $onlineip = getenv('HTTP_X_FORWARDED_FOR'); 53 | } elseif (getenv('REMOTE_ADDR') && strcasecmp(getenv('REMOTE_ADDR'), 'unknown')) { 54 | $onlineip = getenv('REMOTE_ADDR'); 55 | } elseif (isset($_SERVER['REMOTE_ADDR']) && $_SERVER['REMOTE_ADDR'] && strcasecmp($_SERVER['REMOTE_ADDR'], 'unknown')) { 56 | $onlineip = $_SERVER['REMOTE_ADDR']; 57 | } 58 | 59 | if ($onlineip == '::1') 60 | $onlineip = '127.0.0.1'; 61 | 62 | preg_match('/[\d\.]{7,15}/', $onlineip, $onlineipmatches); 63 | $onlineip = $onlineipmatches[0] ? $onlineipmatches[0] : 'unknown'; 64 | return $onlineip; 65 | } 66 | 67 | function path_join() { 68 | $args = func_get_args(); 69 | $has = ""; 70 | if ($args[0] && $args[0]{0} == '/') { 71 | $has = '/'; 72 | } 73 | 74 | foreach ($args as &$arg) { 75 | $arg = trim($arg, '/\\'); 76 | } 77 | 78 | return $has . implode("/", $args); 79 | } 80 | 81 | function startswith($string, $sub) { 82 | return strncmp($string, $sub, min(strlen($sub), strlen($string))) === 0; 83 | } 84 | 85 | function rrmdir($dir) { 86 | foreach (glob($dir . '/*') as $file) { 87 | if (is_dir($file)) 88 | rrmdir($file); 89 | else 90 | unlink($file); 91 | } 92 | rmdir($dir); 93 | } 94 | 95 | function is_cli() { 96 | return defined("STDIN"); 97 | } 98 | 99 | function is_ajax() { 100 | return isset($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) == 'xmlhttprequest' || (isset($_SERVER['HTTP_ORIGIN']) && $_SERVER['HTTP_ORIGIN'] && $_GET['jsonp']); 101 | } 102 | 103 | function is_post() { 104 | return strtolower($_SERVER['REQUEST_METHOD']) == 'post'; 105 | } 106 | 107 | function is_get() { 108 | return strtolower($_SERVER['REQUEST_METHOD']) == 'get'; 109 | } 110 | 111 | function is_https() { 112 | return $_SERVER['SERVER_PORT'] == 443; 113 | } 114 | function get_device_type(){ 115 | $agent = strtolower($_SERVER['HTTP_USER_AGENT']); 116 | $type = 'other'; 117 | if(strpos($agent, 'iphone')){ 118 | $type = 'iphone'; 119 | } 120 | if(strpos($agent, 'ipad')){ 121 | $type = 'ipad'; 122 | } 123 | if(strpos($agent, 'android')){ 124 | $type = 'android'; 125 | } 126 | return $type; 127 | } 128 | 129 | function is_android() { 130 | return get_device_type() == "android"; 131 | } 132 | 133 | function is_iphone() { 134 | return get_device_type() == "iphone"; 135 | } 136 | 137 | function is_ipad() { 138 | return get_device_type() == "ipad"; 139 | } 140 | 141 | function is_ios() { 142 | return in_array(get_device_type(), array("ipad", "iphone")); 143 | } 144 | 145 | function is_mobile() { 146 | return in_array(get_device_type(), array("ipad", "iphone", "android")); 147 | } 148 | 149 | function url($action) { 150 | return rtrim(AtomCode::$config['route']['base'], ' /') . ltrim($action, '/ '); 151 | } 152 | -------------------------------------------------------------------------------- /src/Database.php: -------------------------------------------------------------------------------- 1 | log_sql = isset($config['log']) && $config['log']; 42 | try { 43 | self::$links[$specify_db] = new PDO($dsn, $config['username'], $config['password']); 44 | $this->link = &self::$links[$specify_db]; 45 | 46 | $stmt = $this->link->prepare('SET NAMES ?'); 47 | if (!$stmt->execute(array($config['char_set']))) { 48 | $this->messages[] = "unsupport charset: " . $config['char_set'] . "\nPDO: " . var_export($stmt->errorInfo(), true); 49 | 50 | log_err("unsupport charset: " . $config['char_set']); 51 | log_err("PDO: " . var_export($stmt->errorInfo(), true)); 52 | } 53 | } catch (PDOException $e) { 54 | log_err("cannot connect to db: $dsn, user: $config[username], error: " . $e->getMessage()); 55 | $this->messages[] = "cannot connect to db: $dsn, user: $config[username], error: " . $e->getMessage(); 56 | if (ENVIRONMENT != 'production') { 57 | throw $e; 58 | } 59 | } 60 | } 61 | 62 | /** 63 | * @return PDOStatement 64 | */ 65 | public function bind($sql, $binding) { 66 | $this->_binding = &$binding; 67 | $sql = preg_replace_callback("/::(\\w+)/", array($this, 'repl_origen'), $sql); 68 | 69 | if ($this->_unbinding) { 70 | foreach ($this->_unbinding as $un) { 71 | if (array_key_exists($un, $this->_binding)) { 72 | unset($this->_binding[$un]); 73 | } 74 | } 75 | } 76 | 77 | $stmt = $this->link->prepare($sql); 78 | 79 | foreach ($binding as $k => $_) { 80 | $stmt->bindParam(':' . $k, $binding[$k]); 81 | } 82 | 83 | return $stmt; 84 | } 85 | 86 | private function repl_origen($matches) { 87 | if (!isset($this->_binding[$matches[1]])) { 88 | $this->messages[] = "binding value to param fail, $matches[1] has not been set."; 89 | return ""; 90 | } 91 | 92 | $val = $this->_binding[$matches[1]]; 93 | $this->_unbinding[] = $matches[1]; 94 | 95 | return $val; 96 | } 97 | 98 | /** 99 | * @param PDOStatement $stmt 100 | * @return boolean 101 | */ 102 | public function query($stmt, $binding = array()) { 103 | if (is_string($stmt)) { 104 | $stmt = $this->bind($stmt, $binding); 105 | } 106 | 107 | if ($this->log_sql) { 108 | log_err("query sql: " . $stmt->queryString); 109 | } 110 | 111 | if (!$stmt->execute()) { 112 | log_err("fail sql: " . $stmt->queryString); 113 | $error = $stmt->errorInfo(); 114 | $this->messages[] = "query error: " . print_r($error, true); 115 | log_err("query error: " . print_r($error, true)); 116 | return false; 117 | } 118 | 119 | $this->statement = $stmt; 120 | return $stmt; 121 | } 122 | 123 | /** 124 | * @param PDOStatement|String $stmt 125 | * @return boolean 126 | */ 127 | public function queryArray($stmt, $binding = array()) { 128 | $res = $this->query($stmt, $binding); 129 | 130 | if ($res) { 131 | return $res->fetchAll(PDO::FETCH_ASSOC); 132 | } else { 133 | return array(); 134 | } 135 | } 136 | 137 | function queryRow($sql, $binding = array()) { 138 | $array = call_user_func_array(array($this, 'queryArray'), func_get_args()); 139 | 140 | if (!$array) { 141 | return array(); 142 | } else { 143 | return $array[0]; 144 | } 145 | } 146 | 147 | public function getErrors() { 148 | return $this->messages; 149 | } 150 | 151 | public function affectRows() { 152 | return $this->statement->rowCount(); 153 | } 154 | 155 | public function lastInsertId() { 156 | return $this->link->lastInsertId(); 157 | } 158 | 159 | public function beginTransaction($mode = PDO::ERRMODE_WARNING) { 160 | $this->link->setAttribute(PDO::ATTR_ERRMODE, $mode); 161 | return $this->link->beginTransaction(); 162 | } 163 | 164 | public function commit() { 165 | return $this->link->commit(); 166 | } 167 | 168 | public function rollback() { 169 | return $this->link->rollBack(); 170 | } 171 | } -------------------------------------------------------------------------------- /src/Core.php: -------------------------------------------------------------------------------- 1 | getControllerClass(); 49 | 50 | if (!class_exists($controller_class, true)) { 51 | exit("Controller: $controller_class does not exist"); 52 | } 53 | 54 | // why place the allow origin here? controller class may crash when running. 55 | if (self::$config['view']['ajax-origen'] && $_SERVER['HTTP_ORIGIN'] && preg_match(self::$config['view']['ajax-origen'], $_SERVER['HTTP_ORIGIN'])) { 56 | header('Access-Control-Allow-Origin: ' . $_SERVER['HTTP_ORIGIN']); 57 | } 58 | 59 | $controller = new $controller_class(); 60 | $controller->config = &self::$config; 61 | $action_str = self::$route->getAction(); 62 | 63 | $src_action = $action_str; 64 | $action_str = $controller->_resolve($action_str); 65 | 66 | self::$route->setAction($action_str); 67 | $action = self::$route->getActionName(); 68 | 69 | if (!method_exists($controller, $action)) { 70 | exit("Action: $controller_class does not have method `$action_str`" . ($action == $src_action ? "" : ", origen action is $src_action")); 71 | } 72 | $result = $controller->$action(); 73 | if (!$result) { 74 | $result = $controller->getData(); 75 | } 76 | 77 | if (!$controller->isDisabledRender()) { 78 | $view = $controller->getView(); 79 | if (!$view) { 80 | $view = self::$route->getController() . DIRECTORY_SEPARATOR . self::$route->getAction(); 81 | } 82 | 83 | $render = $controller->getRender(); 84 | if (!$render) { 85 | $render = self::decideRender(); 86 | } 87 | 88 | View::render($render, $view, $result); 89 | } 90 | } 91 | 92 | public static function addConfig($file, $prior = true) { 93 | $config = array(); 94 | $dir = ENVIRONMENT ? ENVIRONMENT . '/' : ''; 95 | 96 | if ($file{0} == '/' || $file{1} == ":") { 97 | include $file; 98 | } elseif ($file{0} == '.') { 99 | include APP_PATH . '/' . $file; 100 | } else { 101 | if (file_exists(APP_PATH . '/config/' . $file . '.php')) { 102 | include APP_PATH . '/config/' . $file . '.php'; 103 | } 104 | if ($dir && file_exists(APP_PATH . '/config/' . $dir . $file . '.php')) { 105 | include APP_PATH . '/config/' . $dir . $file . '.php'; 106 | } 107 | } 108 | 109 | if (!$config || !is_array($config)) { 110 | return array(); 111 | } 112 | 113 | if ($prior) { 114 | self::$config = array_merge(self::$config, $config); 115 | } else { 116 | self::$config = array_merge($config, self::$config); 117 | } 118 | 119 | return self::$config[$file]; 120 | } 121 | 122 | private static function registerAutoload() { 123 | spl_autoload_register("__atomcode_autoload"); 124 | } 125 | 126 | public static function registerAutoloadDir($dir) { 127 | set_include_path(get_include_path() . PATH_SEPARATOR . $dir); 128 | } 129 | 130 | public static function registerAutoloadDirs($dirs) { 131 | set_include_path(get_include_path() . PATH_SEPARATOR . implode(PATH_SEPARATOR, $dirs)); 132 | } 133 | 134 | public static function decideRender() { 135 | if (AtomCode::$config['view']['renderer'] && is_callable(AtomCode::$config['view']['renderer'])) { 136 | return call_user_func(AtomCode::$config['view']['renderer']); 137 | } 138 | if (is_cli()) { 139 | return 'yaml'; 140 | } elseif (is_ajax()) { 141 | return 'json'; 142 | } else { 143 | return 'html'; 144 | } 145 | } 146 | 147 | private static function assocSession() { 148 | session_set_save_handler( 149 | array(self::$session, 'open'), 150 | array(self::$session, 'close'), 151 | array(self::$session, 'read'), 152 | array(self::$session, 'write'), 153 | array(self::$session, 'destroy'), 154 | array(self::$session, 'gc') 155 | ); 156 | } 157 | } 158 | 159 | function __atomcode_autoload($class) { 160 | $class = str_replace('\\', DIRECTORY_SEPARATOR, $class); 161 | 162 | include $class . '.php'; 163 | } 164 | abstract class Route { 165 | 166 | public $config; 167 | 168 | protected $module = ""; 169 | 170 | protected $controller = ""; 171 | 172 | protected $action = ""; 173 | 174 | protected $path = ""; 175 | 176 | public function __construct() { 177 | AtomCode::addConfig('route'); 178 | 179 | $this->config = AtomCode::$config['route']; 180 | 181 | $this->controller = $this->config['default_controller']; 182 | $this->action = $this->config['default_action']; 183 | } 184 | 185 | protected function parsePath($path) { 186 | $this->path = $this->resolve($path); 187 | $path = $this->path; 188 | 189 | if ($path) { 190 | $long_route = $path . '/' . $this->config['default_controller']; 191 | $long_class = $this->getControllerClass($long_route); 192 | $long_path = $this->getControllerFile($long_class); 193 | if ($this->config['default_controller'] && file_exists($long_path)) { 194 | $this->controller = $long_route; 195 | return ; 196 | } 197 | 198 | $ps = explode('/', $path); 199 | 200 | if (count($ps) == 1 || file_exists($this->getControllerFile($this->getControllerClass($path)))) { 201 | $this->controller = $path; 202 | } elseif ($path2 = dirname($path)) { 203 | $this->controller = $path2; 204 | $this->action = substr($path, strlen($path2) + 1); 205 | } 206 | } 207 | } 208 | 209 | /** 210 | * @param $path 解析此 URL 211 | * @return string 最终 url 212 | */ 213 | public function resolve($path) { 214 | $path = trim($path, ' /'); 215 | if (strpos($path, '..') !== false) exit("DANGER!"); 216 | 217 | if (is_array($this->config['replace']))foreach ($this->config['replace'] as $reg => $replace) { 218 | $path = preg_replace('/^' . $reg . '$/', $replace, $path); 219 | } 220 | 221 | return $path; 222 | } 223 | 224 | public function getController() { 225 | return $this->controller; 226 | } 227 | 228 | public function getControllerClass($name = '') { 229 | $ps = explode("/", $name ?: $this->controller); 230 | $class = array_pop($ps); 231 | $dir = $ps ? implode("\\", $ps) . '\\' : ''; 232 | return 'controller\\' . $dir . str_replace(' ','', ucwords(str_replace(array('-', '_'), ' ', $class))) . 'Controller'; 233 | } 234 | 235 | public function getControllerFile($class) { 236 | return APP_PATH . '/' . str_replace('\\', '/', $class) . '.php'; 237 | } 238 | 239 | public function getAction() { 240 | return $this->action; 241 | } 242 | 243 | public function getActionName() { 244 | return str_replace(" ", '', ucwords(str_replace(array('-', '_'), ' ', $this->action))). 'Action'; 245 | } 246 | 247 | public function setAction($ac) { 248 | $this->action = $ac; 249 | } 250 | 251 | public function getPath() { 252 | return $this->path; 253 | } 254 | } -------------------------------------------------------------------------------- /src/Model.php: -------------------------------------------------------------------------------- 1 | _table = $this->getTableName(); 23 | if (!isset(AtomCode::$config['database'])) { 24 | AtomCode::addConfig("database"); 25 | } 26 | 27 | $this->_config = & AtomCode::$config['database'][$this->getDatabaseGroup()]; 28 | 29 | $this->_db = & Database::get($this->_database); 30 | 31 | $this->reset(); 32 | } 33 | 34 | public function rules() { 35 | return []; 36 | } 37 | 38 | public function getCriteria() { 39 | return clone $this->_criteria; 40 | } 41 | 42 | public function setCriteria($criteria) { 43 | $this->_criteria = clone $criteria; 44 | } 45 | 46 | public function offsetSet($offset, $value) { 47 | if (is_null($offset)) { 48 | $this->$offset = $value; 49 | } else { 50 | $this->$offset = $value; 51 | } 52 | } 53 | public function offsetExists($offset) { 54 | return isset($this->$offset); 55 | } 56 | public function offsetUnset($offset) { 57 | unset($this->$offset); 58 | } 59 | public function offsetGet($offset) { 60 | return isset($this->$offset) ? $this->$offset : null; 61 | } 62 | 63 | public function __set($name, $value) { 64 | if ($name{0} != "_") { 65 | return ; 66 | } 67 | 68 | $this->{$name} = $value; 69 | } 70 | 71 | public function getDatabaseGroup() { 72 | if ($this->_database) return $this->_database; 73 | 74 | $class_name = get_called_class(); 75 | $ps = explode("\\", $class_name); 76 | if (count($ps) == 3) { 77 | $this->_database = $ps[1]; 78 | } else { 79 | $this->_database = 'default'; 80 | } 81 | 82 | return $this->_database; 83 | } 84 | 85 | public function getTableName(){ 86 | $class = get_class($this); 87 | $ps = explode('\\', $class); 88 | $class = end($ps); 89 | $name = substr($class, 0, -5); 90 | $name = preg_replace("/([A-Z])/", "_$1", $name); 91 | return strtolower(substr($name, 1)); 92 | } 93 | 94 | public function getUsingTable() { 95 | return $this->_config['dbprefix'] . $this->_table; 96 | } 97 | 98 | public function select($columns = '*') { 99 | $this->_criteria->select = $columns; 100 | } 101 | 102 | public function from($table) { 103 | $this->_criteria->from = $table; 104 | } 105 | 106 | public function alias($name) { 107 | $this->_criteria->from = $this->getUsingTable() . ' AS ' . $name; 108 | } 109 | 110 | public function where($where, $binding = array()) { 111 | if (is_array($where)) { 112 | foreach ($where as $col => $val) { 113 | $this->where("$col = :$col"); 114 | } 115 | 116 | $this->bind($where); 117 | } else { 118 | if (!is_array($binding) && !is_null($binding) && preg_match("/^[\\w\\.`]+$/", $where)) { 119 | $param = "param_" . ($this->_criteria->counter ++); 120 | $binding2[$param] = $binding; 121 | $binding = $binding2; 122 | 123 | $where .= " = :" . $param; 124 | } 125 | 126 | $this->_criteria->where[] = $where; 127 | if ($binding) { 128 | $this->bind($binding); 129 | } 130 | } 131 | } 132 | 133 | /** 134 | * 对于分页查询后仍然需要查询总数的情况,需要首先调用本方法 135 | */ 136 | public function prepareFoundRows() { 137 | $this->_criteria->found_rows = true; 138 | } 139 | 140 | public function orderBy($order) { 141 | $this->_criteria->order = $order; 142 | } 143 | 144 | public function having($having, $binding = array()) { 145 | $this->bind($binding); 146 | $this->_criteria->having[] = $having; 147 | } 148 | 149 | public function groupBy($group) { 150 | $this->_criteria->groupBy = $group; 151 | } 152 | 153 | public function limit($limit, $offset = null) { 154 | $this->_criteria->limit = ($offset ? $offset . ',' : '') . $limit; 155 | } 156 | 157 | public function data($key, $val = false) { 158 | if (is_array($key)) { 159 | $this->inflat($key); 160 | } else { 161 | if ($key{0} != '_' && property_exists($this, $key)) { 162 | $this->{$key} = $val; 163 | } 164 | } 165 | } 166 | 167 | public function inflat($values) { 168 | foreach ($values as $k => $v) { 169 | if ($k{0} != '_' && property_exists($this, $k)) { 170 | $this->{$k} = $v; 171 | } 172 | } 173 | } 174 | 175 | public function find($binding = array()) { 176 | $this->bind($binding); 177 | 178 | $this->_last_query = $this->buildSelectSql(); 179 | $result = $this->_db->queryArray($this->_last_query, $this->_criteria->binding); 180 | 181 | $this->reset(); 182 | return $result; 183 | } 184 | 185 | /** 186 | * 查询出一个来 187 | * @param bool $wrap 188 | * @return static 189 | */ 190 | public function findOne($wrap = false) { 191 | $this->limit(1); 192 | $result = $this->find(); 193 | 194 | if ($result) { 195 | if ($wrap) { 196 | return $this->construct($result[0]); 197 | } else { 198 | return $result[0]; 199 | } 200 | } else { 201 | return null; 202 | } 203 | } 204 | 205 | private function construct($v) { 206 | $c = get_class($this); 207 | $t = new $c(); 208 | $t->inflat($v); 209 | 210 | return $t; 211 | } 212 | 213 | public function insertUpdate($data) { 214 | $this->_last_query = $this->buildInsertUpdateSql($data); 215 | $result = $this->_db->query($this->_last_query, $this->_criteria->binding); 216 | 217 | $this->reset(); 218 | return $result; 219 | } 220 | 221 | public function clean() { 222 | foreach (get_object_vars($this) as $var => $val) { 223 | if ($var{0} != "_") { 224 | $this->{$var} = null; 225 | } 226 | } 227 | } 228 | 229 | public function insertSelect($cols = '*') { 230 | $this->_last_query = $this->buildInsertSelectSql($cols); 231 | } 232 | 233 | /** 234 | * @param array $array 235 | * @return boolean 236 | */ 237 | public function insertBatch($array, $ignore = false) { 238 | $this->_last_query = $this->buildInsertBatchSql($array, $ignore); 239 | $result = $this->_db->query($this->_last_query, $this->_criteria->binding); 240 | 241 | $this->reset(); 242 | return $result; 243 | 244 | } 245 | 246 | public function leftJoin($table, $cond) { 247 | $this->_criteria->join[] = array("LEFT", $table, $cond); 248 | } 249 | 250 | public function rightJoin($table, $cond) { 251 | $this->_criteria->join[] = array("RIGHT", $table, $cond); 252 | } 253 | 254 | public function innerJoin($table, $cond) { 255 | $this->_criteria->join[] = array("INNER", $table, $cond); 256 | } 257 | 258 | public function outerJoin($table, $cond) { 259 | $this->_criteria->join[] = array("OUTER", $table, $cond); 260 | } 261 | 262 | /** 263 | * @param $id 264 | * @return static 265 | */ 266 | public function get($id) { 267 | $this->where($this->_primary . ' = :' . $this->_primary, array($this->_primary => $id)); 268 | $this->limit(1); 269 | $rows = $this->find(); 270 | 271 | return $rows ? $this->construct($rows[0]) : null; 272 | } 273 | 274 | public function insert($data = array(), $ignore = false) { 275 | $this->_last_query = $this->buildInsertSql($data ? $data : $this->value(), $ignore); 276 | $result = $this->_db->query($this->_last_query, $this->_criteria->binding); 277 | 278 | $this->reset(); 279 | if (property_exists($this, $this->_primary) && !$this->{$this->_primary}) { 280 | $this->{$this->_primary} = $this->lastInsertId(); 281 | } 282 | 283 | return $result; 284 | } 285 | 286 | public function update($data = array()) { 287 | $this->_last_query = $this->buildUpdateSql($data ? $data : $this->value()); 288 | $result = $this->_db->query($this->_last_query, $this->_criteria->binding); 289 | 290 | $this->reset(); 291 | return $result; 292 | } 293 | 294 | public function delete($binding = array()) { 295 | $this->bind($binding); 296 | 297 | if ($this->{$this->_primary}) { 298 | $this->where("{$this->_primary}=:primary_key", array('primary_key' => $this->{$this->_primary})); 299 | } 300 | 301 | $this->_last_query = $this->buildDeleteSql(); 302 | $result = $this->_db->query($this->_last_query, $this->_criteria->binding); 303 | 304 | $this->reset(); 305 | return $result; 306 | } 307 | 308 | public function save() { 309 | $data = $this->value(); 310 | 311 | if ($this->{$this->_primary}) { 312 | $this->where("`{$this->_primary}`=:primary_key", array('primary_key' => $this->{$this->_primary})); 313 | } 314 | 315 | $result = !!$this->insertUpdate($data); 316 | $this->reset(); 317 | if (property_exists($this, $this->_primary) && !$this->{$this->_primary}) { 318 | $this->{$this->_primary} = $this->lastInsertId(); 319 | } 320 | 321 | return $result; 322 | } 323 | 324 | public function lock($cond = '') { 325 | if (!$cond) { 326 | $cond = $this->getUsingTable() . " READ"; 327 | } 328 | $this->_last_query = $this->buildLockSql($cond); 329 | $result = $this->_db->query($this->_last_query); 330 | return $result; 331 | } 332 | 333 | public function unlock() { 334 | $this->_last_query = $this->buildUnlockSql(); 335 | $result = $this->_db->query($this->_last_query); 336 | return $result; 337 | } 338 | 339 | public function bind($key, $value = null) { 340 | if (!$key) return ; 341 | 342 | if (is_array($key)) { 343 | foreach ($key as $k => $v) { 344 | if (is_array($v)) { 345 | $v = $this->_getValueArray($v); 346 | } 347 | 348 | $this->_criteria->binding[$k] = $v; 349 | } 350 | } else { 351 | if (is_array($value)) { 352 | $value = $this->_getValueArray($value); 353 | } 354 | 355 | $this->_criteria->binding[$key] = $value; 356 | } 357 | } 358 | 359 | /** 360 | * 361 | * @param array $arr 362 | */ 363 | private function _getValueArray($arr) { 364 | return implode(", ", $this->quote($arr)); 365 | } 366 | 367 | public function query($sql, $binding = array()) { 368 | $this->bind($binding); 369 | $this->_last_query = $sql; 370 | $result = $this->_db->query($this->_last_query, $this->_criteria->binding); 371 | 372 | $this->reset(); 373 | return $result; 374 | } 375 | 376 | public function queryArray($sql, $binding = array()) { 377 | $this->bind($binding); 378 | $this->_last_query = $sql; 379 | $result = $this->_db->queryArray($this->_last_query, $this->_criteria->binding); 380 | 381 | $this->reset(); 382 | return $result; 383 | } 384 | 385 | public function buildSelectSql() { 386 | return $this->partSelectSql() . $this->partFromSql() . $this->partJoinSql() 387 | . $this->partWhereSql() . $this->partGroupSql() . $this->partHavingSql() 388 | . $this->partOrderSql() . $this->partLimitSql() . $this->partForUpdateSql(); 389 | } 390 | 391 | public function buildInsertSelectSql($cols = '*') { 392 | if ($cols == '*') { 393 | $cols_sql = ""; 394 | } else { 395 | $cols_sql = "($cols)"; 396 | } 397 | } 398 | 399 | public function buildInsertUpdateSql($data) { 400 | if (isset($data[$this->_primary])) { 401 | unset($data[$this->_primary]); 402 | } 403 | return $this->buildInsertSql($this->value()) . ' ON DUPLICATE KEY UPDATE ' . $this->partUpdateSql($data); 404 | } 405 | 406 | public function buildInsertBatchUpdateSql($array, $data) { 407 | return $this->buildInsertBatchSql($array) . ' ON DUPLICATE KEY UPDATE ' . $this->partUpdateSql($data); 408 | } 409 | 410 | public function buildInsertSql($data, $ignore = false) { 411 | foreach ($data as $k => $v) { 412 | if (is_null($v)) { 413 | unset($data[$k]); 414 | } 415 | } 416 | $cols = array_keys($data); 417 | $cols = $this->quoteKey($cols); 418 | 419 | return 'INSERT' . ($ignore ? ' IGNORE' : '') . ' INTO ' . $this->getUsingTable() . ' (' . implode(', ', $cols) . ') VALUES (' . implode(', ', $this->quote($data)) . ')'; 420 | } 421 | 422 | public function buildInsertBatchSql($array, $ignore = false) { 423 | if (!count($array)) { 424 | throw new Exception("nothing to be inserted!"); 425 | } 426 | 427 | $values = array(); 428 | // build data array according to the first element. 429 | // so, we detect the first element's values 430 | $cols = array(); 431 | $col_in_key = array(); 432 | foreach ($array[0] as $k => $v) { 433 | if (is_null($v)) { 434 | unset($array[0][$k]); 435 | } 436 | 437 | $cols[] = $k; 438 | } 439 | $this->quoteKey($cols); 440 | 441 | // now we apply a filter for strip uneffective keys in the array 442 | foreach ($array as $data) { 443 | $data2 = array(); 444 | foreach ($cols as $k) { 445 | $data2[$k] = $data[$k]; 446 | } 447 | 448 | $values[] = '(' . implode(', ', $this->quote($data2)) . ')'; 449 | } 450 | 451 | return 'INSERT' . ($ignore ? ' IGNORE' : '') . ' INTO ' . $this->getUsingTable() . ' (' . implode(', ', $cols) . ') VALUES ' . implode(", ", $values); 452 | 453 | } 454 | 455 | public function buildUpdateSql($data) { 456 | $data = $data ? $data : $this->value(); 457 | 458 | return 'UPDATE ' . $this->partFromSql(false) . $this->partJoinSql() . ' SET ' . $this->partUpdateSql($data) . $this->partWhereSql() . $this->partOrderSql() . $this->partLimitSql(); 459 | } 460 | 461 | public function buildDeleteSql() { 462 | return 'DELETE ' . $this->partFromSql() . $this->partWhereSql() . $this->partOrderSql() . $this->partLimitSql(); 463 | } 464 | 465 | public function buildLockSql($read) { 466 | $sql = 'LOCK TABLE ' . $read; 467 | return $sql; 468 | } 469 | 470 | public function buildUnlockSql() { 471 | return 'UNLOCK TABLES'; 472 | } 473 | 474 | private function partSelectSql() { 475 | return 'SELECT ' . ($this->_criteria->found_rows ? 'SQL_CALC_FOUND_ROWS ' : '') . ($this->_criteria->select ? $this->_criteria->select : implode(', ', $this->quoteKey($this->getTableColumns()))); 476 | } 477 | 478 | private function partFromSql($add_from = true) { 479 | return ($add_from ? ' FROM ' : ' ') . ($this->_criteria->from ? $this->_criteria->from : $this->getUsingTable()); 480 | } 481 | 482 | private function partJoinSql() { 483 | $join_str = array(); 484 | foreach ($this->_criteria->join as $row) { 485 | if (count($row) == 3) { 486 | $join_str[] = $row[0] . " JOIN " . $row[1] . ($row[2] ? " ON " . $row[2] : ""); 487 | } 488 | } 489 | 490 | return ' ' . implode(" ", $join_str); 491 | } 492 | 493 | private function partWhereSql() { 494 | if ($this->_criteria->where) { 495 | if (count($this->_criteria->where) > 1) { 496 | return ' WHERE (' . implode(') AND (', $this->_criteria->where) . ')'; 497 | } else { 498 | return ' WHERE ' . implode(' AND ', $this->_criteria->where); 499 | } 500 | 501 | } else { 502 | return ''; 503 | } 504 | } 505 | 506 | private function partGroupSql() { 507 | if ($this->_criteria->groupBy) { 508 | return ' GROUP BY ' . $this->_criteria->groupBy; 509 | } else { 510 | return ''; 511 | } 512 | } 513 | 514 | private function partHavingSql() { 515 | if ($this->_criteria->having) { 516 | if (count($this->_criteria->having) > 1) { 517 | return ' HAVING (' . implode(') AND (', $this->_criteria->having) . ')'; 518 | } else { 519 | return ' HAVING ' . implode(' AND ', $this->_criteria->having); 520 | } 521 | } else { 522 | return ''; 523 | } 524 | } 525 | 526 | private function partOrderSql() { 527 | if ($this->_criteria->order) { 528 | return ' ORDER BY ' . $this->_criteria->order; 529 | } else { 530 | return ''; 531 | } 532 | } 533 | 534 | private function partLimitSql() { 535 | if ($this->_criteria->limit) { 536 | return ' LIMIT ' . $this->_criteria->limit; 537 | } else { 538 | return ''; 539 | } 540 | } 541 | 542 | private function partForUpdateSql() { 543 | if ($this->_criteria->for_update) { 544 | return ' For UPDATE'; 545 | } else { 546 | return ''; 547 | } 548 | } 549 | 550 | private function partUpdateSql($data) { 551 | $items = array(); 552 | foreach ($data as $k => $v) { 553 | if (is_null($v)) continue; 554 | 555 | if ($v === self::ORIGEN_VALUE) { 556 | $items[] = $k; 557 | } else { 558 | $k = $this->quoteKey($k); 559 | 560 | $items[] = $k . '=' . ($v == '?' ? $v : $this->quote($v)); 561 | } 562 | } 563 | 564 | return implode(', ', $items); 565 | } 566 | 567 | public function quote($data) { 568 | if (is_array($data)) { 569 | foreach ($data as &$d) { 570 | $d = addslashes($d); 571 | $d = "'$d'"; 572 | } 573 | } else { 574 | $data = addslashes($data); 575 | $data = "'$data'"; 576 | } 577 | 578 | return $data; 579 | } 580 | 581 | public function quoteKey($keys) { 582 | if (!is_array($keys)) { 583 | if (preg_match("/^\w+$/", $keys) && !is_numeric($keys{0})) { 584 | return '`' . $keys . '`'; 585 | } else { 586 | return $keys; 587 | } 588 | } 589 | 590 | if (array_key_exists(0, $keys)) { 591 | 592 | foreach ($keys as &$key) { 593 | $key = $this->quoteKey($key); 594 | } 595 | 596 | return $keys; 597 | } 598 | 599 | $new = array(); 600 | foreach ($keys as $key => $value) { 601 | $key = $this->quoteKey($key); 602 | 603 | $new[$key] = $value; 604 | } 605 | 606 | return $new; 607 | } 608 | 609 | public function value() { 610 | $c = array(); 611 | $ps = get_object_vars($this); 612 | foreach ($ps as $n => $_) { 613 | if ($n{0} != '_') { 614 | $c[$n] = $_; 615 | } 616 | } 617 | 618 | return $c; 619 | } 620 | 621 | protected function getTableColumns() { 622 | return array_keys($this->value()); 623 | } 624 | 625 | public function getMessages() { 626 | return $this->_db->getErrors(); 627 | } 628 | 629 | public function getLastQuery() { 630 | return $this->_last_query; 631 | } 632 | 633 | public function affectRows() { 634 | return $this->_db->affectRows(); 635 | } 636 | 637 | public function lastInsertId() { 638 | return $this->_db->lastInsertId(); 639 | } 640 | 641 | public function reset() { 642 | $this->_criteria = new Criteria(); 643 | } 644 | 645 | /** 646 | * 647 | * @return 查询 limit 语句中去掉 limit 语句的总条数 648 | */ 649 | public function foundRows() { 650 | $result = $this->_db->query("SELECT FOUND_ROWS() count"); 651 | $all = $result->fetchColumn(); 652 | 653 | return $all; 654 | } 655 | 656 | /** 657 | * @link Model::foundRows() 658 | */ 659 | public function getFoundRows() { 660 | return $this->foundRows(); 661 | } 662 | 663 | /** 664 | * 启动一次事务 665 | * 666 | * 如果之前已经启动了一次事务,则不会有任何反应。启动一次事务的本质是关闭自动提交事务模式。 667 | * 668 | * 如果启动了事务但中途中断执行而没有调用提交事务,则会自动回滚。 669 | * 670 | * 请不要同时使用 mysql_query("begin transaction") 或 $model->query("begin transaction"); 或其他方式启动事务。否则将在每个链接单独启用事务。 671 | * 672 | * @param int 错误类型 673 | * @return bool Returns true on success or false on failure. 674 | * @link http://www.php.net/manual/zh/pdo.begintransaction.php 675 | * @link http://php.net/manual/zh/pdo.constants.php#PDO::ERRMODE_SILENT 676 | */ 677 | public function beginTransaction($err_mode = PDO::ERRMODE_WARNING) { 678 | return $this->_db->beginTransaction($err_mode); 679 | } 680 | 681 | /** 682 | * 提交事务,务必在启用事务后提交,否则没有任何作用 683 | */ 684 | public function commit() { 685 | return $this->_db->commit(); 686 | } 687 | 688 | /** 689 | * 回滚事务 690 | */ 691 | public function rollback() { 692 | return $this->_db->rollback(); 693 | } 694 | 695 | public function forUpate($set = true) 696 | { 697 | $this->_criteria->for_update = $set ? true : false; 698 | } 699 | } -------------------------------------------------------------------------------- /src/Services_JSON.php: -------------------------------------------------------------------------------- 1 | 51 | * @author Matt Knapp 52 | * @author Brett Stimmerman 53 | * @copyright 2005 Michal Migurski 54 | * @version CVS: $Id: JSON.php,v 1.31 2006/06/28 05:54:17 migurski Exp $ 55 | * @license http://www.opensource.org/licenses/bsd-license.php 56 | * @link http://pear.php.net/pepr/pepr-proposal-show.php?id=198 57 | */ 58 | 59 | /** 60 | * Marker constant for Services_JSON::decode(), used to flag stack state 61 | */ 62 | define('SERVICES_JSON_SLICE', 1); 63 | 64 | /** 65 | * Marker constant for Services_JSON::decode(), used to flag stack state 66 | */ 67 | define('SERVICES_JSON_IN_STR', 2); 68 | 69 | /** 70 | * Marker constant for Services_JSON::decode(), used to flag stack state 71 | */ 72 | define('SERVICES_JSON_IN_ARR', 3); 73 | 74 | /** 75 | * Marker constant for Services_JSON::decode(), used to flag stack state 76 | */ 77 | define('SERVICES_JSON_IN_OBJ', 4); 78 | 79 | /** 80 | * Marker constant for Services_JSON::decode(), used to flag stack state 81 | */ 82 | define('SERVICES_JSON_IN_CMT', 5); 83 | 84 | /** 85 | * Behavior switch for Services_JSON::decode() 86 | */ 87 | define('SERVICES_JSON_LOOSE_TYPE', 16); 88 | 89 | /** 90 | * Behavior switch for Services_JSON::decode() 91 | */ 92 | define('SERVICES_JSON_SUPPRESS_ERRORS', 32); 93 | 94 | /** 95 | * Converts to and from JSON format. 96 | * 97 | * Brief example of use: 98 | * 99 | * 100 | * // create a new instance of Services_JSON 101 | * $json = new Services_JSON(); 102 | * 103 | * // convert a complexe value to JSON notation, and send it to the browser 104 | * $value = array('foo', 'bar', array(1, 2, 'baz'), array(3, array(4))); 105 | * $output = $json->encode($value); 106 | * 107 | * print($output); 108 | * // prints: ["foo","bar",[1,2,"baz"],[3,[4]]] 109 | * 110 | * // accept incoming POST data, assumed to be in JSON notation 111 | * $input = file_get_contents('php://input', 1000000); 112 | * $value = $json->decode($input); 113 | * 114 | */ 115 | class Services_JSON 116 | { 117 | /** 118 | * constructs a new JSON instance 119 | * 120 | * @param int $use object behavior flags; combine with boolean-OR 121 | * 122 | * possible values: 123 | * - SERVICES_JSON_LOOSE_TYPE: loose typing. 124 | * "{...}" syntax creates associative arrays 125 | * instead of objects in decode(). 126 | * - SERVICES_JSON_SUPPRESS_ERRORS: error suppression. 127 | * Values which can't be encoded (e.g. resources) 128 | * appear as NULL instead of throwing errors. 129 | * By default, a deeply-nested resource will 130 | * bubble up with an error, so all return values 131 | * from encode() should be checked with isError() 132 | */ 133 | function Services_JSON($use = 0) 134 | { 135 | $this->use = $use; 136 | } 137 | 138 | /** 139 | * convert a string from one UTF-16 char to one UTF-8 char 140 | * 141 | * Normally should be handled by mb_convert_encoding, but 142 | * provides a slower PHP-only method for installations 143 | * that lack the multibye string extension. 144 | * 145 | * @param string $utf16 UTF-16 character 146 | * @return string UTF-8 character 147 | * @access private 148 | */ 149 | function utf162utf8($utf16) 150 | { 151 | // oh please oh please oh please oh please oh please 152 | if(function_exists('mb_convert_encoding')) { 153 | return mb_convert_encoding($utf16, 'UTF-8', 'UTF-16'); 154 | } 155 | 156 | $bytes = (ord($utf16{0}) << 8) | ord($utf16{1}); 157 | 158 | switch(true) { 159 | case ((0x7F & $bytes) == $bytes): 160 | // this case should never be reached, because we are in ASCII range 161 | // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 162 | return chr(0x7F & $bytes); 163 | 164 | case (0x07FF & $bytes) == $bytes: 165 | // return a 2-byte UTF-8 character 166 | // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 167 | return chr(0xC0 | (($bytes >> 6) & 0x1F)) 168 | . chr(0x80 | ($bytes & 0x3F)); 169 | 170 | case (0xFFFF & $bytes) == $bytes: 171 | // return a 3-byte UTF-8 character 172 | // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 173 | return chr(0xE0 | (($bytes >> 12) & 0x0F)) 174 | . chr(0x80 | (($bytes >> 6) & 0x3F)) 175 | . chr(0x80 | ($bytes & 0x3F)); 176 | } 177 | 178 | // ignoring UTF-32 for now, sorry 179 | return ''; 180 | } 181 | 182 | /** 183 | * convert a string from one UTF-8 char to one UTF-16 char 184 | * 185 | * Normally should be handled by mb_convert_encoding, but 186 | * provides a slower PHP-only method for installations 187 | * that lack the multibye string extension. 188 | * 189 | * @param string $utf8 UTF-8 character 190 | * @return string UTF-16 character 191 | * @access private 192 | */ 193 | function utf82utf16($utf8) 194 | { 195 | // oh please oh please oh please oh please oh please 196 | if(function_exists('mb_convert_encoding')) { 197 | return mb_convert_encoding($utf8, 'UTF-16', 'UTF-8'); 198 | } 199 | 200 | switch(strlen($utf8)) { 201 | case 1: 202 | // this case should never be reached, because we are in ASCII range 203 | // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 204 | return $utf8; 205 | 206 | case 2: 207 | // return a UTF-16 character from a 2-byte UTF-8 char 208 | // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 209 | return chr(0x07 & (ord($utf8{0}) >> 2)) 210 | . chr((0xC0 & (ord($utf8{0}) << 6)) 211 | | (0x3F & ord($utf8{1}))); 212 | 213 | case 3: 214 | // return a UTF-16 character from a 3-byte UTF-8 char 215 | // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 216 | return chr((0xF0 & (ord($utf8{0}) << 4)) 217 | | (0x0F & (ord($utf8{1}) >> 2))) 218 | . chr((0xC0 & (ord($utf8{1}) << 6)) 219 | | (0x7F & ord($utf8{2}))); 220 | } 221 | 222 | // ignoring UTF-32 for now, sorry 223 | return ''; 224 | } 225 | 226 | /** 227 | * encodes an arbitrary variable into JSON format 228 | * 229 | * @param mixed $var any number, boolean, string, array, or object to be encoded. 230 | * see argument 1 to Services_JSON() above for array-parsing behavior. 231 | * if var is a strng, note that encode() always expects it 232 | * to be in ASCII or UTF-8 format! 233 | * 234 | * @return mixed JSON string representation of input var or an error if a problem occurs 235 | * @access public 236 | */ 237 | function encode($var) 238 | { 239 | switch (gettype($var)) { 240 | case 'boolean': 241 | return $var ? 'true' : 'false'; 242 | 243 | case 'NULL': 244 | return 'null'; 245 | 246 | case 'integer': 247 | return (int) $var; 248 | 249 | case 'double': 250 | case 'float': 251 | return (float) $var; 252 | 253 | case 'string': 254 | // STRINGS ARE EXPECTED TO BE IN ASCII OR UTF-8 FORMAT 255 | $ascii = ''; 256 | $strlen_var = strlen($var); 257 | 258 | /* 259 | * Iterate over every character in the string, 260 | * escaping with a slash or encoding to UTF-8 where necessary 261 | */ 262 | for ($c = 0; $c < $strlen_var; ++$c) { 263 | 264 | $ord_var_c = ord($var{$c}); 265 | 266 | switch (true) { 267 | case $ord_var_c == 0x08: 268 | $ascii .= '\b'; 269 | break; 270 | case $ord_var_c == 0x09: 271 | $ascii .= '\t'; 272 | break; 273 | case $ord_var_c == 0x0A: 274 | $ascii .= '\n'; 275 | break; 276 | case $ord_var_c == 0x0C: 277 | $ascii .= '\f'; 278 | break; 279 | case $ord_var_c == 0x0D: 280 | $ascii .= '\r'; 281 | break; 282 | 283 | case $ord_var_c == 0x22: 284 | case $ord_var_c == 0x2F: 285 | case $ord_var_c == 0x5C: 286 | // double quote, slash, slosh 287 | $ascii .= '\\'.$var{$c}; 288 | break; 289 | 290 | case (($ord_var_c >= 0x20) && ($ord_var_c <= 0x7F)): 291 | // characters U-00000000 - U-0000007F (same as ASCII) 292 | $ascii .= $var{$c}; 293 | break; 294 | 295 | case (($ord_var_c & 0xE0) == 0xC0): 296 | // characters U-00000080 - U-000007FF, mask 110----- 297 | // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 298 | $char = pack('C*', $ord_var_c, ord($var{$c + 1})); 299 | $c += 1; 300 | $utf16 = $this->utf82utf16($char); 301 | $ascii .= sprintf('\u%04s', bin2hex($utf16)); 302 | break; 303 | 304 | case (($ord_var_c & 0xF0) == 0xE0): 305 | // characters U-00000800 - U-0000FFFF, mask 1110---- 306 | // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 307 | $char = pack('C*', $ord_var_c, 308 | ord($var{$c + 1}), 309 | ord($var{$c + 2})); 310 | $c += 2; 311 | $utf16 = $this->utf82utf16($char); 312 | $ascii .= sprintf('\u%04s', bin2hex($utf16)); 313 | break; 314 | 315 | case (($ord_var_c & 0xF8) == 0xF0): 316 | // characters U-00010000 - U-001FFFFF, mask 11110--- 317 | // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 318 | $char = pack('C*', $ord_var_c, 319 | ord($var{$c + 1}), 320 | ord($var{$c + 2}), 321 | ord($var{$c + 3})); 322 | $c += 3; 323 | $utf16 = $this->utf82utf16($char); 324 | $ascii .= sprintf('\u%04s', bin2hex($utf16)); 325 | break; 326 | 327 | case (($ord_var_c & 0xFC) == 0xF8): 328 | // characters U-00200000 - U-03FFFFFF, mask 111110XX 329 | // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 330 | $char = pack('C*', $ord_var_c, 331 | ord($var{$c + 1}), 332 | ord($var{$c + 2}), 333 | ord($var{$c + 3}), 334 | ord($var{$c + 4})); 335 | $c += 4; 336 | $utf16 = $this->utf82utf16($char); 337 | $ascii .= sprintf('\u%04s', bin2hex($utf16)); 338 | break; 339 | 340 | case (($ord_var_c & 0xFE) == 0xFC): 341 | // characters U-04000000 - U-7FFFFFFF, mask 1111110X 342 | // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 343 | $char = pack('C*', $ord_var_c, 344 | ord($var{$c + 1}), 345 | ord($var{$c + 2}), 346 | ord($var{$c + 3}), 347 | ord($var{$c + 4}), 348 | ord($var{$c + 5})); 349 | $c += 5; 350 | $utf16 = $this->utf82utf16($char); 351 | $ascii .= sprintf('\u%04s', bin2hex($utf16)); 352 | break; 353 | } 354 | } 355 | 356 | return '"'.$ascii.'"'; 357 | 358 | case 'array': 359 | /* 360 | * As per JSON spec if any array key is not an integer 361 | * we must treat the the whole array as an object. We 362 | * also try to catch a sparsely populated associative 363 | * array with numeric keys here because some JS engines 364 | * will create an array with empty indexes up to 365 | * max_index which can cause memory issues and because 366 | * the keys, which may be relevant, will be remapped 367 | * otherwise. 368 | * 369 | * As per the ECMA and JSON specification an object may 370 | * have any string as a property. Unfortunately due to 371 | * a hole in the ECMA specification if the key is a 372 | * ECMA reserved word or starts with a digit the 373 | * parameter is only accessible using ECMAScript's 374 | * bracket notation. 375 | */ 376 | 377 | // treat as a JSON object 378 | if (is_array($var) && count($var) && (array_keys($var) !== range(0, sizeof($var) - 1))) { 379 | $properties = array_map(array($this, 'name_value'), 380 | array_keys($var), 381 | array_values($var)); 382 | 383 | foreach($properties as $property) { 384 | if(Services_JSON::isError($property)) { 385 | return $property; 386 | } 387 | } 388 | 389 | return '{' . join(',', $properties) . '}'; 390 | } 391 | 392 | // treat it like a regular array 393 | $elements = array_map(array($this, 'encode'), $var); 394 | 395 | foreach($elements as $element) { 396 | if(Services_JSON::isError($element)) { 397 | return $element; 398 | } 399 | } 400 | 401 | return '[' . join(',', $elements) . ']'; 402 | 403 | case 'object': 404 | $vars = get_object_vars($var); 405 | 406 | $properties = array_map(array($this, 'name_value'), 407 | array_keys($vars), 408 | array_values($vars)); 409 | 410 | foreach($properties as $property) { 411 | if(Services_JSON::isError($property)) { 412 | return $property; 413 | } 414 | } 415 | 416 | return '{' . join(',', $properties) . '}'; 417 | 418 | default: 419 | return ($this->use & SERVICES_JSON_SUPPRESS_ERRORS) 420 | ? 'null' 421 | : new Services_JSON_Error(gettype($var)." can not be encoded as JSON string"); 422 | } 423 | } 424 | 425 | /** 426 | * array-walking function for use in generating JSON-formatted name-value pairs 427 | * 428 | * @param string $name name of key to use 429 | * @param mixed $value reference to an array element to be encoded 430 | * 431 | * @return string JSON-formatted name-value pair, like '"name":value' 432 | * @access private 433 | */ 434 | function name_value($name, $value) 435 | { 436 | $encoded_value = $this->encode($value); 437 | 438 | if(Services_JSON::isError($encoded_value)) { 439 | return $encoded_value; 440 | } 441 | 442 | return $this->encode(strval($name)) . ':' . $encoded_value; 443 | } 444 | 445 | /** 446 | * reduce a string by removing leading and trailing comments and whitespace 447 | * 448 | * @param $str string string value to strip of comments and whitespace 449 | * 450 | * @return string string value stripped of comments and whitespace 451 | * @access private 452 | */ 453 | function reduce_string($str) 454 | { 455 | $str = preg_replace(array( 456 | 457 | // eliminate single line comments in '// ...' form 458 | '#^\s*//(.+)$#m', 459 | 460 | // eliminate multi-line comments in '/* ... */' form, at start of string 461 | '#^\s*/\*(.+)\*/#Us', 462 | 463 | // eliminate multi-line comments in '/* ... */' form, at end of string 464 | '#/\*(.+)\*/\s*$#Us' 465 | 466 | ), '', $str); 467 | 468 | // eliminate extraneous space 469 | return trim($str); 470 | } 471 | 472 | /** 473 | * decodes a JSON string into appropriate variable 474 | * 475 | * @param string $str JSON-formatted string 476 | * 477 | * @return mixed number, boolean, string, array, or object 478 | * corresponding to given JSON input string. 479 | * See argument 1 to Services_JSON() above for object-output behavior. 480 | * Note that decode() always returns strings 481 | * in ASCII or UTF-8 format! 482 | * @access public 483 | */ 484 | function decode($str) 485 | { 486 | $str = $this->reduce_string($str); 487 | 488 | switch (strtolower($str)) { 489 | case 'true': 490 | return true; 491 | 492 | case 'false': 493 | return false; 494 | 495 | case 'null': 496 | return null; 497 | 498 | default: 499 | $m = array(); 500 | 501 | if (is_numeric($str)) { 502 | // Lookie-loo, it's a number 503 | 504 | // This would work on its own, but I'm trying to be 505 | // good about returning integers where appropriate: 506 | // return (float)$str; 507 | 508 | // Return float or int, as appropriate 509 | return ((float)$str == (integer)$str) 510 | ? (integer)$str 511 | : (float)$str; 512 | 513 | } elseif (preg_match('/^("|\').*(\1)$/s', $str, $m) && $m[1] == $m[2]) { 514 | // STRINGS RETURNED IN UTF-8 FORMAT 515 | $delim = substr($str, 0, 1); 516 | $chrs = substr($str, 1, -1); 517 | $utf8 = ''; 518 | $strlen_chrs = strlen($chrs); 519 | 520 | for ($c = 0; $c < $strlen_chrs; ++$c) { 521 | 522 | $substr_chrs_c_2 = substr($chrs, $c, 2); 523 | $ord_chrs_c = ord($chrs{$c}); 524 | 525 | switch (true) { 526 | case $substr_chrs_c_2 == '\b': 527 | $utf8 .= chr(0x08); 528 | ++$c; 529 | break; 530 | case $substr_chrs_c_2 == '\t': 531 | $utf8 .= chr(0x09); 532 | ++$c; 533 | break; 534 | case $substr_chrs_c_2 == '\n': 535 | $utf8 .= chr(0x0A); 536 | ++$c; 537 | break; 538 | case $substr_chrs_c_2 == '\f': 539 | $utf8 .= chr(0x0C); 540 | ++$c; 541 | break; 542 | case $substr_chrs_c_2 == '\r': 543 | $utf8 .= chr(0x0D); 544 | ++$c; 545 | break; 546 | 547 | case $substr_chrs_c_2 == '\\"': 548 | case $substr_chrs_c_2 == '\\\'': 549 | case $substr_chrs_c_2 == '\\\\': 550 | case $substr_chrs_c_2 == '\\/': 551 | if (($delim == '"' && $substr_chrs_c_2 != '\\\'') || 552 | ($delim == "'" && $substr_chrs_c_2 != '\\"')) { 553 | $utf8 .= $chrs{++$c}; 554 | } 555 | break; 556 | 557 | case preg_match('/\\\u[0-9A-F]{4}/i', substr($chrs, $c, 6)): 558 | // single, escaped unicode character 559 | $utf16 = chr(hexdec(substr($chrs, ($c + 2), 2))) 560 | . chr(hexdec(substr($chrs, ($c + 4), 2))); 561 | $utf8 .= $this->utf162utf8($utf16); 562 | $c += 5; 563 | break; 564 | 565 | case ($ord_chrs_c >= 0x20) && ($ord_chrs_c <= 0x7F): 566 | $utf8 .= $chrs{$c}; 567 | break; 568 | 569 | case ($ord_chrs_c & 0xE0) == 0xC0: 570 | // characters U-00000080 - U-000007FF, mask 110----- 571 | //see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 572 | $utf8 .= substr($chrs, $c, 2); 573 | ++$c; 574 | break; 575 | 576 | case ($ord_chrs_c & 0xF0) == 0xE0: 577 | // characters U-00000800 - U-0000FFFF, mask 1110---- 578 | // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 579 | $utf8 .= substr($chrs, $c, 3); 580 | $c += 2; 581 | break; 582 | 583 | case ($ord_chrs_c & 0xF8) == 0xF0: 584 | // characters U-00010000 - U-001FFFFF, mask 11110--- 585 | // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 586 | $utf8 .= substr($chrs, $c, 4); 587 | $c += 3; 588 | break; 589 | 590 | case ($ord_chrs_c & 0xFC) == 0xF8: 591 | // characters U-00200000 - U-03FFFFFF, mask 111110XX 592 | // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 593 | $utf8 .= substr($chrs, $c, 5); 594 | $c += 4; 595 | break; 596 | 597 | case ($ord_chrs_c & 0xFE) == 0xFC: 598 | // characters U-04000000 - U-7FFFFFFF, mask 1111110X 599 | // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 600 | $utf8 .= substr($chrs, $c, 6); 601 | $c += 5; 602 | break; 603 | 604 | } 605 | 606 | } 607 | 608 | return $utf8; 609 | 610 | } elseif (preg_match('/^\[.*\]$/s', $str) || preg_match('/^\{.*\}$/s', $str)) { 611 | // array, or object notation 612 | 613 | if ($str{0} == '[') { 614 | $stk = array(SERVICES_JSON_IN_ARR); 615 | $arr = array(); 616 | } else { 617 | if ($this->use & SERVICES_JSON_LOOSE_TYPE) { 618 | $stk = array(SERVICES_JSON_IN_OBJ); 619 | $obj = array(); 620 | } else { 621 | $stk = array(SERVICES_JSON_IN_OBJ); 622 | $obj = new stdClass(); 623 | } 624 | } 625 | 626 | array_push($stk, array('what' => SERVICES_JSON_SLICE, 627 | 'where' => 0, 628 | 'delim' => false)); 629 | 630 | $chrs = substr($str, 1, -1); 631 | $chrs = $this->reduce_string($chrs); 632 | 633 | if ($chrs == '') { 634 | if (reset($stk) == SERVICES_JSON_IN_ARR) { 635 | return $arr; 636 | 637 | } else { 638 | return $obj; 639 | 640 | } 641 | } 642 | 643 | //print("\nparsing {$chrs}\n"); 644 | 645 | $strlen_chrs = strlen($chrs); 646 | 647 | for ($c = 0; $c <= $strlen_chrs; ++$c) { 648 | 649 | $top = end($stk); 650 | $substr_chrs_c_2 = substr($chrs, $c, 2); 651 | 652 | if (($c == $strlen_chrs) || (($chrs{$c} == ',') && ($top['what'] == SERVICES_JSON_SLICE))) { 653 | // found a comma that is not inside a string, array, etc., 654 | // OR we've reached the end of the character list 655 | $slice = substr($chrs, $top['where'], ($c - $top['where'])); 656 | array_push($stk, array('what' => SERVICES_JSON_SLICE, 'where' => ($c + 1), 'delim' => false)); 657 | //print("Found split at {$c}: ".substr($chrs, $top['where'], (1 + $c - $top['where']))."\n"); 658 | 659 | if (reset($stk) == SERVICES_JSON_IN_ARR) { 660 | // we are in an array, so just push an element onto the stack 661 | array_push($arr, $this->decode($slice)); 662 | 663 | } elseif (reset($stk) == SERVICES_JSON_IN_OBJ) { 664 | // we are in an object, so figure 665 | // out the property name and set an 666 | // element in an associative array, 667 | // for now 668 | $parts = array(); 669 | 670 | if (preg_match('/^\s*(["\'].*[^\\\]["\'])\s*:\s*(\S.*),?$/Uis', $slice, $parts)) { 671 | // "name":value pair 672 | $key = $this->decode($parts[1]); 673 | $val = $this->decode($parts[2]); 674 | 675 | if ($this->use & SERVICES_JSON_LOOSE_TYPE) { 676 | $obj[$key] = $val; 677 | } else { 678 | $obj->$key = $val; 679 | } 680 | } elseif (preg_match('/^\s*(\w+)\s*:\s*(\S.*),?$/Uis', $slice, $parts)) { 681 | // name:value pair, where name is unquoted 682 | $key = $parts[1]; 683 | $val = $this->decode($parts[2]); 684 | 685 | if ($this->use & SERVICES_JSON_LOOSE_TYPE) { 686 | $obj[$key] = $val; 687 | } else { 688 | $obj->$key = $val; 689 | } 690 | } 691 | 692 | } 693 | 694 | } elseif ((($chrs{$c} == '"') || ($chrs{$c} == "'")) && ($top['what'] != SERVICES_JSON_IN_STR)) { 695 | // found a quote, and we are not inside a string 696 | array_push($stk, array('what' => SERVICES_JSON_IN_STR, 'where' => $c, 'delim' => $chrs{$c})); 697 | //print("Found start of string at {$c}\n"); 698 | 699 | } elseif (($chrs{$c} == $top['delim']) && 700 | ($top['what'] == SERVICES_JSON_IN_STR) && 701 | ((strlen(substr($chrs, 0, $c)) - strlen(rtrim(substr($chrs, 0, $c), '\\'))) % 2 != 1)) { 702 | // found a quote, we're in a string, and it's not escaped 703 | // we know that it's not escaped becase there is _not_ an 704 | // odd number of backslashes at the end of the string so far 705 | array_pop($stk); 706 | //print("Found end of string at {$c}: ".substr($chrs, $top['where'], (1 + 1 + $c - $top['where']))."\n"); 707 | 708 | } elseif (($chrs{$c} == '[') && 709 | in_array($top['what'], array(SERVICES_JSON_SLICE, SERVICES_JSON_IN_ARR, SERVICES_JSON_IN_OBJ))) { 710 | // found a left-bracket, and we are in an array, object, or slice 711 | array_push($stk, array('what' => SERVICES_JSON_IN_ARR, 'where' => $c, 'delim' => false)); 712 | //print("Found start of array at {$c}\n"); 713 | 714 | } elseif (($chrs{$c} == ']') && ($top['what'] == SERVICES_JSON_IN_ARR)) { 715 | // found a right-bracket, and we're in an array 716 | array_pop($stk); 717 | //print("Found end of array at {$c}: ".substr($chrs, $top['where'], (1 + $c - $top['where']))."\n"); 718 | 719 | } elseif (($chrs{$c} == '{') && 720 | in_array($top['what'], array(SERVICES_JSON_SLICE, SERVICES_JSON_IN_ARR, SERVICES_JSON_IN_OBJ))) { 721 | // found a left-brace, and we are in an array, object, or slice 722 | array_push($stk, array('what' => SERVICES_JSON_IN_OBJ, 'where' => $c, 'delim' => false)); 723 | //print("Found start of object at {$c}\n"); 724 | 725 | } elseif (($chrs{$c} == '}') && ($top['what'] == SERVICES_JSON_IN_OBJ)) { 726 | // found a right-brace, and we're in an object 727 | array_pop($stk); 728 | //print("Found end of object at {$c}: ".substr($chrs, $top['where'], (1 + $c - $top['where']))."\n"); 729 | 730 | } elseif (($substr_chrs_c_2 == '/*') && 731 | in_array($top['what'], array(SERVICES_JSON_SLICE, SERVICES_JSON_IN_ARR, SERVICES_JSON_IN_OBJ))) { 732 | // found a comment start, and we are in an array, object, or slice 733 | array_push($stk, array('what' => SERVICES_JSON_IN_CMT, 'where' => $c, 'delim' => false)); 734 | $c++; 735 | //print("Found start of comment at {$c}\n"); 736 | 737 | } elseif (($substr_chrs_c_2 == '*/') && ($top['what'] == SERVICES_JSON_IN_CMT)) { 738 | // found a comment end, and we're in one now 739 | array_pop($stk); 740 | $c++; 741 | 742 | for ($i = $top['where']; $i <= $c; ++$i) 743 | $chrs = substr_replace($chrs, ' ', $i, 1); 744 | 745 | //print("Found end of comment at {$c}: ".substr($chrs, $top['where'], (1 + $c - $top['where']))."\n"); 746 | 747 | } 748 | 749 | } 750 | 751 | if (reset($stk) == SERVICES_JSON_IN_ARR) { 752 | return $arr; 753 | 754 | } elseif (reset($stk) == SERVICES_JSON_IN_OBJ) { 755 | return $obj; 756 | 757 | } 758 | 759 | } 760 | } 761 | } 762 | 763 | /** 764 | * @todo Ultimately, this should just call PEAR::isError() 765 | */ 766 | function isError($data, $code = null) 767 | { 768 | if (is_object($data) && (get_class($data) == 'services_json_error' || 769 | is_subclass_of($data, 'services_json_error'))) { 770 | return true; 771 | } 772 | 773 | return false; 774 | } 775 | } 776 | 777 | class Services_JSON_Error 778 | { 779 | function Services_JSON_Error($message = 'unknown error', $code = null, 780 | $mode = null, $options = null, $userinfo = null) 781 | { 782 | 783 | } 784 | } 785 | -------------------------------------------------------------------------------- /src/Spyc.php: -------------------------------------------------------------------------------- 1 | 6 | * @author Chris Wanstrath 7 | * @link https://github.com/mustangostang/spyc/ 8 | * @copyright Copyright 2005-2006 Chris Wanstrath, 2006-2011 Vlad Andersen 9 | * @license http://www.opensource.org/licenses/mit-license.php MIT License 10 | * @package Spyc 11 | */ 12 | if (!function_exists('spyc_load')) { 13 | 14 | /** 15 | * Parses YAML to array. 16 | * 17 | * @param string $string 18 | * YAML string. 19 | * @return array 20 | */ 21 | function spyc_load($string) { 22 | return Spyc::YAMLLoadString($string); 23 | } 24 | } 25 | if (!function_exists('spyc_load_file')) { 26 | 27 | /** 28 | * Parses YAML to array. 29 | * 30 | * @param string $file 31 | * Path to YAML file. 32 | * @return array 33 | */ 34 | function spyc_load_file($file) { 35 | return Spyc::YAMLLoad($file); 36 | } 37 | } 38 | if (!function_exists('spyc_dump')) { 39 | 40 | /** 41 | * Dumps array to YAML. 42 | * 43 | * @param array $data 44 | * Array. 45 | * @return string 46 | */ 47 | function spyc_dump($data) { 48 | return Spyc::YAMLDump($data, false, false, true); 49 | } 50 | } 51 | /** 52 | * The Simple PHP YAML Class. 53 | * 54 | * This class can be used to read a YAML file and convert its contents 55 | * into a PHP array. It currently supports a very limited subsection of 56 | * the YAML spec. 57 | * 58 | * Usage: 59 | * 60 | * $Spyc = new Spyc; 61 | * $array = $Spyc->load($file); 62 | * 63 | * or: 64 | * 65 | * $array = Spyc::YAMLLoad($file); 66 | * 67 | * or: 68 | * 69 | * $array = spyc_load_file($file); 70 | * 71 | * 72 | * @package Spyc 73 | */ 74 | class Spyc { 75 | // SETTINGS 76 | const REMPTY = "\0\0\0\0\0"; 77 | 78 | /** 79 | * Setting this to true will force YAMLDump to enclose any string value in 80 | * quotes. 81 | * False by default. 82 | * 83 | * @var bool 84 | */ 85 | public $setting_dump_force_quotes = false; 86 | 87 | /** 88 | * Setting this to true will forse YAMLLoad to use syck_load function when 89 | * possible. 90 | * False by default. 91 | * 92 | * @var bool 93 | */ 94 | public $setting_use_syck_is_possible = false; 95 | 96 | /** 97 | * #@+ 98 | * 99 | * @access private 100 | * @var mixed 101 | */ 102 | private $_dumpIndent; 103 | 104 | private $_dumpWordWrap; 105 | 106 | private $_containsGroupAnchor = false; 107 | 108 | private $_containsGroupAlias = false; 109 | 110 | private $path; 111 | 112 | private $result; 113 | 114 | private $LiteralPlaceHolder = '___YAML_Literal_Block___'; 115 | 116 | private $SavedGroups = array(); 117 | 118 | private $indent; 119 | 120 | /** 121 | * Path modifier that should be applied after adding current element. 122 | * 123 | * @var array 124 | */ 125 | private $delayedPath = array(); 126 | 127 | /** 128 | * #@+ 129 | * 130 | * @access public 131 | * @var mixed 132 | */ 133 | public $_nodeId; 134 | 135 | /** 136 | * Load a valid YAML string to Spyc. 137 | * 138 | * @param string $input 139 | * @return array 140 | */ 141 | public function load($input) { 142 | return $this->__loadString($input); 143 | } 144 | 145 | /** 146 | * Load a valid YAML file to Spyc. 147 | * 148 | * @param string $file 149 | * @return array 150 | */ 151 | public function loadFile($file) { 152 | return $this->__load($file); 153 | } 154 | 155 | /** 156 | * Load YAML into a PHP array statically 157 | * 158 | * The load method, when supplied with a YAML stream (string or file), 159 | * will do its best to convert YAML in a file into a PHP array. Pretty 160 | * simple. 161 | * Usage: 162 | * 163 | * $array = Spyc::YAMLLoad('lucky.yaml'); 164 | * print_r($array); 165 | * 166 | * 167 | * @access public 168 | * @return array 169 | * @param string $input 170 | * Path of YAML file or string containing YAML 171 | */ 172 | public static function YAMLLoad($input) { 173 | $Spyc = new Spyc(); 174 | return $Spyc->__load($input); 175 | } 176 | 177 | /** 178 | * Load a string of YAML into a PHP array statically 179 | * 180 | * The load method, when supplied with a YAML string, will do its best 181 | * to convert YAML in a string into a PHP array. Pretty simple. 182 | * 183 | * Note: use this function if you don't want files from the file system 184 | * loaded and processed as YAML. This is of interest to people concerned 185 | * about security whose input is from a string. 186 | * 187 | * Usage: 188 | * 189 | * $array = Spyc::YAMLLoadString("---\n0: hello world\n"); 190 | * print_r($array); 191 | * 192 | * 193 | * @access public 194 | * @return array 195 | * @param string $input 196 | * String containing YAML 197 | */ 198 | public static function YAMLLoadString($input) { 199 | $Spyc = new Spyc(); 200 | return $Spyc->__loadString($input); 201 | } 202 | 203 | /** 204 | * Dump YAML from PHP array statically 205 | * 206 | * The dump method, when supplied with an array, will do its best 207 | * to convert the array into friendly YAML. Pretty simple. Feel free to 208 | * save the returned string as nothing.yaml and pass it around. 209 | * 210 | * Oh, and you can decide how big the indent is and what the wordwrap 211 | * for folding is. Pretty cool -- just pass in 'false' for either if 212 | * you want to use the default. 213 | * 214 | * Indent's default is 2 spaces, wordwrap's default is 40 characters. And 215 | * you can turn off wordwrap by passing in 0. 216 | * 217 | * @access public 218 | * @return string 219 | * @param array $array 220 | * PHP array 221 | * @param int $indent 222 | * Pass in false to use the default, which is 2 223 | * @param int $wordwrap 224 | * Pass in 0 for no wordwrap, false for default (40) 225 | * @param int $no_opening_dashes 226 | * Do not start YAML file with "---\n" 227 | */ 228 | public static function YAMLDump($array, $indent = false, $wordwrap = false, $no_opening_dashes = false) { 229 | $spyc = new Spyc(); 230 | return $spyc->dump($array, $indent, $wordwrap, $no_opening_dashes); 231 | } 232 | 233 | /** 234 | * Dump PHP array to YAML 235 | * 236 | * The dump method, when supplied with an array, will do its best 237 | * to convert the array into friendly YAML. Pretty simple. Feel free to 238 | * save the returned string as tasteful.yaml and pass it around. 239 | * 240 | * Oh, and you can decide how big the indent is and what the wordwrap 241 | * for folding is. Pretty cool -- just pass in 'false' for either if 242 | * you want to use the default. 243 | * 244 | * Indent's default is 2 spaces, wordwrap's default is 40 characters. And 245 | * you can turn off wordwrap by passing in 0. 246 | * 247 | * @access public 248 | * @return string 249 | * @param array $array 250 | * PHP array 251 | * @param int $indent 252 | * Pass in false to use the default, which is 2 253 | * @param int $wordwrap 254 | * Pass in 0 for no wordwrap, false for default (40) 255 | */ 256 | public function dump($array, $indent = false, $wordwrap = false, $no_opening_dashes = false) { 257 | // Dumps to some very clean YAML. We'll have to add some more features 258 | // and options soon. And better support for folding. 259 | // New features and options. 260 | if ($indent === false or !is_numeric($indent)) { 261 | $this->_dumpIndent = 2; 262 | } else { 263 | $this->_dumpIndent = $indent; 264 | } 265 | if ($wordwrap === false or !is_numeric($wordwrap)) { 266 | $this->_dumpWordWrap = 40; 267 | } else { 268 | $this->_dumpWordWrap = $wordwrap; 269 | } 270 | // New YAML document 271 | $string = ""; 272 | if (!$no_opening_dashes) 273 | $string = "---\n"; 274 | // Start at the base of the array and move through it. 275 | if ($array) { 276 | $array = (array)$array; 277 | $previous_key = -1; 278 | foreach ($array as $key => $value) { 279 | if (!isset($first_key)) 280 | $first_key = $key; 281 | $string .= $this->_yamlize($key, $value, 0, $previous_key, $first_key, $array); 282 | $previous_key = $key; 283 | } 284 | } 285 | return $string; 286 | } 287 | 288 | /** 289 | * Attempts to convert a key / value array item to YAML 290 | * 291 | * @access private 292 | * @return string 293 | * @param $key The 294 | * name of the key 295 | * @param $value The 296 | * value of the item 297 | * @param $indent The 298 | * indent of the current node 299 | */ 300 | private function _yamlize($key, $value, $indent, $previous_key = -1, $first_key = 0, $source_array = null) { 301 | if (is_array($value)) { 302 | if (empty($value)) 303 | return $this->_dumpNode($key, array(), $indent, $previous_key, $first_key, $source_array); 304 | // It has children. What to do? 305 | // Make it the right kind of item 306 | $string = $this->_dumpNode($key, self::REMPTY, $indent, $previous_key, $first_key, $source_array); 307 | // Add the indent 308 | $indent += $this->_dumpIndent; 309 | // Yamlize the array 310 | $string .= $this->_yamlizeArray($value, $indent); 311 | } elseif (!is_array($value)) { 312 | // It doesn't have children. Yip. 313 | $string = $this->_dumpNode($key, $value, $indent, $previous_key, $first_key, $source_array); 314 | } 315 | return $string; 316 | } 317 | 318 | /** 319 | * Attempts to convert an array to YAML 320 | * 321 | * @access private 322 | * @return string 323 | * @param $array The 324 | * array you want to convert 325 | * @param $indent The 326 | * indent of the current level 327 | */ 328 | private function _yamlizeArray($array, $indent) { 329 | if (is_array($array)) { 330 | $string = ''; 331 | $previous_key = -1; 332 | foreach ($array as $key => $value) { 333 | if (!isset($first_key)) 334 | $first_key = $key; 335 | $string .= $this->_yamlize($key, $value, $indent, $previous_key, $first_key, $array); 336 | $previous_key = $key; 337 | } 338 | return $string; 339 | } else { 340 | return false; 341 | } 342 | } 343 | 344 | /** 345 | * Returns YAML from a key and a value 346 | * 347 | * @access private 348 | * @return string 349 | * @param $key The 350 | * name of the key 351 | * @param $value The 352 | * value of the item 353 | * @param $indent The 354 | * indent of the current node 355 | */ 356 | private function _dumpNode($key, $value, $indent, $previous_key = -1, $first_key = 0, $source_array = null) { 357 | // do some folding here, for blocks 358 | if (is_string($value) && ((strpos($value, "\n") !== false || strpos($value, ": ") !== false || strpos($value, "- ") !== false || strpos($value, "*") !== false || strpos($value, "#") !== false || strpos($value, "<") !== false || strpos($value, ">") !== false || strpos($value, ' ') !== false || strpos($value, "[") !== false || strpos($value, "]") !== false || strpos($value, "{") !== false || strpos($value, "}") !== false) || strpos($value, "&") !== false || strpos($value, "'") !== false || strpos($value, "!") === 0 || substr($value, -1, 1) == ':')) { 359 | $value = $this->_doLiteralBlock($value, $indent); 360 | } else { 361 | $value = $this->_doFolding($value, $indent); 362 | } 363 | if ($value === array()) 364 | $value = '[ ]'; 365 | if ($value === "") 366 | $value = '""'; 367 | if (self::isTranslationWord($value)) { 368 | $value = $this->_doLiteralBlock($value, $indent); 369 | } 370 | if (trim($value) != $value) 371 | $value = $this->_doLiteralBlock($value, $indent); 372 | if (is_bool($value)) { 373 | $value = $value ? "true" : "false"; 374 | } 375 | if ($value === null) 376 | $value = 'null'; 377 | if ($value === "'" . self::REMPTY . "'") 378 | $value = null; 379 | $spaces = str_repeat(' ', $indent); 380 | // if (is_int($key) && $key - 1 == $previous_key && $first_key===0) { 381 | if (is_array($source_array) && array_keys($source_array) === range(0, count($source_array) - 1)) { 382 | // It's a sequence 383 | $string = $spaces . '- ' . $value . "\n"; 384 | } else { 385 | // if ($first_key===0) throw new Exception('Keys are all screwy. The first one was zero, now it\'s "'. $key .'"'); 386 | // It's mapped 387 | if (strpos($key, ":") !== false || strpos($key, "#") !== false) { 388 | $key = '"' . $key . '"'; 389 | } 390 | $string = rtrim($spaces . $key . ': ' . $value) . "\n"; 391 | } 392 | return $string; 393 | } 394 | 395 | /** 396 | * Creates a literal block for dumping 397 | * 398 | * @access private 399 | * @return string 400 | * @param 401 | * $value 402 | * @param $indent int 403 | * The value of the indent 404 | */ 405 | private function _doLiteralBlock($value, $indent) { 406 | if ($value === "\n") 407 | return '\n'; 408 | if (strpos($value, "\n") === false && strpos($value, "'") === false) { 409 | return sprintf("'%s'", $value); 410 | } 411 | if (strpos($value, "\n") === false && strpos($value, '"') === false) { 412 | return sprintf('"%s"', $value); 413 | } 414 | $exploded = explode("\n", $value); 415 | $newValue = '|'; 416 | $indent += $this->_dumpIndent; 417 | $spaces = str_repeat(' ', $indent); 418 | foreach ($exploded as $line) { 419 | $newValue .= "\n" . $spaces . ($line); 420 | } 421 | return $newValue; 422 | } 423 | 424 | /** 425 | * Folds a string of text, if necessary 426 | * 427 | * @access private 428 | * @return string 429 | * @param $value The 430 | * string you wish to fold 431 | */ 432 | private function _doFolding($value, $indent) { 433 | // Don't do anything if wordwrap is set to 0 434 | if ($this->_dumpWordWrap !== 0 && is_string($value) && strlen($value) > $this->_dumpWordWrap) { 435 | $indent += $this->_dumpIndent; 436 | $indent = str_repeat(' ', $indent); 437 | $wrapped = wordwrap($value, $this->_dumpWordWrap, "\n$indent"); 438 | $value = ">\n" . $indent . $wrapped; 439 | } else { 440 | if ($this->setting_dump_force_quotes && is_string($value) && $value !== self::REMPTY) 441 | $value = '"' . $value . '"'; 442 | if (is_numeric($value) && is_string($value)) 443 | $value = '"' . $value . '"'; 444 | } 445 | return $value; 446 | } 447 | 448 | private function isTrueWord($value) { 449 | $words = self::getTranslations(array('true', 'on', 'yes', 'y')); 450 | return in_array($value, $words, true); 451 | } 452 | 453 | private function isFalseWord($value) { 454 | $words = self::getTranslations(array('false', 'off', 'no', 'n')); 455 | return in_array($value, $words, true); 456 | } 457 | 458 | private function isNullWord($value) { 459 | $words = self::getTranslations(array('null', '~')); 460 | return in_array($value, $words, true); 461 | } 462 | 463 | private function isTranslationWord($value) { 464 | return (self::isTrueWord($value) || self::isFalseWord($value) || self::isNullWord($value)); 465 | } 466 | 467 | /** 468 | * Coerce a string into a native type 469 | * Reference: http://yaml.org/type/bool.html 470 | * TODO: Use only words from the YAML spec. 471 | * 472 | * @access private 473 | * @param $value The 474 | * value to coerce 475 | */ 476 | private function coerceValue(&$value) { 477 | if (self::isTrueWord($value)) { 478 | $value = true; 479 | } else if (self::isFalseWord($value)) { 480 | $value = false; 481 | } else if (self::isNullWord($value)) { 482 | $value = null; 483 | } 484 | } 485 | 486 | /** 487 | * Given a set of words, perform the appropriate translations on them to 488 | * match the YAML 1.1 specification for type coercing. 489 | * 490 | * @param $words The 491 | * words to translate 492 | * @access private 493 | */ 494 | private static function getTranslations(array $words) { 495 | $result = array(); 496 | foreach ($words as $i) { 497 | $result = array_merge($result, array(ucfirst($i), strtoupper($i), strtolower($i))); 498 | } 499 | return $result; 500 | } 501 | // LOADING FUNCTIONS 502 | private function __load($input) { 503 | $Source = $this->loadFromSource($input); 504 | return $this->loadWithSource($Source); 505 | } 506 | 507 | private function __loadString($input) { 508 | $Source = $this->loadFromString($input); 509 | return $this->loadWithSource($Source); 510 | } 511 | 512 | private function loadWithSource($Source) { 513 | if (empty($Source)) 514 | return array(); 515 | $this->path = array(); 516 | $this->result = array(); 517 | $cnt = count($Source); 518 | for($i = 0; $i < $cnt; $i++) { 519 | $line = $Source[$i]; 520 | $this->indent = strlen($line) - strlen(ltrim($line)); 521 | $tempPath = $this->getParentPathByIndent($this->indent); 522 | $line = self::stripIndent($line, $this->indent); 523 | if (self::isComment($line)) 524 | continue; 525 | if (self::isEmpty($line)) 526 | continue; 527 | $this->path = $tempPath; 528 | $literalBlockStyle = self::startsLiteralBlock($line); 529 | if ($literalBlockStyle) { 530 | $line = rtrim($line, $literalBlockStyle . " \n"); 531 | $literalBlock = ''; 532 | $line .= ' ' . $this->LiteralPlaceHolder; 533 | $literal_block_indent = strlen($Source[$i + 1]) - strlen(ltrim($Source[$i + 1])); 534 | while (++$i < $cnt && $this->literalBlockContinues($Source[$i], $this->indent)) { 535 | $literalBlock = $this->addLiteralLine($literalBlock, $Source[$i], $literalBlockStyle, $literal_block_indent); 536 | } 537 | $i--; 538 | } 539 | // Strip out comments 540 | if (strpos($line, '#')) { 541 | $line = preg_replace('/\s*#([^"\']+)$/', '', $line); 542 | } 543 | while (++$i < $cnt && self::greedilyNeedNextLine($line)) { 544 | $line = rtrim($line, " \n\t\r") . ' ' . ltrim($Source[$i], " \t"); 545 | } 546 | $i--; 547 | $lineArray = $this->_parseLine($line); 548 | if ($literalBlockStyle) 549 | $lineArray = $this->revertLiteralPlaceHolder($lineArray, $literalBlock); 550 | $this->addArray($lineArray, $this->indent); 551 | foreach ($this->delayedPath as $indent => $delayedPath) 552 | $this->path[$indent] = $delayedPath; 553 | $this->delayedPath = array(); 554 | } 555 | return $this->result; 556 | } 557 | 558 | private function loadFromSource($input) { 559 | if (!empty($input) && strpos($input, "\n") === false && file_exists($input)) 560 | $input = file_get_contents($input); 561 | return $this->loadFromString($input); 562 | } 563 | 564 | private function loadFromString($input) { 565 | $lines = explode("\n", $input); 566 | foreach ($lines as $k => $_) { 567 | $lines[$k] = rtrim($_, "\r"); 568 | } 569 | return $lines; 570 | } 571 | 572 | /** 573 | * Parses YAML code and returns an array for a node 574 | * 575 | * @access private 576 | * @return array 577 | * @param string $line 578 | * A line from the YAML file 579 | */ 580 | private function _parseLine($line) { 581 | if (!$line) 582 | return array(); 583 | $line = trim($line); 584 | if (!$line) 585 | return array(); 586 | $array = array(); 587 | $group = $this->nodeContainsGroup($line); 588 | if ($group) { 589 | $this->addGroup($line, $group); 590 | $line = $this->stripGroup($line, $group); 591 | } 592 | if ($this->startsMappedSequence($line)) 593 | return $this->returnMappedSequence($line); 594 | if ($this->startsMappedValue($line)) 595 | return $this->returnMappedValue($line); 596 | if ($this->isArrayElement($line)) 597 | return $this->returnArrayElement($line); 598 | if ($this->isPlainArray($line)) 599 | return $this->returnPlainArray($line); 600 | return $this->returnKeyValuePair($line); 601 | } 602 | 603 | /** 604 | * Finds the type of the passed value, returns the value as the new type. 605 | * 606 | * @access private 607 | * @param string $value 608 | * @return mixed 609 | */ 610 | private function _toType($value) { 611 | if ($value === '') 612 | return ""; 613 | $first_character = $value[0]; 614 | $last_character = substr($value, -1, 1); 615 | $is_quoted = false; 616 | do { 617 | if (!$value) 618 | break; 619 | if ($first_character != '"' && $first_character != "'") 620 | break; 621 | if ($last_character != '"' && $last_character != "'") 622 | break; 623 | $is_quoted = true; 624 | } while (0); 625 | if ($is_quoted) { 626 | $value = str_replace('\n', "\n", $value); 627 | return strtr(substr($value, 1, -1), array('\\"' => '"', '\'\'' => '\'', '\\\'' => '\'')); 628 | } 629 | if (strpos($value, ' #') !== false && !$is_quoted) 630 | $value = preg_replace('/\s+#(.+)$/', '', $value); 631 | if ($first_character == '[' && $last_character == ']') { 632 | // Take out strings sequences and mappings 633 | $innerValue = trim(substr($value, 1, -1)); 634 | if ($innerValue === '') 635 | return array(); 636 | $explode = $this->_inlineEscape($innerValue); 637 | // Propagate value array 638 | $value = array(); 639 | foreach ($explode as $v) { 640 | $value[] = $this->_toType($v); 641 | } 642 | return $value; 643 | } 644 | if (strpos($value, ': ') !== false && $first_character != '{') { 645 | $array = explode(': ', $value); 646 | $key = trim($array[0]); 647 | array_shift($array); 648 | $value = trim(implode(': ', $array)); 649 | $value = $this->_toType($value); 650 | return array($key => $value); 651 | } 652 | if ($first_character == '{' && $last_character == '}') { 653 | $innerValue = trim(substr($value, 1, -1)); 654 | if ($innerValue === '') 655 | return array(); 656 | // Inline Mapping 657 | // Take out strings sequences and mappings 658 | $explode = $this->_inlineEscape($innerValue); 659 | // Propagate value array 660 | $array = array(); 661 | foreach ($explode as $v) { 662 | $SubArr = $this->_toType($v); 663 | if (empty($SubArr)) 664 | continue; 665 | if (is_array($SubArr)) { 666 | $array[key($SubArr)] = $SubArr[key($SubArr)]; 667 | continue; 668 | } 669 | $array[] = $SubArr; 670 | } 671 | return $array; 672 | } 673 | if ($value == 'null' || $value == 'NULL' || $value == 'Null' || $value == '' || $value == '~') { 674 | return null; 675 | } 676 | if (is_numeric($value) && preg_match('/^(-|)[1-9]+[0-9]*$/', $value)) { 677 | $intvalue = (int)$value; 678 | if ($intvalue != PHP_INT_MAX) 679 | $value = $intvalue; 680 | return $value; 681 | } 682 | if (is_numeric($value) && preg_match('/^0[xX][0-9a-fA-F]+$/', $value)) { 683 | // Hexadecimal value. 684 | return hexdec($value); 685 | } 686 | $this->coerceValue($value); 687 | if (is_numeric($value)) { 688 | if ($value === '0') 689 | return 0; 690 | if (rtrim($value, 0) === $value) 691 | $value = (float)$value; 692 | return $value; 693 | } 694 | return $value; 695 | } 696 | 697 | /** 698 | * Used in inlines to check for more inlines or quoted strings 699 | * 700 | * @access private 701 | * @return array 702 | */ 703 | private function _inlineEscape($inline) { 704 | // There's gotta be a cleaner way to do this... 705 | // While pure sequences seem to be nesting just fine, 706 | // pure mappings and mappings with sequences inside can't go very 707 | // deep. This needs to be fixed. 708 | $seqs = array(); 709 | $maps = array(); 710 | $saved_strings = array(); 711 | $saved_empties = array(); 712 | // Check for empty strings 713 | $regex = '/("")|(\'\')/'; 714 | if (preg_match_all($regex, $inline, $strings)) { 715 | $saved_empties = $strings[0]; 716 | $inline = preg_replace($regex, 'YAMLEmpty', $inline); 717 | } 718 | unset($regex); 719 | // Check for strings 720 | $regex = '/(?:(")|(?:\'))((?(1)[^"]+|[^\']+))(?(1)"|\')/'; 721 | if (preg_match_all($regex, $inline, $strings)) { 722 | $saved_strings = $strings[0]; 723 | $inline = preg_replace($regex, 'YAMLString', $inline); 724 | } 725 | unset($regex); 726 | // echo $inline; 727 | $i = 0; 728 | do { 729 | // Check for sequences 730 | while (preg_match('/\[([^{}\[\]]+)\]/U', $inline, $matchseqs)) { 731 | $seqs[] = $matchseqs[0]; 732 | $inline = preg_replace('/\[([^{}\[\]]+)\]/U', ('YAMLSeq' . (count($seqs) - 1) . 's'), $inline, 1); 733 | } 734 | // Check for mappings 735 | while (preg_match('/{([^\[\]{}]+)}/U', $inline, $matchmaps)) { 736 | $maps[] = $matchmaps[0]; 737 | $inline = preg_replace('/{([^\[\]{}]+)}/U', ('YAMLMap' . (count($maps) - 1) . 's'), $inline, 1); 738 | } 739 | if ($i++ >= 10) 740 | break; 741 | } while (strpos($inline, '[') !== false || strpos($inline, '{') !== false); 742 | $explode = explode(',', $inline); 743 | $explode = array_map('trim', $explode); 744 | $stringi = 0; 745 | $i = 0; 746 | while (1) { 747 | // Re-add the sequences 748 | if (!empty($seqs)) { 749 | foreach ($explode as $key => $value) { 750 | if (strpos($value, 'YAMLSeq') !== false) { 751 | foreach ($seqs as $seqk => $seq) { 752 | $explode[$key] = str_replace(('YAMLSeq' . $seqk . 's'), $seq, $value); 753 | $value = $explode[$key]; 754 | } 755 | } 756 | } 757 | } 758 | // Re-add the mappings 759 | if (!empty($maps)) { 760 | foreach ($explode as $key => $value) { 761 | if (strpos($value, 'YAMLMap') !== false) { 762 | foreach ($maps as $mapk => $map) { 763 | $explode[$key] = str_replace(('YAMLMap' . $mapk . 's'), $map, $value); 764 | $value = $explode[$key]; 765 | } 766 | } 767 | } 768 | } 769 | // Re-add the strings 770 | if (!empty($saved_strings)) { 771 | foreach ($explode as $key => $value) { 772 | while (strpos($value, 'YAMLString') !== false) { 773 | $explode[$key] = preg_replace('/YAMLString/', $saved_strings[$stringi], $value, 1); 774 | unset($saved_strings[$stringi]); 775 | ++$stringi; 776 | $value = $explode[$key]; 777 | } 778 | } 779 | } 780 | // Re-add the empties 781 | if (!empty($saved_empties)) { 782 | foreach ($explode as $key => $value) { 783 | while (strpos($value, 'YAMLEmpty') !== false) { 784 | $explode[$key] = preg_replace('/YAMLEmpty/', '', $value, 1); 785 | $value = $explode[$key]; 786 | } 787 | } 788 | } 789 | $finished = true; 790 | foreach ($explode as $key => $value) { 791 | if (strpos($value, 'YAMLSeq') !== false) { 792 | $finished = false; 793 | break; 794 | } 795 | if (strpos($value, 'YAMLMap') !== false) { 796 | $finished = false; 797 | break; 798 | } 799 | if (strpos($value, 'YAMLString') !== false) { 800 | $finished = false; 801 | break; 802 | } 803 | if (strpos($value, 'YAMLEmpty') !== false) { 804 | $finished = false; 805 | break; 806 | } 807 | } 808 | if ($finished) 809 | break; 810 | $i++; 811 | if ($i > 10) 812 | break; // Prevent infinite loops. 813 | } 814 | return $explode; 815 | } 816 | 817 | private function literalBlockContinues($line, $lineIndent) { 818 | if (!trim($line)) 819 | return true; 820 | if (strlen($line) - strlen(ltrim($line)) > $lineIndent) 821 | return true; 822 | return false; 823 | } 824 | 825 | private function referenceContentsByAlias($alias) { 826 | do { 827 | if (!isset($this->SavedGroups[$alias])) { 828 | echo "Bad group name: $alias."; 829 | break; 830 | } 831 | $groupPath = $this->SavedGroups[$alias]; 832 | $value = $this->result; 833 | foreach ($groupPath as $k) { 834 | $value = $value[$k]; 835 | } 836 | } while (false); 837 | return $value; 838 | } 839 | 840 | private function addArrayInline($array, $indent) { 841 | $CommonGroupPath = $this->path; 842 | if (empty($array)) 843 | return false; 844 | foreach ($array as $k => $_) { 845 | $this->addArray(array($k => $_), $indent); 846 | $this->path = $CommonGroupPath; 847 | } 848 | return true; 849 | } 850 | 851 | private function addArray($incoming_data, $incoming_indent) { 852 | // print_r ($incoming_data); 853 | if (count($incoming_data) > 1) 854 | return $this->addArrayInline($incoming_data, $incoming_indent); 855 | $key = key($incoming_data); 856 | $value = isset($incoming_data[$key]) ? $incoming_data[$key] : null; 857 | if ($key === '__!YAMLZero') 858 | $key = '0'; 859 | if ($incoming_indent == 0 && !$this->_containsGroupAlias && !$this->_containsGroupAnchor) { // Shortcut for root-level values. 860 | if ($key || $key === '' || $key === '0') { 861 | $this->result[$key] = $value; 862 | } else { 863 | $this->result[] = $value; 864 | end($this->result); 865 | $key = key($this->result); 866 | } 867 | $this->path[$incoming_indent] = $key; 868 | return; 869 | } 870 | $history = array(); 871 | // Unfolding inner array tree. 872 | $history[] = $_arr = $this->result; 873 | foreach ($this->path as $k) { 874 | $history[] = $_arr = $_arr[$k]; 875 | } 876 | if ($this->_containsGroupAlias) { 877 | $value = $this->referenceContentsByAlias($this->_containsGroupAlias); 878 | $this->_containsGroupAlias = false; 879 | } 880 | // Adding string or numeric key to the innermost level or $this->arr. 881 | if (is_string($key) && $key == '<<') { 882 | if (!is_array($_arr)) { 883 | $_arr = array(); 884 | } 885 | $_arr = array_merge($_arr, $value); 886 | } else if ($key || $key === '' || $key === '0') { 887 | if (!is_array($_arr)) 888 | $_arr = array($key => $value); 889 | else 890 | $_arr[$key] = $value; 891 | } else { 892 | if (!is_array($_arr)) { 893 | $_arr = array($value); 894 | $key = 0; 895 | } else { 896 | $_arr[] = $value; 897 | end($_arr); 898 | $key = key($_arr); 899 | } 900 | } 901 | $reverse_path = array_reverse($this->path); 902 | $reverse_history = array_reverse($history); 903 | $reverse_history[0] = $_arr; 904 | $cnt = count($reverse_history) - 1; 905 | for($i = 0; $i < $cnt; $i++) { 906 | $reverse_history[$i + 1][$reverse_path[$i]] = $reverse_history[$i]; 907 | } 908 | $this->result = $reverse_history[$cnt]; 909 | $this->path[$incoming_indent] = $key; 910 | if ($this->_containsGroupAnchor) { 911 | $this->SavedGroups[$this->_containsGroupAnchor] = $this->path; 912 | if (is_array($value)) { 913 | $k = key($value); 914 | if (!is_int($k)) { 915 | $this->SavedGroups[$this->_containsGroupAnchor][$incoming_indent + 2] = $k; 916 | } 917 | } 918 | $this->_containsGroupAnchor = false; 919 | } 920 | } 921 | 922 | private static function startsLiteralBlock($line) { 923 | $lastChar = substr(trim($line), -1); 924 | if ($lastChar != '>' && $lastChar != '|') 925 | return false; 926 | if ($lastChar == '|') 927 | return $lastChar; 928 | // HTML tags should not be counted as literal blocks. 929 | if (preg_match('#<.*?>$#', $line)) 930 | return false; 931 | return $lastChar; 932 | } 933 | 934 | private static function greedilyNeedNextLine($line) { 935 | $line = trim($line); 936 | if (!strlen($line)) 937 | return false; 938 | if (substr($line, -1, 1) == ']') 939 | return false; 940 | if ($line[0] == '[') 941 | return true; 942 | if (preg_match('#^[^:]+?:\s*\[#', $line)) 943 | return true; 944 | return false; 945 | } 946 | 947 | private function addLiteralLine($literalBlock, $line, $literalBlockStyle, $indent = -1) { 948 | $line = self::stripIndent($line, $indent); 949 | if ($literalBlockStyle !== '|') { 950 | $line = self::stripIndent($line); 951 | } 952 | $line = rtrim($line, "\r\n\t ") . "\n"; 953 | if ($literalBlockStyle == '|') { 954 | return $literalBlock . $line; 955 | } 956 | if (strlen($line) == 0) 957 | return rtrim($literalBlock, ' ') . "\n"; 958 | if ($line == "\n" && $literalBlockStyle == '>') { 959 | return rtrim($literalBlock, " \t") . "\n"; 960 | } 961 | if ($line != "\n") 962 | $line = trim($line, "\r\n ") . " "; 963 | return $literalBlock . $line; 964 | } 965 | 966 | function revertLiteralPlaceHolder($lineArray, $literalBlock) { 967 | foreach ($lineArray as $k => $_) { 968 | if (is_array($_)) 969 | $lineArray[$k] = $this->revertLiteralPlaceHolder($_, $literalBlock); 970 | else if (substr($_, -1 * strlen($this->LiteralPlaceHolder)) == $this->LiteralPlaceHolder) 971 | $lineArray[$k] = rtrim($literalBlock, " \r\n"); 972 | } 973 | return $lineArray; 974 | } 975 | 976 | private static function stripIndent($line, $indent = -1) { 977 | if ($indent == -1) 978 | $indent = strlen($line) - strlen(ltrim($line)); 979 | return substr($line, $indent); 980 | } 981 | 982 | private function getParentPathByIndent($indent) { 983 | if ($indent == 0) 984 | return array(); 985 | $linePath = $this->path; 986 | do { 987 | end($linePath); 988 | $lastIndentInParentPath = key($linePath); 989 | if ($indent <= $lastIndentInParentPath) 990 | array_pop($linePath); 991 | } while ($indent <= $lastIndentInParentPath); 992 | return $linePath; 993 | } 994 | 995 | private function clearBiggerPathValues($indent) { 996 | if ($indent == 0) 997 | $this->path = array(); 998 | if (empty($this->path)) 999 | return true; 1000 | foreach ($this->path as $k => $_) { 1001 | if ($k > $indent) 1002 | unset($this->path[$k]); 1003 | } 1004 | return true; 1005 | } 1006 | 1007 | private static function isComment($line) { 1008 | if (!$line) 1009 | return false; 1010 | if ($line[0] == '#') 1011 | return true; 1012 | if (trim($line, " \r\n\t") == '---') 1013 | return true; 1014 | return false; 1015 | } 1016 | 1017 | private static function isEmpty($line) { 1018 | return (trim($line) === ''); 1019 | } 1020 | 1021 | private function isArrayElement($line) { 1022 | if (!$line || !is_scalar($line)) 1023 | return false; 1024 | if (substr($line, 0, 2) != '- ') 1025 | return false; 1026 | if (strlen($line) > 3) 1027 | if (substr($line, 0, 3) == '---') 1028 | return false; 1029 | return true; 1030 | } 1031 | 1032 | private function isHashElement($line) { 1033 | return strpos($line, ':'); 1034 | } 1035 | 1036 | private function isLiteral($line) { 1037 | if ($this->isArrayElement($line)) 1038 | return false; 1039 | if ($this->isHashElement($line)) 1040 | return false; 1041 | return true; 1042 | } 1043 | 1044 | private static function unquote($value) { 1045 | if (!$value) 1046 | return $value; 1047 | if (!is_string($value)) 1048 | return $value; 1049 | if ($value[0] == '\'') 1050 | return trim($value, '\''); 1051 | if ($value[0] == '"') 1052 | return trim($value, '"'); 1053 | return $value; 1054 | } 1055 | 1056 | private function startsMappedSequence($line) { 1057 | return (substr($line, 0, 2) == '- ' && substr($line, -1, 1) == ':'); 1058 | } 1059 | 1060 | private function returnMappedSequence($line) { 1061 | $array = array(); 1062 | $key = self::unquote(trim(substr($line, 1, -1))); 1063 | $array[$key] = array(); 1064 | $this->delayedPath = array(strpos($line, $key) + $this->indent => $key); 1065 | return array($array); 1066 | } 1067 | 1068 | private function checkKeysInValue($value) { 1069 | if (strchr('[{"\'', $value[0]) === false) { 1070 | if (strchr($value, ': ') !== false) { 1071 | throw new Exception('Too many keys: ' . $value); 1072 | } 1073 | } 1074 | } 1075 | 1076 | private function returnMappedValue($line) { 1077 | $this->checkKeysInValue($line); 1078 | $array = array(); 1079 | $key = self::unquote(trim(substr($line, 0, -1))); 1080 | $array[$key] = ''; 1081 | return $array; 1082 | } 1083 | 1084 | private function startsMappedValue($line) { 1085 | return (substr($line, -1, 1) == ':'); 1086 | } 1087 | 1088 | private function isPlainArray($line) { 1089 | return ($line[0] == '[' && substr($line, -1, 1) == ']'); 1090 | } 1091 | 1092 | private function returnPlainArray($line) { 1093 | return $this->_toType($line); 1094 | } 1095 | 1096 | private function returnKeyValuePair($line) { 1097 | $array = array(); 1098 | $key = ''; 1099 | if (strpos($line, ': ')) { 1100 | // It's a key/value pair most likely 1101 | // If the key is in double quotes pull it out 1102 | if (($line[0] == '"' || $line[0] == "'") && preg_match('/^(["\'](.*)["\'](\s)*:)/', $line, $matches)) { 1103 | $value = trim(str_replace($matches[1], '', $line)); 1104 | $key = $matches[2]; 1105 | } else { 1106 | // Do some guesswork as to the key and the value 1107 | $explode = explode(': ', $line); 1108 | $key = trim(array_shift($explode)); 1109 | $value = trim(implode(': ', $explode)); 1110 | $this->checkKeysInValue($value); 1111 | } 1112 | // Set the type of the value. Int, string, etc 1113 | $value = $this->_toType($value); 1114 | if ($key === '0') 1115 | $key = '__!YAMLZero'; 1116 | $array[$key] = $value; 1117 | } else { 1118 | $array = array($line); 1119 | } 1120 | return $array; 1121 | } 1122 | 1123 | private function returnArrayElement($line) { 1124 | if (strlen($line) <= 1) 1125 | return array(array()); // Weird %) 1126 | $array = array(); 1127 | $value = trim(substr($line, 1)); 1128 | $value = $this->_toType($value); 1129 | if ($this->isArrayElement($value)) { 1130 | $value = $this->returnArrayElement($value); 1131 | } 1132 | $array[] = $value; 1133 | return $array; 1134 | } 1135 | 1136 | private function nodeContainsGroup($line) { 1137 | $symbolsForReference = 'A-z0-9_\-'; 1138 | if (strpos($line, '&') === false && strpos($line, '*') === false) 1139 | return false; // Please die fast ;-) 1140 | if ($line[0] == '&' && preg_match('/^(&[' . $symbolsForReference . ']+)/', $line, $matches)) 1141 | return $matches[1]; 1142 | if ($line[0] == '*' && preg_match('/^(\*[' . $symbolsForReference . ']+)/', $line, $matches)) 1143 | return $matches[1]; 1144 | if (preg_match('/(&[' . $symbolsForReference . ']+)$/', $line, $matches)) 1145 | return $matches[1]; 1146 | if (preg_match('/(\*[' . $symbolsForReference . ']+$)/', $line, $matches)) 1147 | return $matches[1]; 1148 | if (preg_match('#^\s*<<\s*:\s*(\*[^\s]+).*$#', $line, $matches)) 1149 | return $matches[1]; 1150 | return false; 1151 | } 1152 | 1153 | private function addGroup($line, $group) { 1154 | if ($group[0] == '&') 1155 | $this->_containsGroupAnchor = substr($group, 1); 1156 | if ($group[0] == '*') 1157 | $this->_containsGroupAlias = substr($group, 1); 1158 | // print_r ($this->path); 1159 | } 1160 | 1161 | private function stripGroup($line, $group) { 1162 | $line = trim(str_replace($group, '', $line)); 1163 | return $line; 1164 | } 1165 | } 1166 | 1167 | // Enable use of Spyc from command line 1168 | // The syntax is the following: php Spyc.php spyc.yaml 1169 | do { 1170 | if (PHP_SAPI != 'cli') 1171 | break; 1172 | if (empty($_SERVER['argc']) || $_SERVER['argc'] < 2) 1173 | break; 1174 | if (empty($_SERVER['PHP_SELF']) || FALSE === strpos($_SERVER['PHP_SELF'], 'Spyc.php')) 1175 | break; 1176 | $file = $argv[1]; 1177 | echo Json::encode(spyc_load_file($file)); 1178 | } while (0); --------------------------------------------------------------------------------