├── .gitignore ├── LICENSE ├── README.md ├── boot.php ├── composer.json ├── docs └── README.md ├── src ├── Auth │ ├── CookieAuth.php │ ├── RedisAuth.php │ └── SessionAuth.php ├── Cache │ ├── Driver │ │ ├── FileCacheDriver.php │ │ ├── MemcacheDriver.php │ │ └── RedisDriver.php │ ├── Request │ │ ├── FileCache.php │ │ ├── Memcache.php │ │ └── RedisCache.php │ └── RequestCache.php ├── Core │ ├── Annotate.php │ ├── Application.php │ ├── Config.php │ ├── CrossArray.php │ ├── Delegate.php │ ├── FrameBase.php │ ├── Helper.php │ ├── HttpAuth.php │ ├── Loader.php │ ├── Rest.php │ └── Router.php ├── DB │ ├── Connector │ │ ├── BaseConnector.php │ │ ├── MySQLConnector.php │ │ ├── OracleConnector.php │ │ ├── PgSQLConnector.php │ │ └── SQLiteConnector.php │ ├── DBFactory.php │ ├── Drivers │ │ ├── CouchDriver.php │ │ ├── MongoDriver.php │ │ ├── PDOOracleDriver.php │ │ └── PDOSqlDriver.php │ └── SQLAssembler │ │ ├── MySQLAssembler.php │ │ ├── OracleAssembler.php │ │ ├── PgSQLAssembler.php │ │ ├── SQLAssembler.php │ │ └── SQLiteAssembler.php ├── Exception │ ├── CacheException.php │ ├── CoreException.php │ ├── CrossException.php │ ├── DBConnectException.php │ ├── FrontException.php │ ├── LogicStatusException.php │ └── tpl │ │ ├── cli_error.tpl.php │ │ └── front_error.tpl.php ├── Http │ ├── Request.php │ └── Response.php ├── I │ ├── CacheInterface.php │ ├── HttpAuthInterface.php │ ├── ILog.php │ ├── IModelInfo.php │ ├── PDOConnector.php │ ├── RequestCacheInterface.php │ ├── RouterInterface.php │ └── SqlInterface.php ├── Interactive │ ├── DataFilter.php │ └── ResponseData.php ├── Lib │ ├── Array2XML.php │ ├── Document │ │ ├── CallTree.php │ │ ├── CallTreeToHTML.php │ │ └── HTML.php │ ├── StringToPHPStream.php │ └── Upload │ │ ├── Filter │ │ ├── Image.php │ │ └── MimeType.php │ │ ├── IFilter.php │ │ └── Uploader.php ├── MVC │ ├── Controller.php │ ├── Module.php │ └── View.php ├── Model │ ├── RedisModel.php │ └── SQLModel.php └── Runtime │ ├── ClosureContainer.php │ ├── RequestMapping.php │ └── Rules.php └── tests ├── MainTest.php ├── README.md └── project └── app └── test ├── controllers └── Main.php ├── init.php ├── templates └── default │ └── default.layer.php └── views └── MainView.php /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | /.idea/ 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2010 - 2015 crossphp.com 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #CrossPHP 简洁的开发框架# 2 | 3 | MVC,视图Layer布局,灵活的Module,注释配置,智能路由别名,PSR标准, PHP7.2+ 4 | 5 | 官方网站: [http://www.crossphp.com](http://www.crossphp.com "www.crossphp.com") 6 | 文档地址: [http://document.crossphp.com](http://document.crossphp.com "document.crossphp.com") 7 | 8 | 欢迎加入QQ群120801063 -------------------------------------------------------------------------------- /boot.php: -------------------------------------------------------------------------------- 1 | 18 | * Class CookieAuth 19 | * @package cross\auth 20 | */ 21 | class CookieAuth implements HttpAuthInterface 22 | { 23 | /** 24 | * 加解密默认key 25 | * 26 | * @var string 27 | */ 28 | private $authKey = '!wl<@>c(r#%o*s&s'; 29 | 30 | function __construct(string $authKey = '') 31 | { 32 | if ($authKey) { 33 | $this->authKey = $authKey; 34 | } 35 | } 36 | 37 | /** 38 | * 生成加密cookie 39 | * 40 | * @param string $name 41 | * @param string|array $params 42 | * @param int $expire 43 | * @return bool 44 | */ 45 | function set(string $name, $params, int $expire = 0): bool 46 | { 47 | if ($params === '' || $params === null) { 48 | $expire = time() - 3600; 49 | $value = null; 50 | } else { 51 | $encryptKey = $this->getEncryptKey($name); 52 | if (is_array($params)) { 53 | $params = json_encode($params, JSON_UNESCAPED_UNICODE); 54 | } 55 | $value = Helper::authCode($params, 'ENCODE', $encryptKey); 56 | if ($expire > 0) { 57 | $expire = time() + $expire; 58 | } 59 | } 60 | 61 | Response::getInstance()->setRawCookie($name, $value, $expire, '/', Delegate::env('cookie.domain') ?? ''); 62 | return true; 63 | } 64 | 65 | /** 66 | * 从已加密的cookie中取出值 67 | * 68 | * @param string $params cookie的key 69 | * @param bool $deCode 70 | * @return bool|string 71 | */ 72 | function get(string $params, bool $deCode = false) 73 | { 74 | if (false !== strpos($params, ':') && $deCode) { 75 | list($name, $arrKey) = explode(':', $params); 76 | } else { 77 | $name = $params; 78 | } 79 | 80 | if (!isset($_COOKIE[$name])) { 81 | return false; 82 | } 83 | 84 | $value = $_COOKIE[$name]; 85 | $encryptKey = $this->getEncryptKey($name); 86 | $result = Helper::authCode($value, 'DECODE', $encryptKey); 87 | if (!$result) { 88 | return false; 89 | } 90 | 91 | if ($deCode) { 92 | $result = json_decode($result, true); 93 | if (isset($arrKey) && isset($result[$arrKey])) { 94 | return $result[$arrKey]; 95 | } 96 | } 97 | 98 | return $result; 99 | } 100 | 101 | /** 102 | * 生成密钥 103 | * 104 | * @param string $cookieName 105 | * @return string 106 | */ 107 | protected function getEncryptKey(string $cookieName): string 108 | { 109 | return md5($cookieName . $this->authKey); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/Auth/RedisAuth.php: -------------------------------------------------------------------------------- 1 | 17 | * Class RedisAuth 18 | * @package Cross\Auth 19 | */ 20 | class RedisAuth implements HttpAuthInterface 21 | { 22 | /** 23 | * auth key前缀 24 | * 25 | * @var string 26 | */ 27 | protected $authKeyPrefix; 28 | 29 | /** 30 | * RedisAuth constructor. 31 | */ 32 | function __construct() 33 | { 34 | if (session_status() == PHP_SESSION_NONE) { 35 | session_start(); 36 | } 37 | 38 | $this->authKeyPrefix = '_@CPA@_' . session_id(); 39 | } 40 | 41 | /** 42 | * 设置session的值 43 | * 44 | * @param string $key 45 | * @param string|array $value 46 | * @param int $expire 47 | * @return bool|mixed 48 | * @throws CoreException 49 | */ 50 | function set(string $key, $value, int $expire = 0): bool 51 | { 52 | $key = $this->authKeyPrefix . '@' . $key; 53 | if (is_array($value)) { 54 | $value = json_encode($value, JSON_UNESCAPED_UNICODE); 55 | } 56 | 57 | RedisModel::use('auth')->set($key, $value, $expire); 58 | return true; 59 | } 60 | 61 | /** 62 | * 获取session的值 63 | * 64 | * @param string $key 65 | * @param bool $deCode 66 | * @return bool|mixed 67 | * @throws CoreException 68 | */ 69 | function get(string $key, bool $deCode = false) 70 | { 71 | $key = $this->authKeyPrefix . '@' . $key; 72 | if (false !== strpos($key, ':') && $deCode) { 73 | list($key, $arrKey) = explode(':', $key); 74 | } 75 | 76 | $result = RedisModel::use('auth')->get($key); 77 | if (!$result) { 78 | return false; 79 | } 80 | 81 | if ($deCode) { 82 | $result = json_decode($result, true); 83 | if (isset($arrKey) && isset($result[$arrKey])) { 84 | return $result[$arrKey]; 85 | } 86 | } 87 | 88 | return $result; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/Auth/SessionAuth.php: -------------------------------------------------------------------------------- 1 | 15 | * Class SessionAuth 16 | * @package Cross\Auth 17 | */ 18 | class SessionAuth implements HttpAuthInterface 19 | { 20 | function __construct() 21 | { 22 | if (session_status() == PHP_SESSION_NONE) { 23 | session_start(); 24 | } 25 | } 26 | 27 | /** 28 | * 设置session的值 29 | * 30 | * @param string $key 31 | * @param string|array $value 32 | * @param int $expire 33 | * @return bool|mixed 34 | */ 35 | function set(string $key, $value, int $expire = 0): bool 36 | { 37 | if (is_array($value)) { 38 | $value = json_encode($value, JSON_UNESCAPED_UNICODE); 39 | } 40 | 41 | $_SESSION[$key] = $value; 42 | return true; 43 | } 44 | 45 | /** 46 | * 获取session的值 47 | * 48 | * @param string $key 49 | * @param bool $deCode 50 | * @return bool|mixed 51 | */ 52 | function get(string $key, bool $deCode = false) 53 | { 54 | if (false !== strpos($key, ':') && $deCode) { 55 | list($key, $arrKey) = explode(':', $key); 56 | } 57 | 58 | if (!isset($_SESSION[$key])) { 59 | return false; 60 | } 61 | 62 | $result = $_SESSION[$key]; 63 | if ($deCode) { 64 | $result = json_decode($result, true); 65 | if (isset($arrKey) && isset($result[$arrKey])) { 66 | return $result[$arrKey]; 67 | } 68 | } 69 | 70 | return $result; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Cache/Driver/FileCacheDriver.php: -------------------------------------------------------------------------------- 1 | 16 | * Class FileCacheDriver 17 | * @package Cross\Cache\Driver 18 | */ 19 | class FileCacheDriver implements CacheInterface 20 | { 21 | /** 22 | * 缓存文件路径 23 | * 24 | * @var string 25 | */ 26 | private $cachePath; 27 | 28 | function __construct(string $cachePath) 29 | { 30 | $cachePath = rtrim($cachePath, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR; 31 | $this->cachePath = $cachePath; 32 | } 33 | 34 | /** 35 | * 返回缓存文件 36 | * 37 | * @param string $key 38 | * @return mixed 39 | */ 40 | function get(string $key = '') 41 | { 42 | $cacheFile = $this->cachePath . $key; 43 | if (!file_exists($cacheFile)) { 44 | return false; 45 | } 46 | 47 | return file_get_contents($cacheFile); 48 | } 49 | 50 | /** 51 | * 保存缓存 52 | * 53 | * @param string $key 54 | * @param mixed $value 55 | * @return mixed|void 56 | * @throws CoreException 57 | */ 58 | function set(string $key, $value) 59 | { 60 | $cacheFile = $this->cachePath . $key; 61 | if (!file_exists($cacheFile)) { 62 | $filePath = dirname($cacheFile); 63 | if (!is_dir($filePath)) { 64 | $createDir = mkdir($filePath, 0755, true); 65 | if (!$createDir) { 66 | throw new CoreException('创建缓存目录失败'); 67 | } 68 | } 69 | } 70 | 71 | file_put_contents($cacheFile, $value, LOCK_EX); 72 | } 73 | 74 | /** 75 | * 删除缓存 76 | * 77 | * @param string $key 78 | * @return mixed|void 79 | */ 80 | function del(string $key) 81 | { 82 | $cacheFile = $this->cachePath . $key; 83 | if (file_exists($cacheFile)) { 84 | unlink($cacheFile); 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/Cache/Driver/MemcacheDriver.php: -------------------------------------------------------------------------------- 1 | 17 | * Class MemcacheDriver 18 | * @package Cross\Cache\Driver 19 | */ 20 | class MemcacheDriver 21 | { 22 | /** 23 | * @var Memcache 24 | */ 25 | public $link; 26 | 27 | /** 28 | * 集群参数默认配置 29 | * 30 | * @var array 31 | */ 32 | protected $defaultOptions = [ 33 | 'persistent' => true, 34 | 'weight' => 1, 35 | 'timeout' => 1, 36 | 'retry_interval' => 15, 37 | 'status' => true, 38 | 'failure_callback' => null 39 | ]; 40 | 41 | /** 42 | * MemcacheDriver constructor. 43 | * 44 | * @param array $option 45 | * @throws CoreException 46 | */ 47 | function __construct(array $option) 48 | { 49 | if (!extension_loaded('memcache')) { 50 | throw new CoreException('Not support memcache extension !'); 51 | } 52 | 53 | if (!isset($option['host'])) { 54 | $option['host'] = '127.0.0.1'; 55 | } 56 | 57 | if (!isset($option['port'])) { 58 | $option['port'] = 11211; 59 | } 60 | 61 | if (!isset($option['timeout'])) { 62 | $option['timeout'] = 1; 63 | } 64 | 65 | try { 66 | $mc = new Memcache(); 67 | //集群服务器IP用|分隔 68 | if (false !== strpos($option['host'], '|')) { 69 | $opt = &$this->defaultOptions; 70 | foreach ($opt as $k => &$v) { 71 | if (isset($option[$k])) { 72 | $v = $option[$k]; 73 | } 74 | } 75 | 76 | $serverList = explode('|', $option['host']); 77 | foreach ($serverList as $server) { 78 | $host = $server; 79 | $port = $option['port']; 80 | if (false !== strpos($server, ':')) { 81 | list($host, $port) = explode(':', $server); 82 | } 83 | 84 | $host = trim($host); 85 | $port = trim($port); 86 | $mc->addServer($host, $port, $opt['persistent'], $opt['weight'], $opt['timeout'], 87 | $opt['retry_interval'], $opt['status'], $opt['failure_callback']); 88 | } 89 | } else { 90 | $mc->connect($option['host'], $option['port'], $option['timeout']); 91 | } 92 | 93 | $this->link = $mc; 94 | } catch (Exception $e) { 95 | throw new CoreException($e->getMessage()); 96 | } 97 | } 98 | 99 | /** 100 | * 从缓存中获取内容 101 | * 102 | * @param string $key 103 | * @param int|array $flag 104 | * @return array|string 105 | */ 106 | function get(string $key, &$flag = 0) 107 | { 108 | return $this->link->get($key, $flag); 109 | } 110 | 111 | /** 112 | * 调用Memcache类提供的方法 113 | * 114 | * @param $method 115 | * @param $argv 116 | * @return mixed|null 117 | */ 118 | public function __call($method, $argv) 119 | { 120 | $result = null; 121 | if (method_exists($this->link, $method)) { 122 | $result = ($argv == null) 123 | ? $this->link->$method() 124 | : call_user_func_array(array($this->link, $method), $argv); 125 | } 126 | 127 | return $result; 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/Cache/Driver/RedisDriver.php: -------------------------------------------------------------------------------- 1 | 17 | * Class RedisDriver 18 | * @package Cross\Cache\Driver 19 | */ 20 | class RedisDriver 21 | { 22 | /** 23 | * @var string 24 | */ 25 | private $id; 26 | 27 | /** 28 | * @var Redis|RedisCluster 29 | */ 30 | protected $link; 31 | 32 | /** 33 | * 是否集群模式 34 | * 35 | * @var bool 36 | */ 37 | protected $clusterMode = false; 38 | 39 | /** 40 | * @var array 41 | */ 42 | protected $option; 43 | 44 | /** 45 | * 连接redis 46 | *
 47 |      * unixsocket设置
 48 |      * unixsocket /tmp/redis.sock
 49 |      * unixsocketperm 777
 50 |      * 
51 | * 52 | * @param $option 53 | * @throws 54 | */ 55 | function __construct(array $option) 56 | { 57 | if (!extension_loaded('redis')) { 58 | throw new CoreException('Not support redis extension !'); 59 | } 60 | 61 | $option['host'] = $option['host'] ?? '127.0.0.1'; 62 | $option['port'] = $option['port'] ?? 6379; 63 | $option['timeout'] = $option['timeout'] ?? 3; 64 | $option['readTimeout'] = $option['readTimeout'] ?? $option['timeout']; 65 | $option['clusterName'] = $option['clusterName'] ?? null; 66 | 67 | //是否使用长链接 68 | if (PHP_SAPI == 'cli') { 69 | ini_set('default_socket_timeout', -1); 70 | $persistent = true; 71 | } else { 72 | $persistent = $option['persistent'] ?? false; 73 | } 74 | 75 | if (is_array($option['host'])) { 76 | $id = implode('|', $option['host']); 77 | $useUnixSocket = false; 78 | $this->clusterMode = true; 79 | } else { 80 | $this->clusterMode = false; 81 | if (strcasecmp(PHP_OS, 'linux') == 0 && !empty($option['unix_socket'])) { 82 | $id = $option['unix_socket']; 83 | $useUnixSocket = true; 84 | } else { 85 | $id = "{$option['host']}:{$option['port']}:{$option['timeout']}"; 86 | $useUnixSocket = false; 87 | } 88 | } 89 | 90 | static $connects; 91 | if (!isset($connects[$id])) { 92 | if ($this->clusterMode) { 93 | $redis = new RedisCluster($option['clusterName'], $option['host'], $option['timeout'], 94 | $option['readTimeout'], $persistent, $option['pass'] ?: null); 95 | } else { 96 | $redis = new Redis(); 97 | if ($persistent) { 98 | if ($useUnixSocket) { 99 | $redis->pconnect($option['unix_socket']); 100 | } else { 101 | $redis->pconnect($option['host'], $option['port'], 0); 102 | } 103 | } else { 104 | if ($useUnixSocket) { 105 | $redis->connect($option['unix_socket']); 106 | } else { 107 | $redis->connect($option['host'], $option['port'], $option['timeout']); 108 | } 109 | } 110 | 111 | if (!empty($option['pass'])) { 112 | $authStatus = $redis->auth($option['pass']); 113 | if (!$authStatus) { 114 | throw new CoreException('Redis auth failed !'); 115 | } 116 | } 117 | } 118 | 119 | $connects[$id] = $redis; 120 | } else { 121 | $redis = &$connects[$id]; 122 | } 123 | 124 | $this->id = $id; 125 | $this->link = $redis; 126 | $this->option = $option; 127 | } 128 | 129 | /** 130 | * 获取连接属性 131 | * 132 | * @return array 133 | */ 134 | function getLinkOption() 135 | { 136 | return $this->option; 137 | } 138 | 139 | /** 140 | * 调用redis类提供的方法 141 | * 142 | * @param $method 143 | * @param $argv 144 | * @return mixed|null 145 | * @throws CoreException 146 | */ 147 | public function __call($method, $argv) 148 | { 149 | $result = null; 150 | if (method_exists($this->link, $method)) { 151 | $this->selectCurrentDatabase(); 152 | $result = ($argv == null) 153 | ? $this->link->$method() 154 | : call_user_func_array([$this->link, $method], $argv); 155 | } 156 | 157 | return $result; 158 | } 159 | 160 | /** 161 | * 选择当前数据库 162 | * 163 | * @throws CoreException 164 | */ 165 | protected function selectCurrentDatabase() 166 | { 167 | if ($this->clusterMode) { 168 | return; 169 | } 170 | 171 | static $selected = null; 172 | $db = &$this->option['db']; 173 | $current = $this->id . ':' . $db; 174 | if ($selected !== $current) { 175 | $selectRet = $this->link->select($db); 176 | if ($selectRet) { 177 | $selected = $current; 178 | } else { 179 | throw new CoreException("Redis select DB($current) failed!"); 180 | } 181 | } 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /src/Cache/Request/FileCache.php: -------------------------------------------------------------------------------- 1 | 17 | * 18 | * Class FileCache 19 | * @package Cross\Cache\Request 20 | */ 21 | class FileCache implements RequestCacheInterface 22 | { 23 | /** 24 | * @var string 25 | */ 26 | private $cacheKey; 27 | 28 | /** 29 | * @var string 30 | */ 31 | private $cachePath; 32 | 33 | /** 34 | * @var array 35 | */ 36 | private $config; 37 | 38 | /** 39 | * 缓存过期时间(秒) 40 | * 41 | * @var int 42 | */ 43 | private $expireTime = 3600; 44 | 45 | /** 46 | * @var FileCacheDriver 47 | */ 48 | private $driver; 49 | 50 | /** 51 | * FileCache constructor. 52 | * 53 | * @param array $config 54 | * @throws CoreException 55 | */ 56 | function __construct(array $config) 57 | { 58 | if (!isset($config['cache_path'])) { 59 | throw new CoreException('请指定缓存目录'); 60 | } 61 | 62 | if (!isset($config['key'])) { 63 | throw new CoreException('请指定缓存KEY'); 64 | } 65 | 66 | if (isset($config['expire_time'])) { 67 | $this->expireTime = &$config['expire_time']; 68 | } 69 | 70 | $this->cacheKey = &$config['key']; 71 | 72 | $this->cachePath = rtrim($config['cache_path'], DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR; 73 | $this->driver = new FileCacheDriver($this->cachePath); 74 | } 75 | 76 | /** 77 | * 写入缓存 78 | * 79 | * @param string $value 80 | * @throws CoreException 81 | */ 82 | function set(string $value): void 83 | { 84 | $this->driver->set($this->cacheKey, $value); 85 | } 86 | 87 | /** 88 | * 获取缓存内容 89 | * 90 | * @return mixed get cache 91 | */ 92 | function get(): string 93 | { 94 | return $this->driver->get($this->cacheKey); 95 | } 96 | 97 | /** 98 | * 是否有效 99 | * 100 | * @return bool 101 | */ 102 | function isValid(): bool 103 | { 104 | $cacheFile = $this->cachePath . $this->cacheKey; 105 | if (!file_exists($cacheFile)) { 106 | return false; 107 | } 108 | 109 | if ((time() - filemtime($cacheFile)) < $this->expireTime) { 110 | return true; 111 | } 112 | 113 | return false; 114 | } 115 | 116 | /** 117 | * 缓存配置 118 | * 119 | * @param array $config 120 | */ 121 | function setConfig(array $config = []) 122 | { 123 | $this->config = $config; 124 | } 125 | 126 | /** 127 | * 获取缓存配置 128 | * 129 | * @return mixed 130 | */ 131 | function getConfig() 132 | { 133 | return $this->config; 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/Cache/Request/Memcache.php: -------------------------------------------------------------------------------- 1 | 17 | * Class RequestMemcache 18 | * @package Cross\Cache\Request 19 | */ 20 | class Memcache implements RequestCacheInterface 21 | { 22 | /** 23 | * @var array 24 | */ 25 | protected $config; 26 | 27 | /** 28 | * 缓存key 29 | * 30 | * @var string 31 | */ 32 | protected $cacheKey; 33 | 34 | /** 35 | * 有效时间 36 | * 37 | * @var int 38 | */ 39 | protected $expireTime = 3600; 40 | 41 | /** 42 | * @var int 43 | */ 44 | protected $flag = 0; 45 | 46 | /** 47 | * @var MemcacheDriver 48 | */ 49 | protected $driver; 50 | 51 | /** 52 | * Memcache constructor. 53 | * 54 | * @param array $option 55 | * @throws CoreException 56 | */ 57 | function __construct(array $option) 58 | { 59 | if (empty($option['key'])) { 60 | throw new CoreException('请指定缓存KEY'); 61 | } 62 | 63 | $this->cacheKey = &$option['key']; 64 | $this->driver = new MemcacheDriver($option); 65 | 66 | if (isset($option['flag']) && $option['flag']) { 67 | $this->flag = &$option['flag']; 68 | } 69 | 70 | if (isset($option['expire_time'])) { 71 | $this->expireTime = &$option['expire_time']; 72 | } 73 | } 74 | 75 | /** 76 | * 设置request缓存 77 | * 78 | * @param string $value 79 | */ 80 | function set(string $value): void 81 | { 82 | $this->driver->set($this->cacheKey, $value, $this->flag, $this->expireTime); 83 | } 84 | 85 | /** 86 | * 获取request缓存 87 | * 88 | * @param int $flag 89 | * @return array|string 90 | */ 91 | function get(&$flag = 0): string 92 | { 93 | return $this->driver->get($this->cacheKey, $flag); 94 | } 95 | 96 | /** 97 | * 查看key是否有效 98 | * 99 | * @return bool 100 | */ 101 | function isValid(): bool 102 | { 103 | if ($this->driver->get($this->cacheKey)) { 104 | return true; 105 | } 106 | 107 | return false; 108 | } 109 | 110 | /** 111 | * 设置配置 112 | * 113 | * @param array $config 114 | */ 115 | function setConfig(array $config = []) 116 | { 117 | $this->config = $config; 118 | } 119 | 120 | /** 121 | * 获取缓存配置 122 | * 123 | * @return mixed 124 | */ 125 | function getConfig() 126 | { 127 | return $this->config; 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/Cache/Request/RedisCache.php: -------------------------------------------------------------------------------- 1 | 17 | * Class RequestRedisCache 18 | * @package Cross\Cache\Request 19 | */ 20 | class RedisCache implements RequestCacheInterface 21 | { 22 | /** 23 | * @var array 24 | */ 25 | protected $config; 26 | 27 | /** 28 | * 缓存key 29 | * 30 | * @var string 31 | */ 32 | protected $cacheKey; 33 | 34 | /** 35 | * 有效时间 36 | * 37 | * @var int 38 | */ 39 | protected $expireTime = 3600; 40 | 41 | /** 42 | * @var bool 43 | */ 44 | protected $compress = false; 45 | 46 | /** 47 | * @var RedisDriver 48 | */ 49 | protected $driver; 50 | 51 | /** 52 | * 设置缓存key和缓存有效期 53 | * 54 | * @param array $option 55 | * @throws CoreException 56 | */ 57 | function __construct(array $option) 58 | { 59 | if (empty($option['key'])) { 60 | throw new CoreException('请指定缓存KEY'); 61 | } 62 | 63 | if (isset($option['expire_time'])) { 64 | $this->expireTime = &$option['expire_time']; 65 | } 66 | 67 | if (isset($option['compress'])) { 68 | $this->compress = &$option['compress']; 69 | } 70 | 71 | $this->cacheKey = &$option['key']; 72 | $this->driver = new RedisDriver($option); 73 | } 74 | 75 | /** 76 | * 设置request请求 77 | * 78 | * @param string $value 79 | */ 80 | function set(string $value): void 81 | { 82 | if ($this->compress) { 83 | $value = gzcompress($value); 84 | } 85 | 86 | $this->driver->setex($this->cacheKey, $this->expireTime, $value); 87 | } 88 | 89 | /** 90 | * 返回request的内容 91 | * 92 | * @return bool|mixed|string 93 | */ 94 | function get(): string 95 | { 96 | $content = $this->driver->get($this->cacheKey); 97 | if ($this->compress) { 98 | $content = gzuncompress($content); 99 | } 100 | 101 | return $content; 102 | } 103 | 104 | /** 105 | * 检查缓存key是否有效 106 | * 107 | * @return bool 108 | */ 109 | function isValid(): bool 110 | { 111 | return $this->driver->ttl($this->cacheKey) > 0; 112 | } 113 | 114 | /** 115 | * 设置配置 116 | * 117 | * @param array $config 118 | */ 119 | function setConfig(array $config = []) 120 | { 121 | $this->config = $config; 122 | } 123 | 124 | /** 125 | * 获取缓存配置 126 | * 127 | * @return mixed 128 | */ 129 | function getConfig() 130 | { 131 | return $this->config; 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/Cache/RequestCache.php: -------------------------------------------------------------------------------- 1 | 25 | * Class RequestCache 26 | * @package Cross\Cache 27 | */ 28 | class RequestCache 29 | { 30 | const FILE = 1; 31 | const MEMCACHE = 2; 32 | const REDIS = 3; 33 | 34 | /** 35 | * @var FileCache|Memcache|RedisCache|RequestCacheInterface 36 | */ 37 | static $instance; 38 | 39 | /** 40 | * RequestCache 41 | * 42 | * @param int|object|string $type 43 | * @param array $options 44 | * @return FileCache|Memcache|RedisCache|RequestCacheInterface 45 | * @throws CoreException 46 | */ 47 | static function factory($type, array $options) 48 | { 49 | switch ($type) { 50 | case 'file': 51 | case self::FILE: 52 | $instance = new FileCache($options); 53 | break; 54 | 55 | case 'memcache': 56 | case self::MEMCACHE: 57 | $instance = new Memcache($options); 58 | break; 59 | 60 | case 'redis': 61 | case self::REDIS: 62 | $instance = new RedisCache($options); 63 | break; 64 | 65 | default: 66 | if (is_string($type)) { 67 | try { 68 | $rf = new ReflectionClass($type); 69 | if ($rf->implementsInterface('Cross\I\RequestCacheInterface')) { 70 | $instance = $rf->newInstance(); 71 | } else { 72 | throw new CoreException('Must implement RequestCacheInterface'); 73 | } 74 | } catch (Exception $e) { 75 | throw new CoreException('Reflection ' . $e->getMessage()); 76 | } 77 | } elseif (is_object($type)) { 78 | if ($type instanceof RequestCacheInterface) { 79 | $instance = $type; 80 | } else { 81 | throw new CoreException('Must implement RequestCacheInterface'); 82 | } 83 | } else { 84 | throw new CoreException('Unsupported cache type!'); 85 | } 86 | } 87 | 88 | $instance->setConfig($options); 89 | return $instance; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/Core/Annotate.php: -------------------------------------------------------------------------------- 1 | 15 | * Class Annotate 16 | * @package Cross\Core 17 | */ 18 | class Annotate 19 | { 20 | /** 21 | * @var Delegate 22 | */ 23 | private $delegate; 24 | 25 | /** 26 | * 注释参数前缀 27 | * 28 | * @var string 29 | */ 30 | private $prefix = 'cp_'; 31 | 32 | /** 33 | * @var Annotate 34 | */ 35 | private static $instance; 36 | 37 | /** 38 | * 注册一个wrapper 39 | * 40 | * @param Delegate $delegate 41 | */ 42 | private function __construct(Delegate &$delegate) 43 | { 44 | $this->delegate = $delegate; 45 | stream_register_wrapper('annotate', 'Cross\Lib\StringToPHPStream'); 46 | } 47 | 48 | /** 49 | * 生成解析注释配置单例对象 50 | * 51 | * @param Delegate $delegate 52 | * @return Annotate 53 | */ 54 | public static function getInstance(Delegate &$delegate): self 55 | { 56 | if (!self::$instance) { 57 | self::$instance = new Annotate($delegate); 58 | } 59 | 60 | return self::$instance; 61 | } 62 | 63 | /** 64 | * 设置前缀 65 | * 66 | * @param string $prefix 67 | * @return $this 68 | */ 69 | function setPrefix(string $prefix): self 70 | { 71 | $this->prefix = $prefix; 72 | return $this; 73 | } 74 | 75 | /** 76 | * 注释配置转换为数组 77 | * 78 | * @param string $annotate 79 | * @return array 80 | */ 81 | public function parse(string $annotate = ''): array 82 | { 83 | if (empty($annotate)) { 84 | return []; 85 | } 86 | 87 | $flag = preg_match_all("/@{$this->prefix}(.*?)\s+(.*)/", $annotate, $content); 88 | if (!$flag || empty($content[1])) { 89 | return []; 90 | } 91 | 92 | $configs = []; 93 | $values = &$content[2]; 94 | array_walk($content[1], function ($k, $index) use ($values, &$configs) { 95 | $v = &$values[$index]; 96 | if (null !== $v) { 97 | $v = trim($v); 98 | } 99 | 100 | if (isset($configs[$k]) && !is_array($configs[$k])) { 101 | $configs[$k] = [$configs[$k]]; 102 | $configs[$k][] = $v; 103 | } elseif (isset($configs[$k])) { 104 | $configs[$k][] = $v; 105 | } else { 106 | $configs[$k] = $v; 107 | } 108 | }); 109 | 110 | return $this->parseAnnotate($configs); 111 | } 112 | 113 | /** 114 | * 把PHP代码绑定到匿名函数中 115 | * 116 | * @param string $params 117 | * @return Closure 118 | */ 119 | public function bindToClosure(string $params): Closure 120 | { 121 | return function ($self) use ($params) { 122 | return include("annotate://{$params}"); 123 | }; 124 | } 125 | 126 | /** 127 | * php字符串代码通过wrapper转换为php代码 128 | * 129 | * @param string $params 130 | * @return mixed 131 | */ 132 | public function toCode(string $params) 133 | { 134 | return include("annotate://{$params}"); 135 | } 136 | 137 | /** 138 | * 配置参数值解析 139 | *
140 |      * 如: a, b=file, c 会被解析为
141 |      * array(
142 |      *      'a' => '',
143 |      *      'b' => file,
144 |      *      'c' => '',
145 |      * )
146 |      * 
147 | * 148 | * @param string $params 149 | * @return array 150 | */ 151 | public function toArray(string $params): array 152 | { 153 | $result = []; 154 | $conf = array_map('trim', explode(',', $params)); 155 | foreach ($conf as $c) { 156 | if (false !== strpos($c, '=')) { 157 | $c = explode('=', $c); 158 | $result[trim($c[0])] = isset($c[1]) ? trim($c[1]) : ''; 159 | } else { 160 | $result[$c] = ''; 161 | } 162 | } 163 | unset($conf); 164 | return $result; 165 | } 166 | 167 | /** 168 | * 注释配置解析 169 | * 170 | * @param array $annotateConfigs 171 | * @return array 172 | */ 173 | private function parseAnnotate(array $annotateConfigs): array 174 | { 175 | $result = []; 176 | foreach ($annotateConfigs as $conf => $params) { 177 | switch ($conf) { 178 | case 'params': 179 | $result['params'] = $this->toArray($params); 180 | break; 181 | 182 | case 'cache': 183 | case 'response': 184 | case 'basicAuth': 185 | $result[$conf] = $this->toCode($params); 186 | break; 187 | 188 | case 'after': 189 | case 'before': 190 | $result[$conf] = $this->bindToClosure($params); 191 | break; 192 | 193 | default: 194 | $closureContainer = $this->delegate->getClosureContainer(); 195 | $hasClosure = $closureContainer->has('parseAnnotate'); 196 | if ($hasClosure) { 197 | $closureContainer->run('parseAnnotate', array($conf, &$params, $this)); 198 | } 199 | 200 | $result[$conf] = $params; 201 | } 202 | } 203 | 204 | return $result; 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /src/Core/Config.php: -------------------------------------------------------------------------------- 1 | 15 | * Class Config 16 | * @package Cross\Core 17 | */ 18 | class Config 19 | { 20 | /** 21 | * @var string 22 | */ 23 | private $file; 24 | 25 | /** 26 | * @var array 27 | */ 28 | private $configData; 29 | 30 | /** 31 | * @var self 32 | */ 33 | private static $instance; 34 | 35 | /** 36 | * @var CrossArray 37 | */ 38 | private $ca; 39 | 40 | /** 41 | * 查询缓存 42 | * 43 | * @var array 44 | */ 45 | private static $cache; 46 | 47 | /** 48 | * 读取配置 49 | * 50 | * @param string $file 配置文件绝对路径 51 | * @throws CoreException 52 | */ 53 | private function __construct(string $file) 54 | { 55 | $this->file = $file; 56 | $this->configData = Loader::read($file); 57 | 58 | $localFile = dirname($file) . DIRECTORY_SEPARATOR . '.' . basename($file); 59 | if (file_exists($localFile)) { 60 | $this->configData = array_merge($this->configData, Loader::read($localFile)); 61 | } 62 | 63 | $this->ca = CrossArray::init($this->configData, $this->file); 64 | } 65 | 66 | /** 67 | * 实例化配置类 68 | * 69 | * @param string $file 70 | * @return Config 71 | * @throws CoreException 72 | */ 73 | static function load(string $file): self 74 | { 75 | if (!isset(self::$instance[$file])) { 76 | self::$instance[$file] = new self($file); 77 | } 78 | 79 | return self::$instance[$file]; 80 | } 81 | 82 | /** 83 | * 合并附加数组到源数组 84 | * 85 | * @param array $appendConfig 86 | * @param bool $cover 是否覆盖已有值 87 | * @return $this 88 | */ 89 | function combine(array $appendConfig = [], bool $cover = true): self 90 | { 91 | if (!empty($appendConfig)) { 92 | foreach ($appendConfig as $key => $value) { 93 | if ($cover) { 94 | $configValue = &$this->configData[$key]; 95 | if (is_array($value) && is_array($configValue)) { 96 | $this->configData[$key] = array_merge($configValue, $value); 97 | } else { 98 | $this->configData[$key] = $value; 99 | } 100 | } else { 101 | if (isset($this->configData[$key])) { 102 | $configValue = &$this->configData[$key]; 103 | if (is_array($value) && is_array($configValue)) { 104 | $this->configData[$key] = array_merge($value, $configValue); 105 | } 106 | } elseif (!isset($this->configData[$key])) { 107 | $this->configData[$key] = $value; 108 | } 109 | } 110 | 111 | $this->clearIndexCache($key); 112 | } 113 | } 114 | 115 | return $this; 116 | } 117 | 118 | /** 119 | * 获取指定配置 120 | * 121 | * @param string $index 122 | * @param string|array $options 123 | * @return string|array 124 | * @see CrossArray::get() 125 | */ 126 | function get(string $index, $options = '') 127 | { 128 | $key = $this->getIndexCacheKey($index); 129 | if (is_array($options)) { 130 | $opk = implode('.', $options); 131 | } elseif ($options) { 132 | $opk = $options; 133 | } else { 134 | $opk = '-###-'; 135 | } 136 | 137 | if (!isset(self::$cache[$key][$opk])) { 138 | self::$cache[$key][$opk] = $this->ca->get($index, $options); 139 | } 140 | 141 | return self::$cache[$key][$opk]; 142 | } 143 | 144 | /** 145 | * 路径查找 146 | * 147 | * @param string $path 148 | * @return mixed 149 | */ 150 | function query(string $path) 151 | { 152 | $val = null; 153 | $data = $this->configData; 154 | if (isset($data[$path])) { 155 | return $data[$path]; 156 | } 157 | 158 | $keys = explode('.', $path); 159 | while ($i = array_shift($keys)) { 160 | if (!isset($data[$i])) { 161 | $val = null; 162 | break; 163 | } 164 | 165 | $data = $val = $data[$i]; 166 | } 167 | 168 | return $val; 169 | } 170 | 171 | /** 172 | * 更新指定配置 173 | * 174 | * @param string $index 175 | * @param array|string $values 176 | * @see CrossArray::get() 177 | */ 178 | function set(string $index, $values = ''): void 179 | { 180 | $this->ca->set($index, $values); 181 | $this->clearIndexCache($index); 182 | } 183 | 184 | /** 185 | * 路径更新 186 | * 187 | * @param string $path 188 | * @param $value 189 | */ 190 | function update(string $path, $value) 191 | { 192 | $data = &$this->configData; 193 | $keys = explode('.', $path); 194 | while ($i = array_shift($keys)) { 195 | if (!isset($data[$i]) || !is_array($data[$i])) { 196 | $data[$i] = []; 197 | } 198 | 199 | $data = &$data[$i]; 200 | } 201 | 202 | $data = $value; 203 | } 204 | 205 | /** 206 | * 返回全部配置数据 207 | * 208 | * @param bool $obj 是否返回对象 209 | * @return array|object 210 | * @see CrossArray::getAll() 211 | */ 212 | function getAll($obj = false) 213 | { 214 | if ($obj) { 215 | return CrossArray::arrayToObject($this->configData); 216 | } 217 | 218 | return $this->configData; 219 | } 220 | 221 | /** 222 | * 获取数组索引缓存key 223 | * 224 | * @param string $index 225 | * @return string 226 | */ 227 | protected function getIndexCacheKey(string $index): string 228 | { 229 | return $this->file . '.' . $index; 230 | } 231 | 232 | /** 233 | * 清除缓存 234 | * 235 | * @param string $index 236 | */ 237 | protected function clearIndexCache(string $index): void 238 | { 239 | $key = $this->getIndexCacheKey($index); 240 | unset(self::$cache[$key]); 241 | } 242 | } 243 | -------------------------------------------------------------------------------- /src/Core/CrossArray.php: -------------------------------------------------------------------------------- 1 | 13 | * Class CrossArray 14 | * @package Cross\Core 15 | */ 16 | class CrossArray 17 | { 18 | /** 19 | * @var array 数据 20 | */ 21 | protected $data; 22 | 23 | /** 24 | * @var self 25 | */ 26 | protected static $instance; 27 | 28 | /** 29 | * CrossArray 30 | * 31 | * @param array $data 32 | */ 33 | private function __construct(array &$data) 34 | { 35 | $this->data = &$data; 36 | } 37 | 38 | /** 39 | * @param array $data 40 | * @param string|null $cacheKey 41 | * @return CrossArray 42 | */ 43 | static function init(array &$data, string $cacheKey = null): self 44 | { 45 | if (null === $cacheKey) { 46 | $cacheKey = md5(json_encode($data)); 47 | } 48 | 49 | if (!isset(self::$instance[$cacheKey])) { 50 | self::$instance[$cacheKey] = new self($data); 51 | } 52 | 53 | return self::$instance[$cacheKey]; 54 | } 55 | 56 | /** 57 | * 获取配置参数 58 | * 59 | * @param string $config 60 | * @param string|array $name 61 | * @return bool|string|array 62 | */ 63 | function get(string $config, $name = '') 64 | { 65 | if (isset($this->data[$config])) { 66 | if ($name) { 67 | if (is_array($name)) { 68 | $result = []; 69 | foreach ($name as $n) { 70 | if (isset($this->data[$config][$n])) { 71 | $result[$n] = $this->data[$config][$n]; 72 | } 73 | } 74 | return $result; 75 | } elseif (isset($this->data[$config][$name])) { 76 | return $this->data[$config][$name]; 77 | } 78 | 79 | return false; 80 | } 81 | 82 | return $this->data[$config]; 83 | } 84 | return false; 85 | } 86 | 87 | /** 88 | * 更新成员或赋值 89 | * 90 | * @param string $index 91 | * @param string|array $values 92 | */ 93 | function set(string $index, $values = ''): void 94 | { 95 | if (is_array($values)) { 96 | if (isset($this->data[$index])) { 97 | $this->data[$index] = array_merge($this->data[$index], $values); 98 | } else { 99 | $this->data[$index] = $values; 100 | } 101 | } else { 102 | $this->data[$index] = $values; 103 | } 104 | } 105 | 106 | /** 107 | * 返回全部数据 108 | * 109 | * @param bool $obj 是否返回对象 110 | * @return array|object 111 | */ 112 | function getAll(bool $obj = false) 113 | { 114 | if ($obj) { 115 | return self::arrayToObject($this->data); 116 | } 117 | 118 | return $this->data; 119 | } 120 | 121 | /** 122 | * 数组转对象 123 | * 124 | * @param $data 125 | * @return object|string 126 | */ 127 | static function arrayToObject($data) 128 | { 129 | if (is_array($data)) { 130 | return (object)array_map('self::arrayToObject', $data); 131 | } else { 132 | return $data; 133 | } 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/Core/HttpAuth.php: -------------------------------------------------------------------------------- 1 | 22 | * Class HttpAuth 23 | * @package Cross\Core 24 | */ 25 | class HttpAuth 26 | { 27 | /** 28 | * @var HttpAuthInterface|object 29 | */ 30 | static $authHandler; 31 | 32 | /** 33 | * 创建用于会话管理的对象 34 | * 35 | * @param string|object $type 36 | *
37 |      *  type 默认为字符串(COOKIE|SESSION|REDIS|包含命名空间的类的路径)
38 |      *  也可以是一个实现了HttpAuthInterface接口的对象
39 |      * 
40 | * @param string $authKey 加解密密钥 41 | * @return HttpAuthInterface|object 42 | * @throws CoreException 43 | */ 44 | public static function factory($type = 'cookie', string $authKey = ''): object 45 | { 46 | if (!self::$authHandler) { 47 | if (is_string($type)) { 48 | if (strcasecmp($type, 'cookie') == 0) { 49 | self::$authHandler = new CookieAuth($authKey); 50 | } elseif (strcasecmp($type, 'session') == 0) { 51 | self::$authHandler = new SessionAuth(); 52 | } elseif (strcasecmp($type, 'redis') == 0) { 53 | self::$authHandler = new RedisAuth(); 54 | } else { 55 | try { 56 | $object = new ReflectionClass($type); 57 | if ($object->implementsInterface('Cross\I\HttpAuthInterface')) { 58 | self::$authHandler = $object->newInstance(); 59 | } else { 60 | throw new CoreException('The auth class must implement the HttpAuthInterface interface.'); 61 | } 62 | } catch (Exception $e) { 63 | throw new CoreException('Reflection ' . $e->getMessage()); 64 | } 65 | } 66 | } elseif (is_object($type)) { 67 | if ($type instanceof HttpAuthInterface) { 68 | self::$authHandler = $type; 69 | } else { 70 | throw new CoreException('The auth class must implement the HttpAuthInterface interface.'); 71 | } 72 | } else { 73 | throw new CoreException('Unrecognized auth classes!'); 74 | } 75 | } 76 | return self::$authHandler; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/Core/Loader.php: -------------------------------------------------------------------------------- 1 | 15 | * Class Loader 16 | * @package Cross\Core 17 | */ 18 | class Loader 19 | { 20 | /** 21 | * @var self 22 | */ 23 | private static $instance; 24 | 25 | /** 26 | * 已注册的命名空间 27 | * 28 | * @var array 29 | */ 30 | private static $namespace = []; 31 | 32 | /** 33 | * 已加载类的文件列表 34 | * 35 | * @var array 36 | */ 37 | private static $loaded = []; 38 | 39 | /** 40 | * 初始化Loader 41 | */ 42 | private function __construct() 43 | { 44 | spl_autoload_register([$this, 'loadClass']); 45 | spl_autoload_register([$this, 'loadPSRClass']); 46 | } 47 | 48 | /** 49 | * 单例模式 50 | * 51 | * @return Loader 52 | */ 53 | static function init(): self 54 | { 55 | if (!self::$instance) { 56 | self::$instance = new self(); 57 | } 58 | 59 | return self::$instance; 60 | } 61 | 62 | /** 63 | * 载入文件 64 | * 65 | * @param array|string $file 66 | * @return mixed 67 | * @throws CoreException 68 | */ 69 | static function import($file) 70 | { 71 | return self::requireFile(PROJECT_REAL_PATH . $file, true); 72 | } 73 | 74 | /** 75 | * 读取指定的单一文件 76 | * 77 | * @param string $file 78 | * @param bool $getFileContent 是否读取文件文本内容 79 | * @return mixed 80 | * @throws CoreException 81 | */ 82 | static function read(string $file, bool $getFileContent = false) 83 | { 84 | if (!file_exists($file)) { 85 | throw new CoreException("{$file} 文件不存在"); 86 | } 87 | 88 | static $cache = null; 89 | $flag = (int)$getFileContent; 90 | if (isset($cache[$file][$flag])) { 91 | return $cache[$file][$flag]; 92 | } 93 | 94 | if (is_readable($file)) { 95 | if ($getFileContent) { 96 | $fileContent = file_get_contents($file); 97 | $cache[$file][$flag] = $fileContent; 98 | return $fileContent; 99 | } 100 | 101 | switch (Helper::getExt($file)) { 102 | case 'php' : 103 | $data = require $file; 104 | $cache[$file][$flag] = $data; 105 | break; 106 | 107 | case 'json' : 108 | $data = json_decode(file_get_contents($file), true); 109 | $cache[$file][$flag] = $data; 110 | break; 111 | 112 | case 'ini': 113 | $data = parse_ini_file($file, true); 114 | $cache[$file][$flag] = $data; 115 | break; 116 | 117 | default : 118 | throw new CoreException('不支持的解析格式'); 119 | } 120 | 121 | return $data; 122 | } else { 123 | throw new CoreException("读取文件失败:{$file}"); 124 | } 125 | } 126 | 127 | /** 128 | * 获取已注册的命名空间 129 | * 130 | * @return array 131 | */ 132 | static function getNamespaceMap(): array 133 | { 134 | return self::$namespace; 135 | } 136 | 137 | /** 138 | * 注册命名空间 139 | * 140 | * @param string $prefix 名称 141 | * @param string $baseDir 源文件绝对路径 142 | * @param bool $prepend 143 | */ 144 | static function registerNamespace(string $prefix, string $baseDir, bool $prepend = false): void 145 | { 146 | $prefix = trim($prefix, '\\') . '\\'; 147 | $baseDir = rtrim($baseDir, DIRECTORY_SEPARATOR) . '/'; 148 | if (isset(self::$namespace[$prefix]) === false) { 149 | self::$namespace[$prefix] = []; 150 | } 151 | 152 | if ($prepend) { 153 | array_unshift(self::$namespace[$prefix], $baseDir); 154 | } else { 155 | array_push(self::$namespace[$prefix], $baseDir); 156 | } 157 | } 158 | 159 | /** 160 | * 自动加载 161 | * 162 | * @param string $className 163 | * @return bool|string 164 | * @throws CoreException 165 | */ 166 | private function loadClass(string $className) 167 | { 168 | $prefix = ''; 169 | $pos = strpos($className, '\\'); 170 | if (false !== $pos) { 171 | $prefix = substr($className, 0, $pos); 172 | $className = str_replace('\\', DIRECTORY_SEPARATOR, $className); 173 | } 174 | 175 | $checkFileExists = true; 176 | if ('' !== $prefix && 0 === strcasecmp($prefix, 'cross')) { 177 | $checkFileExists = false; 178 | $classFile = CP_PATH . substr($className, $pos + 1) . '.php'; 179 | } else { 180 | $classFile = PROJECT_REAL_PATH . $className . '.php'; 181 | } 182 | 183 | $this->requireFile($classFile, false, $checkFileExists); 184 | return $classFile; 185 | } 186 | 187 | /** 188 | * PSR-4 189 | * 190 | * @param string $class 191 | * @return bool|string 192 | * @throws CoreException 193 | */ 194 | private function loadPSRClass(string $class) 195 | { 196 | $prefix = $class; 197 | while (false !== $pos = strrpos($prefix, '\\')) { 198 | $prefix = substr($class, 0, $pos + 1); 199 | $relativeClass = substr($class, $pos + 1); 200 | 201 | $mappedFile = $this->loadMappedFile($prefix, $relativeClass); 202 | if ($mappedFile) { 203 | return $mappedFile; 204 | } 205 | $prefix = rtrim($prefix, '\\'); 206 | } 207 | 208 | return false; 209 | } 210 | 211 | /** 212 | * 匹配已注册的命名空间,require文件 213 | * 214 | * @param string $prefix 215 | * @param string $relativeClass 216 | * @return bool|string 217 | * @throws CoreException 218 | */ 219 | private function loadMappedFile(string $prefix, string $relativeClass) 220 | { 221 | if (isset(self::$namespace[$prefix]) === false) { 222 | return false; 223 | } 224 | 225 | foreach (self::$namespace[$prefix] as $baseDir) { 226 | $file = $baseDir . str_replace('\\', '/', $relativeClass) . '.php'; 227 | if ($this->requireFile($file)) { 228 | return $file; 229 | } 230 | } 231 | 232 | return false; 233 | } 234 | 235 | /** 236 | * require文件 237 | * 238 | * @param string $file 239 | * @param bool $throwException 240 | * @param bool $checkFileExists 241 | * @return bool 242 | * @throws CoreException 243 | */ 244 | private static function requireFile(string $file, bool $throwException = false, bool $checkFileExists = true): bool 245 | { 246 | if (isset(self::$loaded[$file])) { 247 | return true; 248 | } elseif ($checkFileExists === false) { 249 | require $file; 250 | self::$loaded[$file] = true; 251 | return true; 252 | } elseif (is_file($file)) { 253 | require $file; 254 | self::$loaded[$file] = true; 255 | return true; 256 | } elseif ($throwException) { 257 | throw new CoreException("未找到要载入的文件:{$file}"); 258 | } else { 259 | return false; 260 | } 261 | } 262 | 263 | } 264 | -------------------------------------------------------------------------------- /src/Core/Rest.php: -------------------------------------------------------------------------------- 1 | 22 | * Class Rest 23 | * @package Cross\Core 24 | */ 25 | class Rest 26 | { 27 | /** 28 | * @var Request 29 | */ 30 | protected $request; 31 | 32 | /** 33 | * @var Delegate 34 | */ 35 | protected $delegate; 36 | 37 | /** 38 | * @var string 39 | */ 40 | protected $requestType; 41 | 42 | /** 43 | * @var string 44 | */ 45 | protected $requestString; 46 | 47 | /** 48 | * 匹配失败时是否兼容MVC模式 49 | * 50 | * @var bool 51 | */ 52 | protected $compatibleModel = false; 53 | 54 | /** 55 | * @var Rest 56 | */ 57 | private static $instance; 58 | 59 | /** 60 | * 初始化request 61 | * 62 | * @param Delegate $delegate 63 | */ 64 | private function __construct(Delegate &$delegate) 65 | { 66 | $this->delegate = $delegate; 67 | $this->request = $delegate->getRequest(); 68 | $this->requestType = strtoupper($this->request->getRequestType()); 69 | $this->requestString = $delegate->getRouter()->getUriRequest('/', $useless, false); 70 | } 71 | 72 | /** 73 | * 创建rest实例 74 | * 75 | * @param Delegate $delegate 76 | * @return Rest 77 | */ 78 | static function getInstance(Delegate &$delegate): self 79 | { 80 | if (!self::$instance) { 81 | self::$instance = new Rest($delegate); 82 | } 83 | 84 | return self::$instance; 85 | } 86 | 87 | /** 88 | * GET 89 | * 90 | * @param string $customRouter 91 | * @param mixed $handle 92 | * @throws CoreException 93 | */ 94 | function get(string $customRouter, $handle): void 95 | { 96 | $this->addCustomRouter('GET', $customRouter, $handle); 97 | } 98 | 99 | /** 100 | * POST 101 | * 102 | * @param string $customRouter 103 | * @param mixed $handle 104 | * @throws CoreException 105 | */ 106 | function post(string $customRouter, $handle): void 107 | { 108 | $this->addCustomRouter('POST', $customRouter, $handle); 109 | } 110 | 111 | /** 112 | * PUT 113 | * 114 | * @param string $customRouter 115 | * @param mixed $handle 116 | * @throws CoreException 117 | */ 118 | function put(string $customRouter, $handle): void 119 | { 120 | $this->addCustomRouter('PUT', $customRouter, $handle); 121 | } 122 | 123 | /** 124 | * PATCH 125 | * 126 | * @param string $customRouter 127 | * @param mixed $handle 128 | * @throws CoreException 129 | */ 130 | function patch(string $customRouter, $handle): void 131 | { 132 | $this->addCustomRouter('PATCH', $customRouter, $handle); 133 | } 134 | 135 | /** 136 | * OPTIONS 137 | * 138 | * @param string $customRouter 139 | * @param mixed $handle 140 | * @throws CoreException 141 | */ 142 | function options(string $customRouter, $handle): void 143 | { 144 | $this->addCustomRouter('OPTIONS', $customRouter, $handle); 145 | } 146 | 147 | /** 148 | * DELETE 149 | * 150 | * @param string $customRouter 151 | * @param mixed $handle 152 | * @throws CoreException 153 | */ 154 | function delete(string $customRouter, $handle): void 155 | { 156 | $this->addCustomRouter('DELETE', $customRouter, $handle); 157 | } 158 | 159 | /** 160 | * HEAD 161 | * 162 | * @param string $customRouter 163 | * @param mixed $handle 164 | * @throws CoreException 165 | */ 166 | function head(string $customRouter, $handle): void 167 | { 168 | $this->addCustomRouter('HEAD', $customRouter, $handle); 169 | } 170 | 171 | /** 172 | * Any 173 | * 174 | * @param string $customRouter 175 | * @param mixed $handle 176 | * @throws CoreException 177 | */ 178 | function any(string $customRouter, $handle): void 179 | { 180 | $this->addCustomRouter($this->requestType, $customRouter, $handle); 181 | } 182 | 183 | /** 184 | * on 185 | * 186 | * @param string $name 187 | * @param Closure $f 188 | * @return Delegate 189 | * @see Delegate::on() 190 | */ 191 | function on(string $name, Closure $f): Delegate 192 | { 193 | return $this->delegate->on($name, $f); 194 | } 195 | 196 | /** 197 | * 匹配失败后是否兼容MVC模式 198 | * 199 | * @param bool $compatible 200 | */ 201 | function compatibleModel(bool $compatible = true): void 202 | { 203 | $this->compatibleModel = $compatible; 204 | } 205 | 206 | /** 207 | * 参数正则验证规则 208 | * 209 | * @param array $rules 210 | */ 211 | function rules(array $rules): void 212 | { 213 | $this->delegate->getRequestMapping()->setRules($rules); 214 | } 215 | 216 | /** 217 | * 处理请求 218 | * 219 | * @throws CoreException|FrontException 220 | */ 221 | function run(): void 222 | { 223 | $params = []; 224 | $match = $this->delegate->getRequestMapping()->match($this->requestString, $handle, $params); 225 | if ($match && $handle instanceof Closure) { 226 | $this->response($handle, $params); 227 | } elseif ($match && is_string($handle)) { 228 | $this->delegate->get($handle, $params); 229 | } elseif ($this->compatibleModel) { 230 | $this->delegate->run(); 231 | } else { 232 | $closureContainer = $this->delegate->getClosureContainer(); 233 | if ($closureContainer->has('mismatching')) { 234 | $closureContainer->run('mismatching'); 235 | } else { 236 | throw new CoreException('Not match uri'); 237 | } 238 | } 239 | } 240 | 241 | /** 242 | * 输出结果 243 | * 244 | * @param Closure $handle 245 | * @param array $params 246 | * @throws CoreException 247 | */ 248 | private function response(Closure $handle, array $params = []): void 249 | { 250 | try { 251 | $ref = new ReflectionFunction($handle); 252 | $parameters = $ref->getParameters(); 253 | 254 | $closureParams = []; 255 | if (!empty($parameters)) { 256 | foreach ($parameters as $p) { 257 | if (!isset($params[$p->name]) && !$p->isOptional()) { 258 | throw new CoreException("Callback closure need param: {$p->name}"); 259 | } 260 | 261 | $closureParams[$p->name] = new DataFilter($params[$p->name] ?? $p->getDefaultValue()); 262 | } 263 | } 264 | 265 | $content = call_user_func_array($handle, $closureParams); 266 | if (null != $content) { 267 | $this->delegate->getResponse()->setRawContent($content)->send(); 268 | } 269 | } catch (Exception $e) { 270 | throw new CoreException($e->getMessage()); 271 | } 272 | } 273 | 274 | /** 275 | * 解析自定义路由并保存参数key 276 | * 277 | * @param string $requestType 278 | * @param string $customRouter 279 | * @param mixed $handle 280 | * @throws CoreException 281 | */ 282 | private function addCustomRouter(string $requestType, string $customRouter, $handle): void 283 | { 284 | $this->delegate->getRequestMapping()->addRequestRouter($requestType, $customRouter, $handle); 285 | } 286 | } 287 | -------------------------------------------------------------------------------- /src/DB/Connector/BaseConnector.php: -------------------------------------------------------------------------------- 1 | 16 | * Class BaseConnector 17 | * @package Cross\DB\Connector 18 | */ 19 | abstract class BaseConnector implements PDOConnector 20 | { 21 | /** 22 | * @var PDO 23 | */ 24 | protected $pdo; 25 | 26 | /** 27 | * @var string 28 | */ 29 | protected $sequence; 30 | 31 | /** 32 | * 合并用户输入的options 33 | * 34 | * @param array $defaultOptions 35 | * @param array $options 36 | * @return array 37 | */ 38 | protected static function getOptions(array $defaultOptions, array $options): array 39 | { 40 | if (!empty($options)) { 41 | foreach ($options as $optionKey => $optionVal) { 42 | $defaultOptions[$optionKey] = $optionVal; 43 | } 44 | } 45 | 46 | return $defaultOptions; 47 | } 48 | 49 | /** 50 | * 设置序号 51 | * 52 | * @param string $sequence 53 | */ 54 | public function setSequence(string $sequence): void 55 | { 56 | $this->sequence = $sequence; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/DB/Connector/MySQLConnector.php: -------------------------------------------------------------------------------- 1 | 17 | * Class MySQLConnector 18 | * @package Cross\DB\Connector 19 | */ 20 | class MySQLConnector extends BaseConnector 21 | { 22 | /** 23 | * 数据库连接实例 24 | * 25 | * @var object 26 | */ 27 | private static $instance; 28 | 29 | /** 30 | * 默认连接参数 31 | * 39 | * 40 | * @var array 41 | */ 42 | private static $options = [ 43 | PDO::ATTR_PERSISTENT => false, 44 | PDO::ATTR_EMULATE_PREPARES => false, 45 | PDO::MYSQL_ATTR_USE_BUFFERED_QUERY => true, 46 | PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, 47 | PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8mb4' 48 | ]; 49 | 50 | /** 51 | * 创建Mysql的PDO连接 52 | * 53 | * @param string $dsn dsn 54 | * @param string $user 数据库用户名 55 | * @param string $password 数据库密码 56 | * @param array $options 57 | * @throws DBConnectException 58 | */ 59 | private function __construct(string $dsn, string $user, string $password, array $options = []) 60 | { 61 | try { 62 | $this->pdo = new PDO($dsn, $user, $password, parent::getOptions(self::$options, $options)); 63 | } catch (Exception $e) { 64 | throw new DBConnectException($e->getMessage()); 65 | } 66 | } 67 | 68 | /** 69 | * 单例模式连接数据库 70 | * 71 | * @param string $dsn 72 | * @param string $user 73 | * @param string $password 74 | * @param array $options 75 | * @return mixed 76 | * @throws DBConnectException 77 | */ 78 | static function getInstance(string $dsn, string $user, string $password, array $options): self 79 | { 80 | //同时建立多个连接时候已dsn的md5值为key 81 | $key = md5($dsn); 82 | if (!isset(self::$instance[$key])) { 83 | self::$instance [$key] = new self($dsn, $user, $password, $options); 84 | } 85 | 86 | return self::$instance [$key]; 87 | } 88 | 89 | /** 90 | * 返回PDO连接的实例 91 | * 92 | * @return PDO 93 | */ 94 | public function getPDO(): PDO 95 | { 96 | return $this->pdo; 97 | } 98 | 99 | /** 100 | * 获取表的主键名 101 | * 102 | * @param string $table 103 | * @return string 104 | */ 105 | public function getPK(string $table): string 106 | { 107 | $tableInfo = $this->getMetaData($table, false); 108 | foreach ($tableInfo as $ti) { 109 | if ($ti['Extra'] == 'auto_increment') { 110 | return $ti['Field']; 111 | } 112 | } 113 | 114 | return ''; 115 | } 116 | 117 | /** 118 | * 最后插入时的id 119 | * 120 | * @return string 121 | */ 122 | function lastInsertId() 123 | { 124 | return $this->pdo->lastInsertId(); 125 | } 126 | 127 | /** 128 | * 获取表的字段信息 129 | * 130 | * @param string $table 131 | * @param bool $fieldsMap 132 | * @return mixed 133 | */ 134 | function getMetaData(string $table, bool $fieldsMap = true): array 135 | { 136 | $data = $this->pdo->query("SHOW FULL FIELDS FROM {$table}"); 137 | try { 138 | if ($fieldsMap) { 139 | $result = []; 140 | $data->fetchAll(PDO::FETCH_FUNC, 141 | function ($field, $type, $collation, $null, $key, $default, $extra, $privileges, $comment) use (&$result) { 142 | $result[$field] = [ 143 | 'primary' => $key == 'PRI', 144 | 'is_index' => $key ? $key : false, 145 | 'auto_increment' => $extra == 'auto_increment', 146 | 'default_value' => strval($default), 147 | 'not_null' => $null == 'NO', 148 | 'comment' => $comment, 149 | 'type' => $type 150 | ]; 151 | }); 152 | return $result; 153 | } else { 154 | return $data->fetchAll(PDO::FETCH_ASSOC); 155 | } 156 | } catch (Exception $e) { 157 | return []; 158 | } 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /src/DB/Connector/OracleConnector.php: -------------------------------------------------------------------------------- 1 | 18 | * Class OracleConnector 19 | * @package Cross\DB\Connector 20 | */ 21 | class OracleConnector extends BaseConnector 22 | { 23 | /** 24 | * 数据库连接实例 25 | * 26 | * @var object 27 | */ 28 | private static $instance; 29 | 30 | /** 31 | * 默认连接参数 32 | * 33 | * @var array 34 | */ 35 | private static $options = [ 36 | PDO::ATTR_PERSISTENT => false, 37 | PDO::ATTR_EMULATE_PREPARES => false, 38 | PDO::MYSQL_ATTR_USE_BUFFERED_QUERY => true, 39 | PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, 40 | 41 | PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES UTF8' 42 | ]; 43 | 44 | /** 45 | * 创建Mysql的PDO连接 46 | * 47 | * @param string $dsn dsn 48 | * @param string $user 数据库用户名 49 | * @param string $password 数据库密码 50 | * @param array $options 51 | * @throws DBConnectException 52 | */ 53 | private function __construct(string $dsn, string $user, string $password, array $options = []) 54 | { 55 | try { 56 | $this->pdo = new PDO($dsn, $user, $password, parent::getOptions(self::$options, $options)); 57 | } catch (Exception $e) { 58 | throw new DBConnectException($e->getMessage()); 59 | } 60 | } 61 | 62 | /** 63 | * 单例模式连接数据库 64 | * 65 | * @param string $dsn 66 | * @param string $user 67 | * @param string $password 68 | * @param array $options 69 | * @return mixed 70 | * @throws DBConnectException 71 | */ 72 | static function getInstance(string $dsn, string $user, string $password, array $options): self 73 | { 74 | //同时建立多个连接时候已dsn的md5值为key 75 | $key = md5($dsn); 76 | if (!isset(self::$instance[$key])) { 77 | self::$instance [$key] = new self($dsn, $user, $password, $options); 78 | } 79 | 80 | return self::$instance [$key]; 81 | } 82 | 83 | /** 84 | * 返回PDO连接的实例 85 | * 86 | * @return PDO 87 | */ 88 | public function getPDO(): PDO 89 | { 90 | return $this->pdo; 91 | } 92 | 93 | /** 94 | * 获取表的主键名 95 | * 96 | * @param string $table 97 | * @return string 98 | */ 99 | public function getPK(string $table): string 100 | { 101 | $table = strtoupper($table); 102 | $q = $this->pdo->prepare("SELECT cu.* FROM all_cons_columns cu, all_constraints au 103 | WHERE cu.constraint_name = au.constraint_name AND au.constraint_type = 'P' AND au.table_name = ?"); 104 | 105 | $pk = ''; 106 | $q->execute([$table]); 107 | $tablePKList = $q->fetchAll(PDO::FETCH_ASSOC); 108 | if (!empty($tablePKList)) { 109 | array_walk($tablePKList, function ($d) use (&$pk) { 110 | //当存在复合主键时,取第一个 111 | if ($d['POSITION'] == 1) { 112 | $pk = $d['COLUMN_NAME']; 113 | return; 114 | } 115 | }); 116 | } 117 | 118 | return $pk; 119 | } 120 | 121 | /** 122 | * 最后插入时的id 123 | * 124 | * @return string 125 | */ 126 | function lastInsertId() 127 | { 128 | if (!empty($this->sequence)) { 129 | try { 130 | $sh = $this->pdo->query("SELECT {$this->sequence}.CURRVAL AS LID FROM DUAL"); 131 | $saveInfo = $sh->fetch(PDO::FETCH_ASSOC); 132 | if (!empty($saveInfo)) { 133 | return $saveInfo['LID']; 134 | } 135 | } catch (Throwable $e) { 136 | 137 | } 138 | } 139 | 140 | return ''; 141 | } 142 | 143 | /** 144 | * 获取表的字段信息 145 | * 146 | * @param string $table 147 | * @param bool $fieldsMap 148 | * @return mixed 149 | */ 150 | function getMetaData(string $table, bool $fieldsMap = true): array 151 | { 152 | //获取所有字段 153 | $table = strtoupper($table); 154 | $q = $this->pdo->prepare("SELECT t.COLUMN_NAME, t.DATA_TYPE, t.NULLABLE, t.DATA_DEFAULT, c.COMMENTS 155 | FROM all_tab_columns t,all_col_comments c 156 | WHERE t.table_name = c.table_name AND t.column_name = c.column_name AND t.table_name = ?"); 157 | 158 | $q->execute([$table]); 159 | $tableFields = $q->fetchAll(PDO::FETCH_ASSOC); 160 | if (empty($tableFields)) { 161 | return []; 162 | } 163 | 164 | $pk = $this->getPK($table); 165 | $qIndex = $this->pdo->prepare("SELECT t.column_name,t.index_name,i.index_type 166 | FROM all_ind_columns t,all_indexes i 167 | WHERE t.index_name = i.index_name AND t.table_name = i.table_name AND t.table_name = ?"); 168 | 169 | $indexInfo = []; 170 | $qIndex->execute([$table]); 171 | $qIndex->fetchAll(PDO::FETCH_FUNC, function ($name, $indexName, $indexType) use (&$indexInfo, $pk) { 172 | $indexInfo[$name] = [ 173 | 'pk' => $pk == $name, 174 | 'index' => $indexName, 175 | 'type' => $indexType 176 | ]; 177 | }); 178 | 179 | $result = []; 180 | array_map(function ($d) use ($indexInfo, &$result, $fieldsMap) { 181 | $autoIncrement = false; 182 | $isPk = $indexInfo[$d['COLUMN_NAME']]['pk'] ?? false; 183 | if ($isPk) { 184 | $dsq = preg_match("~(.*)\.\"(.*)\"\.nextval.*~", $d['DATA_DEFAULT'], $matches); 185 | if ($dsq && !empty($matches[2])) { 186 | $autoIncrement = true; 187 | } 188 | } 189 | 190 | $data = [ 191 | 'primary' => $isPk, 192 | 'is_index' => isset($indexInfo[$d['COLUMN_NAME']]), 193 | 'auto_increment' => $autoIncrement, 194 | 'default_value' => trim($d['DATA_DEFAULT']), 195 | 'not_null' => $d['NULLABLE'] == 'N', 196 | 'comment' => $d['COMMENTS'] 197 | ]; 198 | 199 | if ($fieldsMap) { 200 | $result[$d['COLUMN_NAME']] = $data; 201 | } else { 202 | $data['field'] = $d['COLUMN_NAME']; 203 | $result[] = $data; 204 | } 205 | }, $tableFields); 206 | 207 | return $result; 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /src/DB/Connector/PgSQLConnector.php: -------------------------------------------------------------------------------- 1 | 17 | * Class PgSQLConnector 18 | * @package Cross\DB\Connector 19 | */ 20 | class PgSQLConnector extends BaseConnector 21 | { 22 | /** 23 | * 数据库连接实例 24 | * 25 | * @var object 26 | */ 27 | private static $instance; 28 | 29 | /** 30 | * 默认连接参数 31 | * 32 | * @var array 33 | */ 34 | private static $options = []; 35 | 36 | /** 37 | * 创建PgSQL的PDO连接 38 | * 39 | * @param string $dsn dsn 40 | * @param string $user 数据库用户名 41 | * @param string $password 数据库密码 42 | * @param array $options 43 | * @throws DBConnectException 44 | */ 45 | private function __construct(string $dsn, string $user, string $password, array $options = []) 46 | { 47 | try { 48 | $this->pdo = new PDO($dsn, $user, $password, parent::getOptions(self::$options, $options)); 49 | } catch (Exception $e) { 50 | throw new DBConnectException($e->getMessage()); 51 | } 52 | } 53 | 54 | /** 55 | * @param string $dsn 56 | * @param string $user 57 | * @param string $password 58 | * @param array $options 59 | * @return mixed 60 | * @throws DBConnectException 61 | * @see MysqlModel::__construct 62 | */ 63 | static function getInstance(string $dsn, string $user, string $password, array $options): self 64 | { 65 | //同时建立多个连接时候已dsn的md5值为key 66 | $key = md5($dsn); 67 | if (!isset(self::$instance[$key])) { 68 | self::$instance [$key] = new self($dsn, $user, $password, $options); 69 | } 70 | 71 | return self::$instance [$key]; 72 | } 73 | 74 | /** 75 | * 返回PDO连接的实例 76 | * 77 | * @return PDO 78 | */ 79 | public function getPDO(): PDO 80 | { 81 | return $this->pdo; 82 | } 83 | 84 | /** 85 | * 获取表的主键名 86 | * 87 | * @param string $tableName 88 | * @return string 89 | */ 90 | public function getPK(string $tableName): string 91 | { 92 | $tableInfo = $this->getMetaData($tableName, false); 93 | foreach ($tableInfo as $info) { 94 | if ($info['contype'] == 'p') { 95 | return $info['column_name']; 96 | } 97 | } 98 | return ''; 99 | } 100 | 101 | /** 102 | * 获取最后插入时的ID 103 | * 104 | * @return mixed 105 | */ 106 | public function lastInsertId() 107 | { 108 | $sql = "SELECT LASTVAL() as insert_id"; 109 | try { 110 | $data = $this->pdo->query($sql)->fetch(PDO::FETCH_ASSOC); 111 | return $data['insert_id']; 112 | } catch (Exception $e) { 113 | return false; 114 | } 115 | } 116 | 117 | /** 118 | * 获取表的字段信息 119 | * 120 | * @param string $table 121 | * @param bool $fieldsMap 122 | * @return array 123 | */ 124 | function getMetaData(string $table, bool $fieldsMap = true): array 125 | { 126 | $sql = "SELECT a.column_name, a.is_nullable, a.column_default, p.contype FROM ( 127 | SELECT i.column_name, i.is_nullable, i.column_default, i.ordinal_position, c.oid 128 | FROM information_schema.columns i LEFT JOIN pg_class c ON c.relname=i.table_name 129 | WHERE i.table_name= ? 130 | ) a LEFT JOIN pg_constraint p ON p.conrelid=a.oid AND a.ordinal_position = ANY (p.conkey)"; 131 | 132 | try { 133 | $data = $this->pdo->prepare($sql); 134 | if ($fieldsMap) { 135 | $result = []; 136 | $data->execute([$table]); 137 | $data->fetchAll(PDO::FETCH_FUNC, function ($columnName, $isNull, $columnDefault, $conType) use (&$result) { 138 | $autoIncrement = preg_match("/nextval\((.*)\)/", $columnDefault); 139 | $result[$columnName] = [ 140 | 'primary' => $conType == 'p', 141 | 'auto_increment' => $autoIncrement, 142 | 'default_value' => $autoIncrement ? '' : strval($columnDefault), 143 | 'not_null' => $isNull == 'NO', 144 | ]; 145 | }); 146 | return $result; 147 | } else { 148 | return $data->fetchAll(PDO::FETCH_ASSOC); 149 | } 150 | } catch (Exception $e) { 151 | return []; 152 | } 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /src/DB/Connector/SQLiteConnector.php: -------------------------------------------------------------------------------- 1 | 17 | * Class SQLiteConnector 18 | * @package Cross\DB\Connector 19 | */ 20 | class SQLiteConnector extends BaseConnector 21 | { 22 | /** 23 | * @var PDO 24 | */ 25 | private static $instance; 26 | 27 | /** 28 | * 默认连接参数 29 | * 30 | * @var array 31 | */ 32 | private static $options = []; 33 | 34 | /** 35 | * 创建一个SQLite的PDO连接 36 | * 37 | * @param string $dsn 38 | * @param array $options 39 | * @throws DBConnectException 40 | */ 41 | private function __construct(string $dsn, array $options) 42 | { 43 | try { 44 | $this->pdo = new PDO($dsn, null, null, parent::getOptions(self::$options, $options)); 45 | } catch (Exception $e) { 46 | throw new DBConnectException($e->getMessage()); 47 | } 48 | } 49 | 50 | /** 51 | * @param string $dsn 52 | * @param string $user 53 | * @param string $password 54 | * @param array $options 55 | * @return SQLiteConnector|PDO 56 | * @throws DBConnectException 57 | */ 58 | static function getInstance(string $dsn, string $user, string $password, array $options): self 59 | { 60 | $key = md5($dsn); 61 | if (empty(self::$instance[$key])) { 62 | self::$instance[$key] = new SQLiteConnector($dsn, $options); 63 | } 64 | 65 | return self::$instance[$key]; 66 | } 67 | 68 | /** 69 | * 返回一个PDO连接对象的实例 70 | * 71 | * @return PDO 72 | */ 73 | function getPDO(): PDO 74 | { 75 | return $this->pdo; 76 | } 77 | 78 | /** 79 | * 获取表的主键名 80 | * 81 | * @param string $table 82 | * @return string 83 | */ 84 | function getPK(string $table): string 85 | { 86 | $info = $this->getMetaData($table, false); 87 | if (!empty($info)) { 88 | foreach ($info as $i) { 89 | if ($i['pk'] == 1) { 90 | return $i['name']; 91 | } 92 | } 93 | } 94 | 95 | return ''; 96 | } 97 | 98 | /** 99 | * 最后插入的id 100 | */ 101 | function lastInsertId() 102 | { 103 | return $this->pdo->lastInsertId(); 104 | } 105 | 106 | /** 107 | * 获取表的字段信息 108 | * 109 | * @param string $table 110 | * @param bool $fieldsMap 111 | * @return mixed 112 | */ 113 | function getMetaData(string $table, bool $fieldsMap = true): array 114 | { 115 | $sql = "PRAGMA table_info('{$table}')"; 116 | try { 117 | $data = $this->pdo->query($sql); 118 | if ($fieldsMap) { 119 | $result = []; 120 | $data->fetchAll(PDO::FETCH_FUNC, function ($cid, $name, $type, $notnull, $dfltValue, $pk) use (&$result) { 121 | $result[$name] = [ 122 | 'primary' => $pk == 1, 123 | 'auto_increment' => (bool)(($pk == 1) && ($type == 'INTEGER')), //INTEGER && PRIMARY KEY. 124 | 'default_value' => strval($dfltValue), 125 | 'not_null' => $notnull == 1 126 | ]; 127 | }); 128 | return $result; 129 | } else { 130 | return $data->fetchAll(PDO::FETCH_ASSOC); 131 | } 132 | } catch (Exception $e) { 133 | return []; 134 | } 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/DB/DBFactory.php: -------------------------------------------------------------------------------- 1 | 36 | * Class DBFactory 37 | * @package Cross\DB 38 | */ 39 | class DBFactory 40 | { 41 | /** 42 | * 生成model实例 43 | * 44 | * @param string $type 45 | * @param array|Closure $params 46 | * @param array $config 47 | * @return RedisDriver|CouchDriver|MongoDriver|PDOSqlDriver|mixed 48 | * @throws CoreException 49 | * @throws DBConnectException 50 | */ 51 | static function make(string $type, &$params, array $config = []): object 52 | { 53 | //如果params是一个匿名函数, 则调用匿名函数创建数据库连接 54 | if ($params instanceof Closure) { 55 | return call_user_func_array($params, $config); 56 | } 57 | 58 | //配置的数据表前缀 59 | $prefix = $params['prefix'] ?? ''; 60 | $options = $params['options'] ?? []; 61 | switch (strtolower($type)) { 62 | case 'mysql' : 63 | $Connector = MySQLConnector::getInstance(self::getDsn($params, 'mysql'), $params['user'], $params['pass'], $options); 64 | return new PDOSqlDriver($Connector, new MySQLAssembler($prefix), $params); 65 | 66 | case 'sqlite': 67 | $Connector = SQLiteConnector::getInstance($params['dsn'], null, null, $options); 68 | return new PDOSqlDriver($Connector, new SQLiteAssembler($prefix), $params); 69 | 70 | case 'pgsql': 71 | $Connector = PgSQLConnector::getInstance(self::getDsn($params, 'pgsql'), $params['user'], $params['pass'], $options); 72 | return new PDOSqlDriver($Connector, new PgSQLAssembler($prefix), $params); 73 | 74 | case 'oracle': 75 | $Connector = OracleConnector::getInstance(self::getOracleTns($params), $params['user'], $params['pass'], $options); 76 | return new PDOOracleDriver($Connector, new OracleAssembler($prefix), $params); 77 | 78 | case 'mongo': 79 | return new MongoDriver($params); 80 | 81 | case 'redis': 82 | return new RedisDriver($params); 83 | 84 | case 'memcache': 85 | return new MemcacheDriver($params); 86 | 87 | case 'couch': 88 | return new CouchDriver($params); 89 | 90 | default: 91 | throw new CoreException('不支持的数据库扩展!'); 92 | } 93 | } 94 | 95 | /** 96 | * 生成DSN 97 | * 98 | * @param array $params 99 | * @param string $type 100 | * @param bool|true $useUnixSocket 101 | * @return string 102 | * @throws CoreException 103 | */ 104 | private static function getDsn(array &$params, string $type = 'mysql', bool $useUnixSocket = true): string 105 | { 106 | if (!empty($params['dsn'])) { 107 | return $params['dsn']; 108 | } 109 | 110 | if (!isset($params['host']) || !isset($params['name'])) { 111 | throw new CoreException('连接数据库所需参数不足'); 112 | } 113 | 114 | if ($useUnixSocket && !empty($params['unix_socket'])) { 115 | $dsn = "{$type}:unix_socket={$params['unix_socket']};dbname={$params['name']};"; 116 | } else { 117 | $dsn = "{$type}:host={$params['host']};dbname={$params['name']};"; 118 | if (isset($params['port'])) { 119 | $dsn .= "port={$params['port']};"; 120 | } 121 | 122 | if (isset($params['charset'])) { 123 | $dsn .= "charset={$params['charset']};"; 124 | } 125 | } 126 | 127 | return $dsn; 128 | } 129 | 130 | /** 131 | * 生成tns 132 | * 133 | * @param array $params 134 | * @return string 135 | * @throws CoreException 136 | */ 137 | private static function getOracleTns(array &$params) 138 | { 139 | if (!empty($params['tns'])) { 140 | return $params['tns']; 141 | } 142 | 143 | $params['port'] = $params['port'] ?? 1521; 144 | $params['charset'] = $params['charset'] ?? 'utf8'; 145 | $params['protocol'] = $params['protocol'] ?? 'tcp'; 146 | if (!isset($params['host']) || !isset($params['name'])) { 147 | throw new CoreException('请指定服务器地址和名称'); 148 | } 149 | 150 | $tns = "(DESCRIPTION = 151 | (ADDRESS_LIST = (ADDRESS = (PROTOCOL = %s)(HOST = %s)(PORT = %s))) 152 | (CONNECT_DATA = (SERVICE_NAME = %s)))"; 153 | 154 | $db = sprintf($tns, $params['protocol'], $params['host'], $params['port'], $params['name']); 155 | return "oci:dbname={$db};charset=" . $params['charset']; 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /src/DB/Drivers/CouchDriver.php: -------------------------------------------------------------------------------- 1 | 21 | * Class CouchDriver 22 | * @package Cross\DB\Drivers 23 | */ 24 | class CouchDriver 25 | { 26 | /** 27 | * @var Bucket 28 | */ 29 | protected $link; 30 | 31 | /** 32 | * @param array $params 33 | * @throws CoreException 34 | */ 35 | function __construct(array $params) 36 | { 37 | if (!class_exists('CouchbaseCluster')) { 38 | throw new CoreException('Class CouchbaseCluster not found!'); 39 | } 40 | 41 | try { 42 | $authenticator = new PasswordAuthenticator(); 43 | $authenticator->username($params['username'])->password($params['password']); 44 | 45 | $cluster = new CouchbaseCluster($params['dsn']); 46 | $cluster->authenticate($authenticator); 47 | 48 | $bucket = isset($params['bucket']) ? $params['bucket'] : 'default'; 49 | if (!empty($params['bucket_password'])) { 50 | $this->link = $cluster->openBucket($bucket, $params['bucket_password']); 51 | } else { 52 | $this->link = $cluster->openBucket($bucket); 53 | } 54 | } catch (Exception $e) { 55 | throw new CoreException ($e->getMessage()); 56 | } 57 | } 58 | 59 | /** 60 | * 调用Couch提供的方法 61 | * 62 | * @param $method 63 | * @param $argv 64 | * @return mixed|null 65 | * @throws CoreException 66 | */ 67 | public function __call($method, $argv) 68 | { 69 | $result = null; 70 | if (method_exists($this->link, $method)) { 71 | try { 72 | $result = ($argv == null) 73 | ? $this->link->$method() 74 | : call_user_func_array(array($this->link, $method), $argv); 75 | } catch (Exception $e) { 76 | throw new CoreException($e->getMessage()); 77 | } 78 | } 79 | 80 | return $result; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/DB/Drivers/MongoDriver.php: -------------------------------------------------------------------------------- 1 | 17 | * Class MongoDriver 18 | * @package Cross\DB\Drivers 19 | */ 20 | class MongoDriver 21 | { 22 | /** 23 | * @var Manager 24 | */ 25 | public $manager; 26 | 27 | /** 28 | * 创建MongoDB实例 29 | * 30 | * @param $params 31 | * @throws CoreException 32 | */ 33 | function __construct(array $params) 34 | { 35 | if (!class_exists('MongoDB\Driver\Manager')) { 36 | throw new CoreException('MongoDB\Driver\Manager not found!'); 37 | } 38 | 39 | try { 40 | $options = empty($params['options']) ? [] : $params['options']; 41 | $driverOptions = empty($params['driverOptions']) ? [] : $params['driverOptions']; 42 | 43 | $this->manager = new Manager($params['dsn'], $options, $driverOptions); 44 | } catch (Exception $e) { 45 | throw new CoreException($e->getMessage()); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/DB/Drivers/PDOOracleDriver.php: -------------------------------------------------------------------------------- 1 | 18 | * Class PDOOracleDriver 19 | * @package Cross\DB\Drivers 20 | */ 21 | class PDOOracleDriver extends PDOSqlDriver 22 | { 23 | /** 24 | * 插入数据 25 | * 26 | * @param string $table 27 | * @param string|array $data 28 | * @param bool $multi 是否批量添加 29 | * @param array $insertData 批量添加的数据 30 | * @param bool $openTA 批量添加时是否开启事务 31 | * @return bool|mixed 32 | * @throws CoreException 33 | * @see SQLAssembler::add() 34 | */ 35 | public function add(string $table, $data, bool $multi = false, &$insertData = [], bool $openTA = false) 36 | { 37 | if (empty($data) || !is_array($data)) { 38 | return false; 39 | } 40 | 41 | $apk = null; 42 | $meta = $this->getConnector()->getMetaData($table); 43 | if (!empty($meta)) { 44 | foreach ($meta as $m => $conf) { 45 | if ($conf['primary'] && $conf['auto_increment']) { 46 | $apk = $m; 47 | break; 48 | } 49 | } 50 | } 51 | 52 | //没有自增主键处理流程不变 53 | if (null === $apk) { 54 | return parent::add($table, $data, $multi, $insertData, $openTA); 55 | } 56 | 57 | $fields = []; 58 | $sqlValues = []; 59 | $tableData = $multi ? current($data) : $data; 60 | foreach ($tableData as $key => $name) { 61 | $fields[] = $key; 62 | $sqlValues[] = ":{$key}"; 63 | } 64 | 65 | $insertSqlSegment = sprintf('(%s) VALUES (%s)', implode(',', $fields), implode(',', $sqlValues)); 66 | $rawSql = "INSERT INTO {$table} {$insertSqlSegment} RETURNING {$apk} INTO :lastInsertId"; 67 | if ($multi) { 68 | $count = 0; 69 | if ($openTA) { 70 | $this->beginTA(); 71 | } 72 | 73 | try { 74 | foreach ($data as $d) { 75 | $count++; 76 | $d[$apk] = $this->saveRowData($rawSql, $d); 77 | $insertData[] = $d; 78 | } 79 | } catch (Throwable $e) { 80 | $insertData = []; 81 | if ($openTA) { 82 | $this->rollBack(); 83 | } 84 | throw new CoreException($e->getMessage()); 85 | } 86 | 87 | if ($openTA) { 88 | $this->commit(); 89 | } 90 | 91 | return $count; 92 | } else { 93 | return $this->saveRowData($rawSql, $data); 94 | } 95 | } 96 | 97 | /** 98 | * 保存数据 99 | * 100 | * @param string $rawSql 101 | * @param array $dataRow 102 | * @return int 103 | * @throws CoreException 104 | */ 105 | protected function saveRowData(string $rawSql, array $dataRow): int 106 | { 107 | $lastInsertId = 0; 108 | $stmt = $this->pdo->prepare(trim(trim($rawSql, ';'))); 109 | 110 | foreach ($dataRow as $k => &$v) { 111 | if (is_numeric($v)) { 112 | $stmt->bindParam($k, $v, PDO::PARAM_INT); 113 | } else { 114 | $stmt->bindParam($k, $v, PDO::PARAM_STR); 115 | } 116 | } 117 | 118 | $stmt->bindParam('lastInsertId', $lastInsertId, PDO::PARAM_INT, 20); 119 | $save = $stmt->execute(); 120 | if (!$save) { 121 | throw new CoreException('Save data fail!'); 122 | } 123 | 124 | return $lastInsertId; 125 | } 126 | 127 | } 128 | -------------------------------------------------------------------------------- /src/DB/SQLAssembler/MySQLAssembler.php: -------------------------------------------------------------------------------- 1 | 15 | * Class MySQLAssembler 16 | * @package Cross\DB\SQLAssembler 17 | */ 18 | class MySQLAssembler extends SQLAssembler 19 | { 20 | /** 21 | * @param string $operator 22 | * @param string $field 23 | * @param mixed $fieldConfig 24 | * @param bool $isMixedField 25 | * @param string $conditionConnector 26 | * @param string $connector 27 | * @param array $params 28 | * @return array 29 | * @throws CoreException 30 | * @see SQLAssembler::parseCondition() 31 | * 32 | */ 33 | function parseCondition(string $operator, string $field, $fieldConfig, bool $isMixedField, string $conditionConnector, string $connector, array &$params): array 34 | { 35 | $condition = []; 36 | switch ($connector) { 37 | case 'FIND_IN_SET': 38 | $condition[" {$conditionConnector} "][] = sprintf('FIND_IN_SET(?, %s)', $field); 39 | $params[] = $fieldConfig; 40 | break; 41 | 42 | case 'REGEXP': 43 | $condition[" {$conditionConnector} "][] = sprintf('%s REGEXP(?)', $field); 44 | $params[] = $fieldConfig; 45 | break; 46 | 47 | case 'INSTR': 48 | $condition[" {$conditionConnector} "][] = sprintf('INSTR(%s, ?)', $field); 49 | $params[] = $fieldConfig; 50 | break; 51 | 52 | default: 53 | $condition = parent::parseCondition($operator, $field, $fieldConfig, $isMixedField, $conditionConnector, $connector, $params); 54 | } 55 | 56 | return $condition; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/DB/SQLAssembler/OracleAssembler.php: -------------------------------------------------------------------------------- 1 | 13 | * Class OracleAssembler 14 | * @package Cross\DB\SQLAssembler 15 | */ 16 | class OracleAssembler extends SQLAssembler 17 | { 18 | /** 19 | * 覆盖默认配置 20 | * 21 | * @var string 22 | */ 23 | protected $fieldQuoteChar = ''; 24 | 25 | /** 26 | * 生成分页片段 27 | * 28 | * @param int $p 29 | * @param int $limit 30 | * @return string 31 | */ 32 | protected function getLimitSQLSegment(int $p, int $limit): string 33 | { 34 | //offset 起始位置, 12c以上版本支持 35 | $offset = $limit * ($p - 1); 36 | return "OFFSET {$offset} ROWS FETCH NEXT {$limit} ROWS ONLY"; 37 | } 38 | 39 | /** 40 | * @param int $start 从第几页开始取 41 | * @param int|null $end 每次取多少条 42 | * @return string 43 | */ 44 | public function limit(int $start, int $end = null): string 45 | { 46 | if (null !== $end) { 47 | $limit = max(1, (int)$end); 48 | $offset = $limit * (max(1, (int)$start) - 1); 49 | 50 | $this->offsetIsValid = false; 51 | return "OFFSET {$offset} ROWS FETCH NEXT {$limit} ROWS ONLY"; 52 | } 53 | 54 | $start = (int)$start; 55 | return "FETCH NEXT {$start} ROWS ONLY "; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/DB/SQLAssembler/PgSQLAssembler.php: -------------------------------------------------------------------------------- 1 | 13 | * Class PgSQLAssembler 14 | * @package Cross\DB\SQLAssembler 15 | */ 16 | class PgSQLAssembler extends SQLAssembler 17 | { 18 | /** 19 | * 覆盖默认配置 20 | * 21 | * @var string 22 | */ 23 | protected $fieldQuoteChar = ''; 24 | 25 | /** 26 | * 生成分页SQL片段 27 | * 28 | * @param int $p 29 | * @param int $limit 30 | * @return string 31 | */ 32 | protected function getLimitSQLSegment(int $p, int $limit): string 33 | { 34 | $offset = $limit * ($p - 1); 35 | return "LIMIT {$limit} OFFSET {$offset}"; 36 | } 37 | 38 | /** 39 | * PgSQL的limit如果有第二个参数, 那么和mysql的limit行为保持一致, 并且offset()不生效 40 | * 41 | * @param int $start 42 | * @param int|null $end 43 | * @return string 44 | */ 45 | public function limit(int $start, int $end = null): string 46 | { 47 | if (null !== $end) { 48 | $limit = max(1, $end); 49 | $offset = $limit * (max(1, $start) - 1); 50 | 51 | $this->offsetIsValid = false; 52 | return "LIMIT {$limit} OFFSET {$offset} "; 53 | } 54 | 55 | $start = (int)$start; 56 | return "LIMIT {$start} "; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/DB/SQLAssembler/SQLiteAssembler.php: -------------------------------------------------------------------------------- 1 | 12 | * Class SQLiteAssembler 13 | * @package Cross\DB\SQLAssembler 14 | */ 15 | class SQLiteAssembler extends SQLAssembler 16 | { 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/Exception/CacheException.php: -------------------------------------------------------------------------------- 1 | 14 | * Class CacheException 15 | * @package Cross\Exception 16 | */ 17 | class CacheException extends CrossException 18 | { 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/Exception/CoreException.php: -------------------------------------------------------------------------------- 1 | 14 | * Class CoreException 15 | * @package Cross\Exception 16 | */ 17 | class CoreException extends CrossException 18 | { 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/Exception/CrossException.php: -------------------------------------------------------------------------------- 1 | 22 | * Class CrossException 23 | * @package Cross\Exception 24 | */ 25 | abstract class CrossException extends Exception 26 | { 27 | /** 28 | * HTTP状态码 29 | * 30 | * @var int 31 | */ 32 | protected $httpStatusCode = 500; 33 | 34 | /** 35 | * 是否返回JSON格式的异常信息 36 | * 37 | * @var bool 38 | */ 39 | protected $responseJSONExceptionMsg = false; 40 | 41 | /** 42 | * 响应数据 43 | * 44 | * @var ResponseData 45 | */ 46 | protected $ResponseData; 47 | 48 | /** 49 | * CrossException constructor. 50 | * 51 | * @param string $message 52 | * @param int|null $code 53 | * @param Throwable|null $previous 54 | */ 55 | function __construct(string $message = 'CrossPHP Exception', int $code = null, Throwable $previous = null) 56 | { 57 | if (PHP_SAPI === 'cli') { 58 | set_exception_handler(array($this, 'cliErrorHandler')); 59 | } else { 60 | set_exception_handler(array($this, 'errorHandler')); 61 | } 62 | 63 | $contentType = Response::getInstance()->getContentType(); 64 | if (0 === strcasecmp($contentType, 'JSON')) { 65 | $this->responseJSONExceptionMsg = true; 66 | } 67 | 68 | parent::__construct($message, $code, $previous); 69 | } 70 | 71 | /** 72 | * 根据trace信息分析源码,生成异常处理详细数据 73 | * 74 | * @param Throwable $e 75 | * @return array 76 | */ 77 | function cpExceptionSource(Throwable $e): array 78 | { 79 | $file = $e->getFile(); 80 | $exceptionLine = $e->getLine(); 81 | 82 | $exceptionFileSource = []; 83 | $exceptionFileInfo = new SplFileObject($file); 84 | foreach ($exceptionFileInfo as $line => $code) { 85 | $line += 1; 86 | if ($line <= $exceptionLine + 6 && $line >= $exceptionLine - 6) { 87 | $exceptionFileSource[$line] = self::highlightCode($code); 88 | } 89 | } 90 | 91 | $result['main'] = [ 92 | 'file' => $file, 93 | 'line' => $exceptionLine, 94 | 'message' => $this->hiddenFileRealPath($e->getMessage()), 95 | 'show_file' => $this->hiddenFileRealPath($file), 96 | 'source' => $exceptionFileSource, 97 | ]; 98 | 99 | $trace = $e->getTrace(); 100 | $this->getTraceInfo($trace, $result['trace']); 101 | if ($e->getPrevious()) { 102 | $this->getTraceInfo($e->getPrevious()->getTrace(), $result['previous_trace']); 103 | } 104 | 105 | return $result; 106 | } 107 | 108 | /** 109 | * cli模式下的异常处理 110 | * 111 | * @param Throwable $e 112 | */ 113 | function cliErrorHandler(Throwable $e): void 114 | { 115 | $traceTable = []; 116 | $trace = $e->getTrace(); 117 | $this->getCliTraceInfo($trace, $traceTable); 118 | 119 | $previousTrace = []; 120 | if ($e->getPrevious()) { 121 | $previousTrace = $e->getPrevious()->getTrace(); 122 | $this->getCliTraceInfo($previousTrace, $traceTable); 123 | } 124 | 125 | $result['line'] = $e->getLine(); 126 | $result['file'] = $e->getFile(); 127 | $result['message'] = $e->getMessage(); 128 | 129 | $result['trace'] = $trace; 130 | $result['trace_table'] = $traceTable; 131 | $result['previous_trace'] = $previousTrace; 132 | 133 | Response::getInstance()->setRawContent($result, __DIR__ . '/tpl/cli_error.tpl.php')->send(); 134 | } 135 | 136 | /** 137 | * 异常处理方法 138 | * 139 | * @param Throwable $e 140 | */ 141 | function errorHandler(Throwable $e): void 142 | { 143 | $Response = Response::getInstance(); 144 | if ($this->responseJSONExceptionMsg) { 145 | if (null !== $this->ResponseData) { 146 | $ResponseData = $this->ResponseData; 147 | } else { 148 | $ResponseData = ResponseData::builder(); 149 | $ResponseData->setStatus($e->getCode()); 150 | $ResponseData->setMessage($e->getMessage()); 151 | } 152 | 153 | $Response->setResponseStatus($this->httpStatusCode) 154 | ->setRawContent(json_encode($ResponseData->getData(), JSON_UNESCAPED_UNICODE))->send(); 155 | } else { 156 | $exceptionMsg = $this->cpExceptionSource($e); 157 | $Response->setResponseStatus($this->httpStatusCode) 158 | ->setRawContent($exceptionMsg, __DIR__ . '/tpl/front_error.tpl.php')->send(); 159 | } 160 | } 161 | 162 | /** 163 | * 设置扩展数据 164 | * 165 | * @param mixed $data 166 | */ 167 | function addResponseData(ResponseData $data): void 168 | { 169 | $this->ResponseData = $data; 170 | } 171 | 172 | /** 173 | * 获取扩展数据 174 | * 175 | * @return ResponseData 176 | */ 177 | function getResponseData(): ResponseData 178 | { 179 | return $this->ResponseData; 180 | } 181 | 182 | /** 183 | * 设置HTTP状态码 184 | * 185 | * @param int $code 186 | */ 187 | function setHttpStatusCode(int $code): void 188 | { 189 | $this->httpStatusCode = $code; 190 | } 191 | 192 | /** 193 | * @return int 194 | */ 195 | function getHttpStatusCode(): int 196 | { 197 | return $this->httpStatusCode; 198 | } 199 | 200 | /** 201 | * trace 202 | * 203 | * @param array $trace 204 | * @param $content 205 | */ 206 | protected function getTraceInfo(array $trace, &$content): void 207 | { 208 | if (!empty($trace)) { 209 | $this->alignmentTraceData($trace); 210 | foreach ($trace as $tn => &$t) { 211 | if (!isset($t['file'])) { 212 | continue; 213 | } 214 | 215 | $i = 0; 216 | $traceFileInfo = new SplFileObject($t['file']); 217 | foreach ($traceFileInfo as $line => $code) { 218 | $line += 1; 219 | if (($line <= $t['end_line'] && $line >= $t['start_line']) && $i < 16) { 220 | $t['source'][$line] = self::highlightCode($code); 221 | $i++; 222 | } 223 | } 224 | 225 | $content[] = $t; 226 | } 227 | } 228 | } 229 | 230 | /** 231 | * CLI trace 232 | * 233 | * @param array $trace 234 | * @param $traceTable 235 | */ 236 | protected function getCliTraceInfo(array &$trace, &$traceTable): void 237 | { 238 | if (!empty($trace)) { 239 | $this->alignmentTraceData($trace); 240 | foreach ($trace as &$t) { 241 | foreach ($t as $typeName => &$traceContent) { 242 | switch ($typeName) { 243 | case 'file': 244 | case 'line': 245 | case 'function': 246 | $lineMaxWidth = max(strlen($typeName), strlen($traceContent)); 247 | if (($lineMaxWidth % 2) != 0) { 248 | $lineMaxWidth += 5; 249 | } else { 250 | $lineMaxWidth += 4; 251 | } 252 | 253 | if (!isset($traceTable[$typeName]) || $lineMaxWidth > $traceTable[$typeName]) { 254 | $traceTable[$typeName] = $lineMaxWidth; 255 | } 256 | break; 257 | default: 258 | unset($t[$typeName]); 259 | } 260 | } 261 | } 262 | } 263 | } 264 | 265 | /** 266 | * 隐藏异常中的真实文件路径 267 | * 268 | * @param string $path 269 | * @return mixed 270 | */ 271 | protected function hiddenFileRealPath(string $path): string 272 | { 273 | return str_replace([PROJECT_REAL_PATH, CP_PATH], ['Project->', 'Cross->'], $path); 274 | } 275 | 276 | /** 277 | * 高亮代码 278 | * 279 | * @param string $code 280 | * @return mixed 281 | */ 282 | private static function highlightCode(string $code): string 283 | { 284 | $code = rtrim($code); 285 | if (0 === strcasecmp(substr($code, 0, 5), 'hiddenFileRealPath($t['file']); 303 | $t['start_line'] = max(1, $t['line'] - 6); 304 | $t['end_line'] = $t['line'] + 6; 305 | } elseif (isset($t['function']) && isset($t['class'])) { 306 | try { 307 | $rc = new ReflectionClass($t['class']); 308 | $t['file'] = $rc->getFileName(); 309 | $t['show_file'] = $this->hiddenFileRealPath($rc->getFileName()); 310 | 311 | $rf = new ReflectionMethod($t['class'], $t['function']); 312 | $t['start_line'] = $rf->getStartLine(); 313 | $t['end_line'] = $rf->getEndLine(); 314 | $t['line'] = sprintf("%s ~ %s", $t['start_line'], $t['end_line']); 315 | } catch (Exception $e) { 316 | continue; 317 | } 318 | } else { 319 | continue; 320 | } 321 | } 322 | } 323 | } 324 | -------------------------------------------------------------------------------- /src/Exception/DBConnectException.php: -------------------------------------------------------------------------------- 1 | 14 | * Class DBConnectException 15 | * @package Cross\Exception 16 | */ 17 | class DBConnectException extends CrossException 18 | { 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/Exception/FrontException.php: -------------------------------------------------------------------------------- 1 | 16 | * Class FrontException 17 | * @package Cross\Exception 18 | */ 19 | class FrontException extends CrossException 20 | { 21 | protected $httpStatusCode = 400; 22 | } 23 | -------------------------------------------------------------------------------- /src/Exception/LogicStatusException.php: -------------------------------------------------------------------------------- 1 | 21 | * Class LogicStatusException 22 | * @package Cross\Exception 23 | */ 24 | class LogicStatusException extends CrossException 25 | { 26 | protected $httpStatusCode = 200; 27 | 28 | /** 29 | * LogicStatusException constructor. 30 | * 31 | * @param int|null $code 32 | * @param string|null $msg 33 | * @param string $ajaxCtxType ajax请求响应类型 34 | */ 35 | function __construct(int $code = null, string $msg = null, string $ajaxCtxType = 'json') 36 | { 37 | try { 38 | if (null === $this->ResponseData) { 39 | $rpd = ResponseData::builder(); 40 | $rpd->setStatus($code); 41 | $rpd->setMessage($msg ?? ''); 42 | parent::addResponseData($rpd); 43 | } 44 | 45 | if (Request::getInstance()->isAjaxRequest()) { 46 | Response::getInstance()->setContentType($ajaxCtxType); 47 | } 48 | 49 | parent::__construct($this->getResponseData()->getMessage(), $code); 50 | } catch (Throwable $e) { 51 | parent::__construct($e->getMessage(), $code); 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Exception/tpl/cli_error.tpl.php: -------------------------------------------------------------------------------- 1 | $max_width) { 36 | $max_width = $length; 37 | } 38 | } 39 | 40 | $half_width = floor($line_width / 2 - ($max_width - $offset) / 2); 41 | foreach ($logo_lines as $line) { 42 | for ($i = 0; $i <= $line_width; $i++) { 43 | if ($i == $half_width) { 44 | echo $line; 45 | } elseif ($i < $half_width) { 46 | echo ' '; 47 | } 48 | } 49 | echo PHP_EOL; 50 | } 51 | echo PHP_EOL; 52 | } 53 | } 54 | 55 | /** 56 | * 输出带标题的横线 57 | * 58 | * @param $txtTableInfo 59 | * @param string $text 60 | * @param string $pad_string 61 | */ 62 | if (!function_exists('line')) { 63 | function line($txtTableInfo, $text = '', $pad_string = '=') 64 | { 65 | $text_length = 0; 66 | if (!empty($text)) { 67 | $text_length = strlen($text); 68 | } 69 | 70 | $line_width = array_sum($txtTableInfo) + count($txtTableInfo) + 1; 71 | $s = floor($line_width / 2 - $text_length / 2); 72 | for ($i = 0; $i < $line_width; $i++) { 73 | if ($i == $s) { 74 | echo $text; 75 | $i += $text_length; 76 | } 77 | echo $pad_string; 78 | } 79 | echo PHP_EOL; 80 | } 81 | } 82 | 83 | /** 84 | * 输出表格边框 85 | * 86 | * @param $txtTableInfo 87 | */ 88 | if (!function_exists('th')) { 89 | function th($txtTableInfo) 90 | { 91 | echo '+'; 92 | foreach ($txtTableInfo as $type_name => $line_width) { 93 | for ($i = 0; $i < $line_width; $i++) { 94 | echo '-'; 95 | } 96 | echo '+'; 97 | } 98 | echo PHP_EOL; 99 | } 100 | } 101 | 102 | /** 103 | * 输出txt表格头(标题居中) 104 | * 105 | * @param $txtTableInfo 106 | */ 107 | if (!function_exists('tHead')) { 108 | function tHead($txtTableInfo) 109 | { 110 | echo '|'; 111 | foreach ($txtTableInfo as $type_name => $line_width) { 112 | $name_width = strlen($type_name); 113 | $name_offset = floor($line_width / 2 - $name_width / 2) + 1; 114 | 115 | $i = 0; 116 | while ($line_width > 0 && $i++ < $line_width) { 117 | if ($i == $name_offset) { 118 | echo ucfirst($type_name); 119 | $i += $name_width - 1; 120 | } else { 121 | echo ' '; 122 | } 123 | } 124 | echo '|'; 125 | } 126 | echo PHP_EOL; 127 | } 128 | } 129 | 130 | /** 131 | * 输出表格的内容 132 | * 133 | * @param $data 134 | * @param $txtTableInfo 135 | */ 136 | if (!function_exists('tBody')) { 137 | function tBody($data, $txtTableInfo) 138 | { 139 | echo '|'; 140 | foreach ($txtTableInfo as $type => $line_width) { 141 | $content_length = !empty($data[$type]) ? strlen($data[$type]) : 1; 142 | $i = 0; 143 | while ($line_width > 0 && $i++ < $line_width) { 144 | if ($i == 2) { 145 | echo $data[$type] ?? ' '; 146 | $i += $content_length - 1; 147 | } else { 148 | echo ' '; 149 | } 150 | } 151 | echo '|'; 152 | } 153 | echo PHP_EOL; 154 | } 155 | } 156 | 157 | echo PHP_EOL; 158 | ascLogo($table); 159 | line($table, '-- Exception Start --'); 160 | printf("\n Message: %s \n File: %s Line: %s \n\n", $data['message'], $data['file'], $data['line']); 161 | 162 | th($table); 163 | thead($table); 164 | th($table); 165 | 166 | if (!empty($trace)) { 167 | foreach ($trace as $t) { 168 | tBody($t, $table); 169 | } 170 | th($table); 171 | } 172 | 173 | if (!empty($previous_trace)) { 174 | line($table, 'Previous Trace', ' '); 175 | th($table); 176 | foreach ($previous_trace as $t) { 177 | tBody($t, $table); 178 | } 179 | th($table); 180 | } 181 | 182 | echo PHP_EOL; 183 | line($table, sprintf("-- Exception END %s --", date('Y-m-d H:i:s', time()))); 184 | } 185 | 186 | 187 | -------------------------------------------------------------------------------- /src/I/CacheInterface.php: -------------------------------------------------------------------------------- 1 | 1, 'limit' => 10], $order = null, $groupBy = null); 53 | 54 | /** 55 | * 添加数据 56 | * 57 | * @param string $table 表名 58 | * @param mixed $data 要插入的数据 59 | * @param bool $multi 是否批量插入 60 | * @return mixed 61 | */ 62 | function add(string $table, $data, bool $multi = false); 63 | 64 | /** 65 | * 更新数据 66 | * 67 | * @param string $table 表名 68 | * @param mixed $data 要更新的数据 69 | * @param mixed $where 筛选条件 70 | * @return mixed 71 | */ 72 | function update(string $table, $data, $where); 73 | 74 | /** 75 | * 删除数据 76 | * 77 | * @param string $table 表名 78 | * @param mixed $where 条件 79 | * @return mixed 80 | */ 81 | function del(string $table, $where); 82 | 83 | } 84 | -------------------------------------------------------------------------------- /src/Interactive/ResponseData.php: -------------------------------------------------------------------------------- 1 | resetData(); 89 | } 90 | 91 | /** 92 | * 返回数据 93 | * 94 | * @param bool $getContent 是否获取数据内容 95 | * @return array 96 | */ 97 | public function getData(bool $getContent = true): array 98 | { 99 | $data = [ 100 | $this->statusName => $this->status, 101 | $this->messageName => $this->getMessage() 102 | ]; 103 | 104 | if ($getContent && !empty($this->data)) { 105 | $data[$this->dataName] = $this->data; 106 | } 107 | 108 | return $data; 109 | } 110 | 111 | /** 112 | * 获取数据内容 113 | * 114 | * @return array 115 | */ 116 | public function getDataContent(): array 117 | { 118 | return $this->data; 119 | } 120 | 121 | /** 122 | * 更新状态和消息属性 123 | * 124 | * @param array $data 125 | */ 126 | public function updateInfoProperty(array &$data): void 127 | { 128 | if (isset($data[$this->statusName])) { 129 | $this->setStatus($data[$this->statusName]); 130 | unset($data[$this->statusName]); 131 | } 132 | 133 | if (isset($data[$this->messageName])) { 134 | $this->setMessage($data[$this->messageName]); 135 | unset($data[$this->messageName]); 136 | } 137 | } 138 | 139 | /** 140 | * 数据内容 141 | * 142 | * @param array $data 143 | * @param bool $merge 默认不合并数据 144 | */ 145 | public function setData(array $data, $merge = false): void 146 | { 147 | if ($merge && !empty($this->data)) { 148 | $this->data = array_merge($this->data, $data); 149 | } else { 150 | $this->data = $data; 151 | } 152 | } 153 | 154 | /** 155 | * 添加数据 156 | * 157 | * @param string $key 158 | * @param mixed $value 159 | */ 160 | public function addData(string $key, $value): void 161 | { 162 | $this->data[$key] = $value; 163 | } 164 | 165 | /** 166 | * @param int $status 167 | */ 168 | public function setStatus(int $status): void 169 | { 170 | $this->status = $status; 171 | } 172 | 173 | /** 174 | * @return int 175 | */ 176 | public function getStatus(): int 177 | { 178 | return $this->status; 179 | } 180 | 181 | /** 182 | * @param string $message 183 | */ 184 | public function setMessage(string $message): void 185 | { 186 | $this->message = $message; 187 | } 188 | 189 | /** 190 | * @return string 191 | */ 192 | public function getMessage(): string 193 | { 194 | if ($this->status != 1 && empty($this->message)) { 195 | try { 196 | $statusConfigFile = Delegate::env('sys.status') ?? 'status.config.php'; 197 | if (!file_exists($statusConfigFile) && defined('PROJECT_REAL_PATH')) { 198 | $statusConfigFile = PROJECT_REAL_PATH . 'config' . DIRECTORY_SEPARATOR . $statusConfigFile; 199 | } 200 | $statusMsg = Loader::read($statusConfigFile); 201 | $this->message = $statusMsg[$this->status] ?? sprintf('unknown error(%s)', $this->status); 202 | } catch (CoreException $e) { 203 | $this->message = sprintf('exception(%s)', $e->getMessage()); 204 | } 205 | } 206 | 207 | return $this->message; 208 | } 209 | 210 | /** 211 | * @return string 212 | */ 213 | public function getStatusName(): string 214 | { 215 | return $this->statusName; 216 | } 217 | 218 | /** 219 | * @param string $statusName 220 | * @return string 221 | */ 222 | public function setStatusName(string $statusName): string 223 | { 224 | $this->statusName = $statusName; 225 | return $statusName; 226 | } 227 | 228 | /** 229 | * @return string 230 | */ 231 | public function getMessageName(): string 232 | { 233 | return $this->messageName; 234 | } 235 | 236 | /** 237 | * @param string $messageName 238 | * @return string 239 | */ 240 | public function setMessageName(string $messageName): string 241 | { 242 | $this->messageName = $messageName; 243 | return $messageName; 244 | } 245 | 246 | /** 247 | * @return string 248 | */ 249 | public function getDataName(): string 250 | { 251 | return $this->dataName; 252 | } 253 | 254 | /** 255 | * @param string $dataName 256 | * @return string 257 | */ 258 | public function setDataName(string $dataName): string 259 | { 260 | $this->dataName = $dataName; 261 | return $dataName; 262 | } 263 | 264 | /** 265 | * 重置数据 266 | * 267 | * @return $this 268 | */ 269 | protected function resetData(): self 270 | { 271 | $this->status = 1; 272 | $this->message = ''; 273 | $this->data = []; 274 | return $this; 275 | } 276 | } -------------------------------------------------------------------------------- /src/Lib/Array2XML.php: -------------------------------------------------------------------------------- 1 | saveXML(); 30 | */ 31 | 32 | namespace Cross\Lib; 33 | 34 | use DOMDocument; 35 | use DOMNode; 36 | use Exception; 37 | 38 | class Array2XML 39 | { 40 | 41 | private static $xml = null; 42 | private static $encoding = 'UTF-8'; 43 | 44 | /** 45 | * Initialize the root XML node [optional] 46 | * 47 | * @param $version 48 | * @param $encoding 49 | * @param $formatOutput 50 | */ 51 | public static function init($version = '1.0', $encoding = 'UTF-8', $formatOutput = true) 52 | { 53 | self::$xml = new DomDocument($version, $encoding); 54 | self::$xml->formatOutput = $formatOutput; 55 | self::$encoding = $encoding; 56 | } 57 | 58 | /** 59 | * Convert an Array to XML 60 | * 61 | * @param mixed $nodeName - name of the root node to be converted 62 | * @param array $arr - array to be converterd 63 | * @return DomDocument 64 | * @throws Exception 65 | */ 66 | public static function &createXML($nodeName, $arr = []) 67 | { 68 | $xml = self::getXMLRoot(); 69 | $xml->appendChild(self::convert($nodeName, $arr)); 70 | 71 | self::$xml = null; // clear the xml node in the class for 2nd time use. 72 | return $xml; 73 | } 74 | 75 | /** 76 | * Convert an Array to XML 77 | * 78 | * @param mixed $nodeName - name of the root node to be converted 79 | * @param array $arr - aray to be converterd 80 | * @throws Exception 81 | * @return DOMNode 82 | */ 83 | private static function &convert($nodeName, $arr = []) 84 | { 85 | 86 | //print_arr($nodeName); 87 | $xml = self::getXMLRoot(); 88 | $node = $xml->createElement($nodeName); 89 | 90 | if (is_array($arr)) { 91 | // get the attributes first.; 92 | if (isset($arr['@attributes'])) { 93 | foreach ($arr['@attributes'] as $key => $value) { 94 | if (!self::isValidTagName($key)) { 95 | throw new Exception('[Array2XML] Illegal character in attribute name. attribute: ' . $key . ' in node: ' . $nodeName); 96 | } 97 | $node->setAttribute($key, self::bool2str($value)); 98 | } 99 | unset($arr['@attributes']); //remove the key from the array once done. 100 | } 101 | 102 | // check if it has a value stored in @value, if yes store the value and return 103 | // else check if its directly stored as string 104 | if (isset($arr['@value'])) { 105 | $node->appendChild($xml->createTextNode(self::bool2str($arr['@value']))); 106 | unset($arr['@value']); //remove the key from the array once done. 107 | //return from recursion, as a note with value cannot have child nodes. 108 | return $node; 109 | } 110 | else if (isset($arr['@cdata'])) { 111 | $node->appendChild($xml->createCDATASection(self::bool2str($arr['@cdata']))); 112 | unset($arr['@cdata']); //remove the key from the array once done. 113 | //return from recursion, as a note with cdata cannot have child nodes. 114 | return $node; 115 | } 116 | } 117 | 118 | //create subnodes using recursion 119 | if (is_array($arr)) { 120 | // recurse to get the node for that key 121 | foreach ($arr as $key => $value) { 122 | if (!self::isValidTagName($key)) { 123 | throw new Exception('[Array2XML] Illegal character in tag name. tag: ' . $key . ' in node: ' . $nodeName); 124 | } 125 | if (is_array($value) && is_numeric(key($value))) { 126 | // MORE THAN ONE NODE OF ITS KIND; 127 | // if the new array is numeric index, means it is array of nodes of the same kind 128 | // it should follow the parent key name 129 | foreach ($value as $k => $v) { 130 | $node->appendChild(self::convert($key, $v)); 131 | } 132 | } 133 | else { 134 | // ONLY ONE NODE OF ITS KIND 135 | $node->appendChild(self::convert($key, $value)); 136 | } 137 | unset($arr[$key]); //remove the key from the array once done. 138 | } 139 | } 140 | 141 | // after we are done with all the keys in the array (if it is one) 142 | // we check if it has any text value, if yes, append it. 143 | if (!is_array($arr)) { 144 | $node->appendChild($xml->createTextNode(self::bool2str($arr))); 145 | } 146 | 147 | return $node; 148 | } 149 | 150 | /* 151 | * Get the root XML node, if there isn't one, create it. 152 | */ 153 | private static function getXMLRoot() 154 | { 155 | if (empty(self::$xml)) { 156 | self::init(); 157 | } 158 | 159 | return self::$xml; 160 | } 161 | 162 | /* 163 | * Get string representation of boolean value 164 | */ 165 | private static function bool2str($v) 166 | { 167 | //convert boolean to text value. 168 | $v = $v === true ? 'true' : $v; 169 | $v = $v === false ? 'false' : $v; 170 | 171 | return $v; 172 | } 173 | 174 | /* 175 | * Check if the tag name or attribute name contains illegal characters 176 | * Ref: http://www.w3.org/TR/xml/#sec-common-syn 177 | */ 178 | private static function isValidTagName($tag) 179 | { 180 | $pattern = '/^[a-z_]+[a-z0-9\:\-\.\_]*[^:]*$/i'; 181 | 182 | return preg_match($pattern, $tag, $matches) && $matches[0] == $tag; 183 | } 184 | } 185 | 186 | -------------------------------------------------------------------------------- /src/Lib/Document/CallTree.php: -------------------------------------------------------------------------------- 1 | 17 | * Class NodeTree 18 | * @package Cross\Lib\Document 19 | */ 20 | class CallTree 21 | { 22 | 23 | private $node = []; 24 | 25 | /** 26 | * @return static 27 | */ 28 | public static function getInstance(): self 29 | { 30 | return new static(); 31 | } 32 | 33 | /** 34 | * 保存调用关系 35 | * 36 | * @param string $nodeName 37 | * @param mixed $nodeArguments 38 | */ 39 | function saveNode(string $nodeName, $nodeArguments): void 40 | { 41 | $this->node = [$nodeName => $nodeArguments]; 42 | } 43 | 44 | /** 45 | * 输出HTML标签 46 | * @param bool $htmlDecode 47 | */ 48 | function html(bool $htmlDecode = true): void 49 | { 50 | echo $this->nodeToHTML($htmlDecode); 51 | } 52 | 53 | /** 54 | * 输出DOM 55 | * 56 | * @return DOMDocument 57 | */ 58 | function dom(): DOMDocument 59 | { 60 | return CallTreeToHTML::getInstance()->getDom($this->getNode()); 61 | } 62 | 63 | /** 64 | * 获取当前node内容 65 | * 66 | * @return array 67 | */ 68 | function getNode(): array 69 | { 70 | return $this->node; 71 | } 72 | 73 | /** 74 | * @return string 75 | * @see nodeToHTML 76 | * 77 | */ 78 | function __toString(): string 79 | { 80 | return $this->nodeToHTML(); 81 | } 82 | 83 | /** 84 | * 把node转换为html 85 | * 86 | * @param bool $htmlDecode 87 | * @return string 88 | */ 89 | private function nodeToHTML(bool $htmlDecode = true): string 90 | { 91 | return CallTreeToHTML::getInstance()->getHTML($this->getNode(), $htmlDecode); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/Lib/Document/CallTreeToHTML.php: -------------------------------------------------------------------------------- 1 | 20 | * Class NodeToHTML 21 | * @package Cross\Lib\Document 22 | */ 23 | class CallTreeToHTML 24 | { 25 | /** 26 | * @var DOMDocument 27 | */ 28 | private $dom; 29 | 30 | /** 31 | * @var DOMNode 32 | */ 33 | private $element; 34 | 35 | private function __construct() 36 | { 37 | $this->dom = new DOMDocument(); 38 | } 39 | 40 | static function getInstance(): self 41 | { 42 | return new self(); 43 | } 44 | 45 | /** 46 | * 返回DOM 47 | * 48 | * @param $node 49 | * @return DOMDocument 50 | */ 51 | function getDom($node): DOMDocument 52 | { 53 | $this->makeNode($node); 54 | $this->dom->appendChild($this->element); 55 | return $this->dom; 56 | } 57 | 58 | /** 59 | * DOM转HTML 60 | * 61 | * @param $node 62 | * @param bool $htmlDecode 63 | * @return string 64 | */ 65 | function getHTML($node, bool $htmlDecode = true): string 66 | { 67 | $dom = $this->getDom($node); 68 | $dom->encoding = 'utf-8'; 69 | $html = $dom->saveHTML($dom->firstChild); 70 | if ($htmlDecode) { 71 | $html = html_entity_decode($html); 72 | } 73 | 74 | return $html; 75 | } 76 | 77 | /** 78 | * 把node转换为dom 79 | * 80 | * @param $node 81 | * @param DOMNode|null $parentElement 82 | */ 83 | function makeNode($node, DOMNode $parentElement = null) 84 | { 85 | $content = null; 86 | $attrSet = []; 87 | 88 | //构造根节点 89 | if (null === $parentElement) { 90 | $rootElementName = current(array_keys($node)); 91 | 92 | $node = current($node); 93 | if (isset($node[0]) && !$node[0] instanceof CallTree) { 94 | if (is_array($node[0])) { 95 | if (isset($node[0]['@content'])) { 96 | $content = $node[0]['@content']; 97 | unset($node[0]['@content']); 98 | } 99 | $attrSet = $node[0]; 100 | } else { 101 | $content = $node[0]; 102 | unset($node[0]); 103 | } 104 | } 105 | 106 | $this->element = $this->dom->createElement($rootElementName, htmlentities($content)); 107 | if (!empty($attrSet)) { 108 | foreach ($attrSet as $attrSetName => $attrSetValue) { 109 | $this->element->setAttribute($attrSetName, $attrSetValue); 110 | } 111 | } 112 | } 113 | 114 | //为parentElement设置属性 115 | if ($parentElement && isset($node[0]) && !$node[0] instanceof CallTree) { 116 | if (!empty($node[0])) { 117 | foreach ($node[0] as $attrSetName => $attrSetValue) { 118 | if ($attrSetValue instanceof Closure) { 119 | $attrSetValue = call_user_func($attrSetValue); 120 | } 121 | $parentElement->setAttribute($attrSetName, $attrSetValue); 122 | } 123 | } 124 | } 125 | 126 | foreach ($node as $n) { 127 | if (!empty($n) && $n instanceof CallTree) { 128 | $nodeDetail = $n->getNode(); 129 | foreach ($nodeDetail as $elementName => $childNode) { 130 | 131 | //获取当前element中的文本内容 132 | if (isset($childNode[0]) && !$childNode[0] instanceof CallTree) { 133 | if (is_array($childNode[0])) { 134 | if (isset($childNode[0]['@content'])) { 135 | $content = $childNode[0]['@content']; 136 | unset($childNode[0]['@content']); 137 | } 138 | } else { 139 | $content = $childNode[0]; 140 | unset($childNode[0]); 141 | } 142 | } 143 | 144 | $element = $this->dom->createElement($elementName, htmlentities($content)); 145 | if ($parentElement instanceof DOMElement) { 146 | $currentElement = $parentElement->appendChild($element); 147 | } else { 148 | $currentElement = $this->element->appendChild($element); 149 | } 150 | 151 | if (!empty($childNode)) { 152 | $this->makeNode($childNode, $currentElement); 153 | } 154 | } 155 | } 156 | } 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /src/Lib/Document/HTML.php: -------------------------------------------------------------------------------- 1 | 15 | * Class HTML 16 | * @package Cross\Lib\Document 17 | * 18 | *
 19 |  * example:
 20 |  *  echo HTML::div('im a div');
 21 |  *  echo HTML::a(['@content'=>'crossphp', 'href'=>'http://www.crossphp.com']);
 22 |  *  echo HTML::div(['@content' => 'im a div', 'style'=>'border:1px solid #dddddd;padding:20px;'],
 23 |  *          HTML::a(['@content'=>'crossphp', 'href'=>'http://www.crossphp.com'])
 24 |  *       );
 25 |  *  echo HTML::form(['method'=>'get'],
 26 |  *          HTML::div(
 27 |  *              HTML::label('User Name:', HTML::input(['type'=>'text'])),
 28 |  *              HTML::label('Password :', HTML::input(['type'=>'password'])),
 29 |  *              HTML::label('          ', HTML::input(['type'=>'submit', 'value'=>'submit']))
 30 |  *          )
 31 |  *       );
 32 |  *
 33 |  * 支持标签列表:
 34 |  * https://developer.mozilla.org/en-US/docs/Web/HTML/Element
 35 |  * 
36 | * 37 | * @method static base($attributes, ...$ctx = null) 38 | * @method static head($attributes, ...$ctx = null) 39 | * @method static link($attributes, ...$ctx = null) 40 | * @method static meta($attributes, ...$ctx = null) 41 | * @method static style($attributes, ...$ctx = null) 42 | * @method static title($attributes, ...$ctx = null) 43 | * @method static body($attributes, ...$ctx = null) 44 | * @method static address($attributes, ...$ctx = null) 45 | * @method static article($attributes, ...$ctx = null) 46 | * @method static aside($attributes, ...$ctx = null) 47 | * @method static footer($attributes, ...$ctx = null) 48 | * @method static header($attributes, ...$ctx = null) 49 | * @method static h1($attributes, ...$ctx = null) 50 | * @method static h2($attributes, ...$ctx = null) 51 | * @method static h3($attributes, ...$ctx = null) 52 | * @method static h4($attributes, ...$ctx = null) 53 | * @method static h5($attributes, ...$ctx = null) 54 | * @method static h6($attributes, ...$ctx = null) 55 | * @method static hgroup($attributes, ...$ctx = null) 56 | * @method static main($attributes, ...$ctx = null) 57 | * @method static nav($attributes, ...$ctx = null) 58 | * @method static section($attributes, ...$ctx = null) 59 | * @method static blockquote($attributes, ...$ctx = null) 60 | * @method static dd($attributes, ...$ctx = null) 61 | * @method static div($attributes, ...$ctx = null) 62 | * @method static dl($attributes, ...$ctx = null) 63 | * @method static dt($attributes, ...$ctx = null) 64 | * @method static figcaption($attributes, ...$ctx = null) 65 | * @method static figure($attributes, ...$ctx = null) 66 | * @method static hr($attributes, ...$ctx = null) 67 | * @method static li($attributes, ...$ctx = null) 68 | * @method static ol($attributes, ...$ctx = null) 69 | * @method static p($attributes, ...$ctx = null) 70 | * @method static pre($attributes, ...$ctx = null) 71 | * @method static ul($attributes, ...$ctx = null) 72 | * @method static a($attributes, ...$ctx = null) 73 | * @method static abbr($attributes, ...$ctx = null) 74 | * @method static b($attributes, ...$ctx = null) 75 | * @method static bdi($attributes, ...$ctx = null) 76 | * @method static bdo($attributes, ...$ctx = null) 77 | * @method static br($attributes, ...$ctx = null) 78 | * @method static cite($attributes, ...$ctx = null) 79 | * @method static code($attributes, ...$ctx = null) 80 | * @method static data($attributes, ...$ctx = null) 81 | * @method static dfn($attributes, ...$ctx = null) 82 | * @method static em($attributes, ...$ctx = null) 83 | * @method static i($attributes, ...$ctx = null) 84 | * @method static kbd($attributes, ...$ctx = null) 85 | * @method static mark($attributes, ...$ctx = null) 86 | * @method static q($attributes, ...$ctx = null) 87 | * @method static rb($attributes, ...$ctx = null) 88 | * @method static rp($attributes, ...$ctx = null) 89 | * @method static rt($attributes, ...$ctx = null) 90 | * @method static rtc($attributes, ...$ctx = null) 91 | * @method static ruby($attributes, ...$ctx = null) 92 | * @method static s($attributes, ...$ctx = null) 93 | * @method static samp($attributes, ...$ctx = null) 94 | * @method static small($attributes, ...$ctx = null) 95 | * @method static span($attributes, ...$ctx = null) 96 | * @method static strong($attributes, ...$ctx = null) 97 | * @method static sub($attributes, ...$ctx = null) 98 | * @method static sup($attributes, ...$ctx = null) 99 | * @method static time($attributes, ...$ctx = null) 100 | * @method static u($attributes, ...$ctx = null) 101 | * @method static var($attributes, ...$ctx = null) 102 | * @method static wbr($attributes, ...$ctx = null) 103 | * @method static area($attributes, ...$ctx = null) 104 | * @method static audio($attributes, ...$ctx = null) 105 | * @method static img($attributes, ...$ctx = null) 106 | * @method static map($attributes, ...$ctx = null) 107 | * @method static track($attributes, ...$ctx = null) 108 | * @method static video($attributes, ...$ctx = null) 109 | * @method static embed($attributes, ...$ctx = null) 110 | * @method static iframe($attributes, ...$ctx = null) 111 | * @method static object($attributes, ...$ctx = null) 112 | * @method static param($attributes, ...$ctx = null) 113 | * @method static picture($attributes, ...$ctx = null) 114 | * @method static source($attributes, ...$ctx = null) 115 | * @method static canvas($attributes, ...$ctx = null) 116 | * @method static noscript($attributes, ...$ctx = null) 117 | * @method static script($attributes, ...$ctx = null) 118 | * @method static del($attributes, ...$ctx = null) 119 | * @method static ins($attributes, ...$ctx = null) 120 | * @method static caption($attributes, ...$ctx = null) 121 | * @method static col($attributes, ...$ctx = null) 122 | * @method static colgroup($attributes, ...$ctx = null) 123 | * @method static table($attributes, ...$ctx = null) 124 | * @method static tbody($attributes, ...$ctx = null) 125 | * @method static td($attributes, ...$ctx = null) 126 | * @method static tfoot($attributes, ...$ctx = null) 127 | * @method static th($attributes, ...$ctx = null) 128 | * @method static thead($attributes, ...$ctx = null) 129 | * @method static tr($attributes, ...$ctx = null) 130 | * @method static button($attributes, ...$ctx = null) 131 | * @method static datalist($attributes, ...$ctx = null) 132 | * @method static fieldset($attributes, ...$ctx = null) 133 | * @method static form($attributes, ...$ctx = null) 134 | * @method static input($attributes, ...$ctx = null) 135 | * @method static label($attributes, ...$ctx = null) 136 | * @method static legend($attributes, ...$ctx = null) 137 | * @method static meter($attributes, ...$ctx = null) 138 | * @method static optgroup($attributes, ...$ctx = null) 139 | * @method static option($attributes, ...$ctx = null) 140 | * @method static output($attributes, ...$ctx = null) 141 | * @method static progress($attributes, ...$ctx = null) 142 | * @method static select($attributes, ...$ctx = null) 143 | * @method static textarea($attributes, ...$ctx = null) 144 | * @method static details($attributes, ...$ctx = null) 145 | * @method static dialog($attributes, ...$ctx = null) 146 | * @method static menu($attributes, ...$ctx = null) 147 | * @method static summary($attributes, ...$ctx = null) 148 | * @method static slot($attributes, ...$ctx = null) 149 | * @method static template($attributes, ...$ctx = null) 150 | */ 151 | class HTML 152 | { 153 | /** 154 | * HTML处理类入口 155 | * 156 | * @param string $name 157 | * @param mixed $arguments 158 | * @return CallTree 159 | */ 160 | static function __callStatic(string $name, $arguments): CallTree 161 | { 162 | $callTree = CallTree::getInstance(); 163 | $callTree->saveNode($name, $arguments); 164 | return $callTree; 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /src/Lib/StringToPHPStream.php: -------------------------------------------------------------------------------- 1 | 4 | * StringToPHPStream.php 5 | */ 6 | 7 | namespace Cross\Lib; 8 | 9 | /** 10 | * 字符串php代码通过wrapper转换为可以执行的php代码 11 | *
 12 |  * 使用方式 stream_register_wrapper("自定义名字", "stringToPHPStream")
 13 |  * $var = include ("自定义名字://字符串代码")
 14 |  * 
15 | * 16 | * @author wonli 17 | * Class StringToPHPStream 18 | * @package Cross\Lib\Other 19 | */ 20 | class StringToPHPStream 21 | { 22 | 23 | /** 24 | * 代码内容 25 | * 26 | * @var array 27 | */ 28 | static $content; 29 | 30 | /** 31 | * 在$content中的标示 32 | * 33 | * @var string 34 | */ 35 | protected $key; 36 | 37 | /** 38 | * @var int 39 | */ 40 | protected $pos; 41 | 42 | /** 43 | * @param $path 44 | * @param $mode 45 | * @param $options 46 | * @param $opened_path 47 | * @return bool 48 | */ 49 | public function stream_open($path, $mode, $options, $opened_path) 50 | { 51 | $this->key = md5($path); 52 | if (!isset(self::$content[$this->key])) { 53 | self::$content[$this->key] = sprintf('pos = 0; 57 | return true; 58 | } 59 | 60 | /** 61 | * @param int $count 62 | * @return string 63 | */ 64 | public function stream_read(int $count): string 65 | { 66 | $content = self::$content[$this->key]; 67 | $ret = substr($content, $this->pos, $count); 68 | $this->pos += strlen($ret); 69 | return $ret; 70 | } 71 | 72 | /** 73 | * @param int $option 74 | * @param int $arg1 75 | * @param int $arg2 76 | */ 77 | public function stream_set_option(int $option, int $arg1, int $arg2) 78 | { 79 | 80 | } 81 | 82 | /** 83 | * 84 | */ 85 | public function stream_stat() 86 | { 87 | 88 | } 89 | 90 | /** 91 | * 92 | */ 93 | public function stream_eof() 94 | { 95 | 96 | } 97 | } 98 | 99 | 100 | -------------------------------------------------------------------------------- /src/Lib/Upload/Filter/Image.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | class Image implements IFilter 20 | { 21 | 22 | /** 23 | * 最小宽度 24 | * 25 | * @var int 26 | */ 27 | protected $width; 28 | 29 | /** 30 | * 最小高度 31 | * 32 | * @var int 33 | */ 34 | protected $height; 35 | 36 | /** 37 | * 限定最大还是最小高宽 默认最小高宽 38 | * 39 | * @var string 40 | */ 41 | protected $minOrMax = 'min'; 42 | 43 | /** 44 | * 图片宽度使用绝对值 45 | * 46 | * @var bool 47 | */ 48 | protected $absoluteWidth = false; 49 | 50 | /** 51 | * 图片高度使用绝对值 52 | * 53 | * @var bool 54 | */ 55 | protected $absoluteHeight = false; 56 | 57 | /** 58 | * 允许上传的文件类型 59 | * 60 | * @var array 61 | */ 62 | protected $imageType = ['jpg', 'png', 'jpeg', 'gif']; 63 | 64 | /** 65 | * 上传文件过滤 66 | * 67 | * @param mixed $file 文件信息 68 | * @param string $error 69 | * @return bool 成功返回true 70 | */ 71 | function filter($file, &$error = '') 72 | { 73 | $tmpFile = &$file['tmp_name']; 74 | if (empty($tmpFile)) { 75 | $error = '没有找到临时文件'; 76 | return false; 77 | } 78 | 79 | $imageInfo = @getimagesize($tmpFile); 80 | if (false === $imageInfo) { 81 | $error = '获取图片信息失败'; 82 | return false; 83 | } 84 | 85 | $imageType = substr(strtolower(image_type_to_extension($imageInfo[2])), 1); 86 | if (!in_array($imageType, $this->imageType)) { 87 | $error = '不允许上传的文件类型: ' . $imageType; 88 | return false; 89 | } 90 | 91 | //不验证高宽 92 | if (!$this->width && !$this->height) { 93 | return true; 94 | } 95 | 96 | $width = &$imageInfo[0]; 97 | $height = &$imageInfo[1]; 98 | if ($this->absoluteWidth && $width != $this->width) { 99 | $error = '图片宽度限定: ' . $this->width . 'px'; 100 | return false; 101 | } 102 | 103 | if ($this->absoluteHeight && $height != $this->height) { 104 | $error = '图片高度限定: ' . $this->height . 'px'; 105 | return false; 106 | } 107 | 108 | if ($this->minOrMax === 'min') { 109 | if ($width < $this->width) { 110 | $error = '图片宽度小于: ' . $this->width . 'px'; 111 | return false; 112 | } elseif ($height < $this->height) { 113 | $error = '图片高度小于: ' . $this->width . 'px'; 114 | return false; 115 | } 116 | } else { 117 | if ($width > $this->width) { 118 | $error = '图片宽度不能大于: ' . $this->width . 'px'; 119 | return false; 120 | } elseif ($height > $this->height) { 121 | $error = '图片高度不能大于: ' . $this->height . 'px'; 122 | return false; 123 | } 124 | } 125 | 126 | return true; 127 | } 128 | 129 | /** 130 | * 设置允许上传的图片类型 131 | * 132 | * @param string $fileType 133 | * @return $this 134 | */ 135 | function fileType($fileType) 136 | { 137 | $this->imageType = explode('|', strtolower($fileType)); 138 | return $this; 139 | } 140 | 141 | /** 142 | * 宽度相等通过验证 143 | * 144 | * @return $this 145 | */ 146 | function useAbsoluteWidth() 147 | { 148 | $this->absoluteWidth = true; 149 | return $this; 150 | } 151 | 152 | /** 153 | * 高度相等通过验证 154 | * 155 | * @return $this 156 | */ 157 | function useAbsoluteHeight() 158 | { 159 | $this->absoluteHeight = true; 160 | return $this; 161 | } 162 | 163 | /** 164 | * 设置图片高宽 165 | * 166 | * @param int $width 167 | * @param int $height 168 | * @param string $minOrMax 限定最小还是最大宽度 169 | * @return Image 170 | */ 171 | function setWidthHeight($width, $height, $minOrMax = 'min') 172 | { 173 | $this->width = (int)$width; 174 | $this->height = (int)$height; 175 | $this->minOrMax = (0 === strcasecmp($minOrMax, 'min')) ? 'min' : 'max'; 176 | return $this; 177 | } 178 | } -------------------------------------------------------------------------------- /src/Lib/Upload/Filter/MimeType.php: -------------------------------------------------------------------------------- 1 | 19 | */ 20 | class MimeType implements IFilter 21 | { 22 | 23 | /** 24 | * 允许上传的文件类型 25 | * 26 | * @var array 27 | */ 28 | protected $allowedFileType = []; 29 | 30 | /** 31 | * 对应关系 32 | * 33 | * @var array 34 | */ 35 | protected $allowMimeContentType = [ 36 | 'txt' => 'text/plain', 37 | 'htm' => 'text/html', 38 | 'html' => 'text/html', 39 | 'php' => 'text/html', 40 | 'css' => 'text/css', 41 | 'js' => 'application/javascript', 42 | 'json' => 'application/json', 43 | 'xml' => 'application/xml', 44 | 'swf' => 'application/x-shockwave-flash', 45 | 'flv' => 'video/x-flv', 46 | 47 | // images 48 | 'png' => 'image/png', 49 | 'jpe' => 'image/jpeg', 50 | 'jpeg' => 'image/jpeg', 51 | 'jpg' => 'image/jpeg', 52 | 'gif' => 'image/gif', 53 | 'bmp' => 'image/bmp', 54 | 'ico' => 'image/vnd.microsoft.icon', 55 | 'tiff' => 'image/tiff', 56 | 'tif' => 'image/tiff', 57 | 'svg' => 'image/svg+xml', 58 | 'svgz' => 'image/svg+xml', 59 | 60 | // archives 61 | '7z' => 'application/x-7z-compressed', 62 | 'zip' => 'application/zip', 63 | 'rar' => 'application/x-rar-compressed', 64 | 'exe' => 'application/x-msdownload', 65 | 'msi' => 'application/x-msdownload', 66 | 'cab' => 'application/vnd.ms-cab-compressed', 67 | 68 | // audio/video 69 | 'mp3' => 'audio/mpeg', 70 | 'qt' => 'video/quicktime', 71 | 'mov' => 'video/quicktime', 72 | 73 | // adobe 74 | 'pdf' => 'application/pdf', 75 | 'psd' => 'image/vnd.adobe.photoshop', 76 | 'ai' => 'application/postscript', 77 | 'eps' => 'application/postscript', 78 | 'ps' => 'application/postscript', 79 | 80 | // ms office 81 | 'doc' => 'application/msword', 82 | 'rtf' => 'application/rtf', 83 | 'xls' => 'application/vnd.ms-excel', 84 | 'ppt' => 'application/vnd.ms-powerpoint', 85 | 86 | // open office 87 | 'odt' => 'application/vnd.oasis.opendocument.text', 88 | 'ods' => 'application/vnd.oasis.opendocument.spreadsheet', 89 | ]; 90 | 91 | /** 92 | * 上传文件过滤 93 | * 94 | * @param mixed $file 文件信息 95 | * @param string $error 失败信息 96 | * @return bool 成功返回true 97 | */ 98 | function filter($file, &$error = '') 99 | { 100 | $m = mime_content_type($file['tmp_name']); 101 | foreach ($this->allowedFileType as $suffix) { 102 | $contentType = &$this->allowMimeContentType[$suffix]; 103 | if (!$contentType) { 104 | $error = '不支持的MimeContentType'; 105 | return false; 106 | } 107 | 108 | if (0 === strcasecmp($m, $contentType)) { 109 | return true; 110 | } 111 | } 112 | 113 | $error = '不支持的类型'; 114 | return false; 115 | } 116 | 117 | /** 118 | * 设定允许上传的文件类型 119 | * 120 | * @param string|array $type 竖线分隔, gif|jpg|jpeg|png|doc 121 | * @return $this 122 | */ 123 | function setAllowedType($type) 124 | { 125 | $this->allowedFileType = explode('|', strtolower($type)); 126 | return $this; 127 | } 128 | 129 | /** 130 | * 增加允许上传的类型 131 | * 132 | * @param string $fileSuffix 133 | * @param string $mimeContentType 134 | * @return $this 135 | */ 136 | function addMimeContentType($fileSuffix, $mimeContentType) 137 | { 138 | $this->allowMimeContentType[$fileSuffix] = $mimeContentType; 139 | return $this; 140 | } 141 | } -------------------------------------------------------------------------------- /src/Lib/Upload/IFilter.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | interface IFilter 18 | { 19 | /** 20 | * 上传文件过滤 21 | * 22 | * @param mixed $file 文件信息 23 | * @param string $error 失败信息 24 | * @return bool 成功返回true 25 | */ 26 | function filter($file, &$error = ''); 27 | } -------------------------------------------------------------------------------- /src/MVC/Controller.php: -------------------------------------------------------------------------------- 1 | 20 | * Class Controller 21 | * @package Cross\MVC 22 | */ 23 | class Controller extends FrameBase 24 | { 25 | /** 26 | * 默认数据 27 | * 28 | * @var array 29 | */ 30 | protected $data = []; 31 | 32 | /** 33 | * 状态配置文件 34 | * 35 | * @var string 36 | */ 37 | protected $statusConfigFile = 'config::status.config.php'; 38 | 39 | /** 40 | * Controller constructor. 41 | */ 42 | function __construct() 43 | { 44 | parent::__construct(); 45 | $this->data = ResponseData::builder()->getData(); 46 | } 47 | 48 | /** 49 | * 判断是否POST请求 50 | * 51 | * @return bool 52 | */ 53 | protected function isPost(): bool 54 | { 55 | return $this->delegate->getRequest()->isPostRequest(); 56 | } 57 | 58 | /** 59 | * 判断是否GET请求 60 | * 61 | * @return bool 62 | */ 63 | protected function isGet(): bool 64 | { 65 | return $this->delegate->getRequest()->isGetRequest(); 66 | } 67 | 68 | /** 69 | * 是否在命令行下执行 70 | * 71 | * @return bool 72 | */ 73 | protected function isCli(): bool 74 | { 75 | return PHP_SAPI === 'cli'; 76 | } 77 | 78 | /** 79 | * 判断是否AJAX请求 80 | * 81 | * @return boolean 82 | */ 83 | protected function isAjax(): bool 84 | { 85 | return $this->delegate->getRequest()->isAjaxRequest(); 86 | } 87 | 88 | /** 89 | * 返回执行的前一页 90 | */ 91 | protected function returnReferer(): void 92 | { 93 | $this->redirect($this->delegate->getRequest()->getUrlReferrer()); 94 | } 95 | 96 | /** 97 | * 获取输入数据 98 | * 99 | * @param string $key 100 | * @param mixed $default 101 | * @return DataFilter 102 | */ 103 | function input(string $key, $default = null): DataFilter 104 | { 105 | $val = ''; 106 | $dataContainer = array_merge($this->params, 107 | $this->request->getRequestData(), $this->request->getPostData(), $this->request->getFileData()); 108 | if (is_array($dataContainer)) { 109 | $val = $dataContainer[$key] ?? null; 110 | } 111 | 112 | if (empty($val) && null !== $default) { 113 | $val = $default; 114 | } 115 | 116 | return new DataFilter($val); 117 | } 118 | 119 | /** 120 | * 重定向到指定的控制器 121 | * 122 | * @param string|null $controller controller:action 123 | * @param string|array|null $params 124 | * @param bool $sec 125 | * @throws CoreException 126 | */ 127 | protected function to(string $controller = null, $params = null, bool $sec = false): void 128 | { 129 | $url = $this->view->url($controller, $params, $sec); 130 | $this->redirect($url); 131 | } 132 | 133 | /** 134 | * 重定向 135 | * 136 | * @param string $url 137 | * @param int $httpResponseStatus 138 | * @see Response::redirect 139 | * 140 | */ 141 | protected function redirect(string $url, int $httpResponseStatus = 302): void 142 | { 143 | $this->delegate->getResponse()->redirect($url, $httpResponseStatus); 144 | } 145 | 146 | /** 147 | * 发送一个错误状态 148 | * 149 | * @param int $status 150 | * @param mixed $message 151 | * @throws CoreException|LogicStatusException 152 | */ 153 | protected function end(int $status, string $message = null) 154 | { 155 | if ($status == 1) { 156 | throw new CoreException('Incorrect status value!'); 157 | } 158 | 159 | throw new LogicStatusException($status, $message); 160 | } 161 | 162 | /** 163 | * 发送JSON数据 164 | * 165 | * @param int $status 166 | * @param array $data 167 | */ 168 | protected function json(int $status, array $data = []) 169 | { 170 | $rdb = ResponseData::builder(); 171 | $rdb->setStatus($status); 172 | $rdb->setData($data); 173 | 174 | $this->delegate->getResponse() 175 | ->setContentType('json')->setRawContent($rdb)->end(); 176 | } 177 | 178 | /** 179 | * 视图渲染 180 | * 181 | * @param mixed $data 182 | * @param string|null $method 183 | * @param int $httpResponseStatus 184 | * @throws CoreException 185 | * @see View::display() 186 | */ 187 | protected function display($data = null, string $method = null, int $httpResponseStatus = 200): void 188 | { 189 | $this->delegate->getResponse()->setResponseStatus($httpResponseStatus); 190 | $this->view->display($data, $method); 191 | } 192 | 193 | /** 194 | * 交互数据对齐 195 | * 196 | * @param mixed $data 197 | * @return ResponseData 198 | * @throws CoreException 199 | */ 200 | protected function getResponseData($data): ResponseData 201 | { 202 | if ($data instanceof ResponseData) { 203 | $responseData = $data; 204 | } else { 205 | $responseData = ResponseData::builder(); 206 | if (is_numeric($data)) { 207 | $responseData->setStatus($data); 208 | } elseif (is_array($data)) { 209 | $responseData->updateInfoProperty($data); 210 | if (!empty($data)) { 211 | $responseData->setData($data); 212 | } 213 | } elseif (is_object($data)) { 214 | if (false === ($jsonData = json_encode($data))) { 215 | throw new CoreException('Unsupported data types!'); 216 | } 217 | 218 | $data = json_decode($jsonData, true); 219 | if (!is_array($data)) { 220 | throw new CoreException('Unsupported data types!'); 221 | } 222 | 223 | $responseData->updateInfoProperty($data); 224 | if (!empty($data)) { 225 | $responseData->setData($data); 226 | } 227 | } elseif (null !== $data && is_scalar($data)) { 228 | $responseData->setMessage((string)$data); 229 | } 230 | } 231 | 232 | $status = $responseData->getStatus(); 233 | if ($status != 1 && empty($responseData->getMessage())) { 234 | $responseData->setMessage($this->getStatusMessage($status)); 235 | } 236 | 237 | return $responseData; 238 | } 239 | 240 | /** 241 | * 获取消息状态内容 242 | * 243 | * @param int $status 244 | * @return string 245 | * @throws CoreException 246 | */ 247 | protected function getStatusMessage(int $status): string 248 | { 249 | static $statusConfig = null; 250 | if ($statusConfig === null) { 251 | $statusConfig = $this->parseGetFile($this->statusConfigFile); 252 | } 253 | 254 | if (!isset($statusConfig[$status])) { 255 | throw new CoreException("未知错误({$status})"); 256 | } 257 | 258 | return $statusConfig[$status]; 259 | } 260 | 261 | /** 262 | * 重设视图action名称 263 | * 264 | * @param string $actionName 265 | * @return self 266 | */ 267 | function setAction(string $actionName): self 268 | { 269 | $this->view->action = $actionName; 270 | return $this; 271 | } 272 | } 273 | -------------------------------------------------------------------------------- /src/MVC/Module.php: -------------------------------------------------------------------------------- 1 | 29 | * Class Module 30 | * @package Cross\MVC 31 | * @property RedisDriver|CouchDriver|MongoDriver|PDOSqlDriver $link 32 | */ 33 | class Module extends FrameBase 34 | { 35 | /** 36 | * 数据库连接的model名称 37 | * 38 | * @var string 39 | */ 40 | private $linkName; 41 | 42 | /** 43 | * 数据库连接model类型 44 | * 45 | * @var string 46 | */ 47 | private $linkType; 48 | 49 | /** 50 | * 数据库连接的model配置 51 | * 52 | * @var array 53 | */ 54 | private $linkConfig; 55 | 56 | /** 57 | * 连接配置文件名 58 | *
 59 |      * 默认为项目目录下的config/db.config.php
 60 |      * 可以在app目录下init.php文件中通过'sys' => 'db_config'指定
 61 |      * 
62 | * 63 | * @var string 64 | */ 65 | protected $dbConfigFile; 66 | 67 | /** 68 | * 解析要连接model的参数 69 | * 70 | * @param string $params 指定要连接的数据库和配置项的key, 如mysql['db']这里的params应该为mysql:db 71 | * @throws CoreException 72 | */ 73 | function __construct(string $params = '') 74 | { 75 | parent::__construct(); 76 | 77 | $config = $this->parseModelParams($params); 78 | $this->linkName = &$config['model_name']; 79 | $this->linkType = &$config['model_type']; 80 | $this->linkConfig = &$config['model_config']; 81 | } 82 | 83 | /** 84 | * 创建model实例,参数格式和构造函数一致 85 | * 86 | * @param string $params 87 | * @param array $config 88 | * @return RedisDriver|CouchDriver|MongoDriver|PDOSqlDriver|mixed 89 | * @throws CoreException 90 | * @throws DBConnectException 91 | */ 92 | function getModel(string $params = '', &$config = []): object 93 | { 94 | static $cache = []; 95 | if (!isset($cache[$params])) { 96 | $config = $this->parseModelParams($params); 97 | $model = DBFactory::make($config['model_type'], $config['model_config'], array($this->getConfig())); 98 | $cache[$params] = array('config' => $config, 'model' => $model); 99 | } else { 100 | $model = $cache[$params]['model']; 101 | $config = $cache[$params]['config']; 102 | } 103 | 104 | return $model; 105 | } 106 | 107 | /** 108 | * 当前link的model名称 109 | * 110 | * @return string 111 | */ 112 | function getLinkName(): string 113 | { 114 | return $this->linkName; 115 | } 116 | 117 | /** 118 | * 当前link的model类型 119 | * 120 | * @return string 121 | */ 122 | function getLinkType(): string 123 | { 124 | return $this->linkType; 125 | } 126 | 127 | /** 128 | * 当前link的model详细配置信息 129 | * 130 | * @return array 131 | */ 132 | function getLinkConfig(): array 133 | { 134 | return $this->linkConfig; 135 | } 136 | 137 | /** 138 | * 获取带配置前缀的表名 139 | * 140 | * @param string $table 141 | * @return string 142 | */ 143 | function getPrefix(string $table = ''): string 144 | { 145 | return $this->link->getPrefix() . $table; 146 | } 147 | 148 | /** 149 | * 读取并解析数据库配置 150 | * 151 | * @return Config 152 | * @throws CoreException 153 | */ 154 | protected function databaseConfig(): Config 155 | { 156 | static $databaseConfig = null; 157 | if (null === $databaseConfig) { 158 | $databaseConfig = parent::loadConfig($this->getModuleConfigFile()); 159 | } 160 | 161 | return $databaseConfig; 162 | } 163 | 164 | /** 165 | * 设置配置文件名 166 | * 167 | * @param string $linkConfigFile 168 | */ 169 | protected function setDatabaseConfigFile(string $linkConfigFile): void 170 | { 171 | $this->dbConfigFile = $linkConfigFile; 172 | } 173 | 174 | /** 175 | * 解析指定model的类型和参数 176 | * 177 | * @param string $params 178 | * @return array 179 | * @throws CoreException 180 | */ 181 | protected function parseModelParams(string $params = ''): array 182 | { 183 | $dbConfigParams = ''; 184 | if ($params) { 185 | $dbConfigParams = $params; 186 | } else { 187 | static $defaultDbConfig = ''; 188 | if ($defaultDbConfig === '') { 189 | $defaultDbConfig = $this->getConfig()->get('sys', 'default_db'); 190 | } 191 | 192 | if ($defaultDbConfig) { 193 | $dbConfigParams = $defaultDbConfig; 194 | } 195 | } 196 | 197 | if ($dbConfigParams) { 198 | if (strpos($dbConfigParams, ':') === false) { 199 | throw new CoreException("数据库参数配置格式不正确: {$dbConfigParams}"); 200 | } 201 | 202 | list($modelType, $modelName) = explode(':', $dbConfigParams); 203 | } else { 204 | $modelName = 'db'; 205 | $modelType = 'mysql'; 206 | } 207 | 208 | static $cache; 209 | if (!isset($cache[$modelType][$modelName])) { 210 | $databaseConfig = $this->databaseConfig(); 211 | $modelConfig = $databaseConfig->get($modelType, $modelName); 212 | if (empty($modelConfig)) { 213 | throw new CoreException("未配置的Model: {$modelType}:{$modelName}"); 214 | } 215 | 216 | $cache[$modelType][$modelName] = [ 217 | 'model_name' => $modelName, 218 | 'model_type' => $modelType, 219 | 'model_config' => $modelConfig, 220 | ]; 221 | } 222 | 223 | return $cache[$modelType][$modelName]; 224 | } 225 | 226 | /** 227 | * 获取默认model的实例 228 | * 229 | * @return RedisDriver|CouchDriver|MongoDriver|PDOSqlDriver|mixed 230 | * @throws CoreException 231 | * @throws DBConnectException 232 | */ 233 | private function getLink(): object 234 | { 235 | return DBFactory::make($this->linkType, $this->linkConfig, [$this->getConfig()]); 236 | } 237 | 238 | /** 239 | * 获取连接配置文件名 240 | * 241 | * @return mixed 242 | */ 243 | private function getModuleConfigFile(): string 244 | { 245 | if (!$this->dbConfigFile) { 246 | $dbConfigFile = $this->getConfig()->get('sys', 'db_config'); 247 | if (!$dbConfigFile) { 248 | $dbConfigFile = 'db.config.php'; 249 | } 250 | 251 | $this->setDatabaseConfigFile($dbConfigFile); 252 | } 253 | 254 | return $this->dbConfigFile; 255 | } 256 | 257 | /** 258 | * 访问link属性时才实例化model 259 | * 260 | * @param mixed $property 261 | * @return RedisDriver|Config|CouchDriver|MongoDriver|PDOSqlDriver|Request|Response|View|mixed|null 262 | * @throws CoreException 263 | * @throws DBConnectException 264 | */ 265 | function __get($property) 266 | { 267 | switch ($property) { 268 | case 'link' : 269 | return $this->link = $this->getLink(); 270 | 271 | default : 272 | return parent::__get($property); 273 | } 274 | } 275 | } 276 | -------------------------------------------------------------------------------- /src/Runtime/ClosureContainer.php: -------------------------------------------------------------------------------- 1 | 16 | * Class ClosureContainer 17 | * @package Cross\Core 18 | */ 19 | class ClosureContainer 20 | { 21 | /** 22 | * @var array 23 | */ 24 | protected $actions = []; 25 | 26 | /** 27 | * @var static 28 | */ 29 | private static $instance; 30 | 31 | /** 32 | * ClosureContainer constructor. 33 | */ 34 | private function __construct() 35 | { 36 | 37 | } 38 | 39 | /** 40 | * @return static 41 | */ 42 | static function getInstance(): self 43 | { 44 | if (null === self::$instance) { 45 | self::$instance = new self(); 46 | } 47 | 48 | return self::$instance; 49 | } 50 | 51 | /** 52 | * 注册一个匿名方法 53 | * 54 | * @param string $name 55 | * @param Closure $f 56 | */ 57 | function add(string $name, Closure $f): void 58 | { 59 | $this->actions[$name] = $f; 60 | } 61 | 62 | /** 63 | * 执行指定的匿名方法 64 | * 65 | * @param string $name 66 | * @param array $params 67 | * @return mixed 68 | */ 69 | function run(string $name, array $params = []) 70 | { 71 | if (isset($this->actions[$name])) { 72 | if (!is_array($params)) { 73 | $params = [$params]; 74 | } 75 | return call_user_func_array($this->actions[$name], $params); 76 | } 77 | 78 | return false; 79 | } 80 | 81 | /** 82 | * 执行指定的匿名方法并缓存执行结果 83 | * 84 | * @param string $name 85 | * @param array $params 86 | * @return mixed 87 | */ 88 | function runOnce(string $name, array $params = []) 89 | { 90 | static $cache = []; 91 | if (isset($cache[$name])) { 92 | return $cache[$name]; 93 | } elseif (isset($this->actions[$name])) { 94 | if (!is_array($params)) { 95 | $params = [$params]; 96 | } 97 | 98 | $cache[$name] = call_user_func_array($this->actions[$name], $params); 99 | return $cache[$name]; 100 | } 101 | 102 | return false; 103 | } 104 | 105 | /** 106 | * 检查指定的匿名方法是否已经注册 107 | * 108 | * @param string $name 109 | * @param Closure|null $closure 110 | * @return bool 111 | */ 112 | function has(string $name, &$closure = null): bool 113 | { 114 | if (isset($this->actions[$name])) { 115 | $closure = $this->actions[$name]; 116 | return true; 117 | } 118 | 119 | return false; 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/Runtime/RequestMapping.php: -------------------------------------------------------------------------------- 1 | matchString = $request; 70 | $routers = $this->mapping[Request::getInstance()->getRequestType()] ?? []; 71 | if (empty($routers)) { 72 | return $match; 73 | } 74 | 75 | if (!empty($routers['high']) && isset($routers['high'][$request])) { 76 | $handle = $routers['high'][$request]; 77 | if (is_array($handle)) { 78 | $handle = $handle['handler']; 79 | } 80 | 81 | $match = true; 82 | } 83 | 84 | if (!$match && !empty($routers['current'])) { 85 | $match = $this->matchProcess($routers['current'], $handle, $params); 86 | } 87 | 88 | if (!$match && !empty($routers['global'])) { 89 | $match = $this->matchProcess($routers['global'], $handle, $params); 90 | } 91 | 92 | return $match; 93 | } 94 | 95 | /** 96 | * 设置验证规则 97 | * 98 | * @param array $rules 99 | */ 100 | function setRules(array $rules): void 101 | { 102 | $this->rules = $rules; 103 | } 104 | 105 | /** 106 | * 添加验证规则 107 | * 108 | * @param string $params 109 | * @param string $pattern 110 | * @return static 111 | */ 112 | function addRule($params, $pattern): self 113 | { 114 | $this->rules[$params] = $pattern; 115 | return $this; 116 | } 117 | 118 | /** 119 | * 添加路由(按当前请求类型分组,等同于Any) 120 | * 121 | * @param mixed $router 122 | * @param mixed $handler 123 | * @return bool 124 | * @throws CoreException 125 | */ 126 | static function addRouter($router, $handler = null): bool 127 | { 128 | return self::getInstance()->addToMapping(Request::getInstance()->getRequestType(), $router, $handler); 129 | } 130 | 131 | /** 132 | * 添加HTTP路由 133 | * 134 | * @param string $requestType 135 | * @param string $router 136 | * @param mixed $handler 137 | * @return bool 138 | * @throws CoreException 139 | */ 140 | function addRequestRouter(string $requestType, string $router, $handler = null): bool 141 | { 142 | return $this->addToMapping($requestType, $router, $handler); 143 | } 144 | 145 | /** 146 | * 循环匹配(参数多的优先) 147 | * 148 | * @param array $routers 149 | * @param mixed $handle 150 | * @param array $params 151 | * @return bool 152 | */ 153 | private function matchProcess(array $routers, &$handle, array &$params): bool 154 | { 155 | foreach ($routers as $r => &$rr) { 156 | if (isset($r[1]) && isset($this->matchString[1]) && 0 === strcasecmp($r[1], $this->matchString[1])) { 157 | $rr['score'] += 10000; 158 | } 159 | } 160 | 161 | uasort($routers, function ($a, $b) { 162 | return $a['score'] < $b['score']; 163 | }); 164 | 165 | foreach ($routers as $router => $setting) { 166 | $params = []; 167 | if (true === $this->matchCustomRouter($router, $setting['params'], $params)) { 168 | $handle = $setting['handler']; 169 | return true; 170 | } 171 | } 172 | 173 | return false; 174 | } 175 | 176 | /** 177 | * 匹配uri和自定义路由 178 | * 179 | * @param string $customRouter 180 | * @param array $paramsKeys 181 | * @param array $params 182 | * @return bool 183 | */ 184 | private function matchCustomRouter(string $customRouter, array $paramsKeys = [], array &$params = []): bool 185 | { 186 | $matchString = $this->matchString; 187 | $customRouterParamsToken = preg_replace("#\{:(.*?)\}#", '{PARAMS}', $customRouter); 188 | while (strlen($customRouterParamsToken) > 0) { 189 | $definedParamsPos = strpos($customRouterParamsToken, '{PARAMS}'); 190 | if ($definedParamsPos) { 191 | $compareRet = substr_compare($customRouterParamsToken, $matchString, 0, $definedParamsPos); 192 | } else { 193 | $compareRet = strcasecmp($customRouterParamsToken, $matchString); 194 | } 195 | 196 | if ($compareRet !== 0) { 197 | return false; 198 | } 199 | 200 | //分段解析 201 | $customRouterParamsToken = substr($customRouterParamsToken, $definedParamsPos + 8); 202 | $matchString = substr($matchString, $definedParamsPos); 203 | 204 | if ($customRouterParamsToken) { 205 | //下一个标识符的位置 206 | $nextDefinedDotPos = strpos($matchString, $customRouterParamsToken[0]); 207 | $paramValue = substr($matchString, 0, $nextDefinedDotPos); 208 | $matchString = substr($matchString, $nextDefinedDotPos); 209 | } else { 210 | $paramValue = $matchString; 211 | } 212 | 213 | $keyName = array_shift($paramsKeys); 214 | if ($keyName && isset($this->rules[$keyName]) && !preg_match($this->rules[$keyName], $paramValue)) { 215 | return false; 216 | } 217 | 218 | if ($keyName) { 219 | $params[$keyName] = $paramValue; 220 | } 221 | } 222 | 223 | return true; 224 | } 225 | 226 | /** 227 | * 构造mapping 228 | * 229 | * @param string $groupKey 分组名称 230 | * @param string $customRouter 231 | * @param null $handler 232 | * @return bool 233 | * @throws CoreException 234 | */ 235 | private function addToMapping(string $groupKey, string $customRouter, $handler = null): bool 236 | { 237 | $customRouter = trim($customRouter); 238 | $isLowLevelRouter = preg_match_all("#(.*?)(?:(?:\[(.))|)\{:(.*?)\}(?:\]|)|(?:.+)#", $customRouter, $matches); 239 | if ($isLowLevelRouter) { 240 | $level = 'current'; 241 | $prefixStringLength = strlen($matches[1][0]); 242 | if ($prefixStringLength == 0) { 243 | $level = 'high'; 244 | } elseif ($prefixStringLength == 1) { 245 | $level = 'global'; 246 | } 247 | 248 | $hasOptional = false; 249 | $optional = $matches[2]; 250 | foreach ($optional as $n => $op) { 251 | if (!empty($op)) { 252 | $hasOptional = true; 253 | } 254 | 255 | if ($hasOptional && empty($op)) { 256 | throw new CoreException('Request mapping syntax error!'); 257 | } 258 | } 259 | 260 | if ($hasOptional) { 261 | $ori = $matches[1][0]; 262 | $oriLevel = 'high'; 263 | $oriRouterHandel = $handler; 264 | 265 | //处理可选参数 266 | $j = 0; 267 | $paramsKey = []; 268 | $optionalRouters = $ori; 269 | foreach ($matches[3] as $n => $optionalParamsName) { 270 | if ($n > 0 && !empty($matches[1][$n])) { 271 | $j++; 272 | $optionalRouters .= $matches[1][$n]; 273 | $this->mapping[$groupKey][$level][$optionalRouters] = [ 274 | 'handler' => $handler, 275 | 'score' => count($paramsKey) * 100 + $j, 276 | 'params' => $paramsKey, 277 | ]; 278 | } 279 | 280 | $j++; 281 | $paramsKey[] = $matches[3][$n]; 282 | $optionalRouters .= sprintf('%s{:%s}', $optional[$n], $optionalParamsName); 283 | $this->mapping[$groupKey][$level][$optionalRouters] = [ 284 | 'handler' => $handler, 285 | 'score' => count($paramsKey) * 100 + $j, 286 | 'params' => $paramsKey, 287 | ]; 288 | } 289 | } else { 290 | $ori = $customRouter; 291 | $oriLevel = $level; 292 | $oriRouterHandel = [ 293 | 'handler' => $handler, 294 | 'score' => count($matches[3]) * 100, 295 | 'params' => array_filter($matches[3]), 296 | ]; 297 | } 298 | 299 | $this->mapping[$groupKey][$oriLevel][$ori] = $oriRouterHandel; 300 | } else { 301 | $this->mapping[$groupKey]['high'][$customRouter] = $handler; 302 | } 303 | 304 | return true; 305 | } 306 | 307 | } -------------------------------------------------------------------------------- /src/Runtime/Rules.php: -------------------------------------------------------------------------------- 1 | rules = []; 31 | } 32 | 33 | /** 34 | * 匹配 35 | * 36 | * @param string $name 37 | * @param mixed $content 38 | * @return mixed 39 | * @throws CoreException 40 | */ 41 | static function match(string $name, $content) 42 | { 43 | $handler = self::instance()->has($name); 44 | if (null === $handler) { 45 | throw new CoreException('未定义的Rule'); 46 | } 47 | 48 | if ($handler instanceof Closure) { 49 | return $handler($content); 50 | } 51 | 52 | $isMatch = preg_match(preg_quote($handler), $content); 53 | if (!$isMatch) { 54 | throw new CoreException('验证Rule失败'); 55 | } 56 | 57 | return $content; 58 | } 59 | 60 | /** 61 | * rule是否存在 62 | * 63 | * @param string $name 64 | * @return mixed 65 | */ 66 | static function has(string $name) 67 | { 68 | return self::instance()->rules[$name] ?? null; 69 | } 70 | 71 | /** 72 | * 获取所有规则 73 | * 74 | * @return array 75 | */ 76 | static function getRules(): array 77 | { 78 | return self::instance()->rules; 79 | } 80 | 81 | /** 82 | * 添加规则 83 | * 84 | * @param string $name 85 | * @param mixed $rule 86 | * @return string 87 | */ 88 | static function addRule(string $name, $rule) 89 | { 90 | self::instance()->rules[$name] = $rule; 91 | return $rule; 92 | } 93 | 94 | /** 95 | * @return Rules 96 | */ 97 | protected static function instance() 98 | { 99 | if (null === self::$instance) { 100 | self::$instance = new self(); 101 | } 102 | 103 | return self::$instance; 104 | } 105 | } -------------------------------------------------------------------------------- /tests/MainTest.php: -------------------------------------------------------------------------------- 1 | 17 | * Class MainTest 18 | */ 19 | class MainTest extends TestCase 20 | { 21 | /** 22 | * 是否能正确加载app 23 | * 24 | * @throws CoreException 25 | * @throws FrontException 26 | */ 27 | function testLoadApp() 28 | { 29 | $app = Delegate::loadApp('test'); 30 | $this->assertInstanceOf('Cross\\Core\\Delegate', $app, 'loadApp error'); 31 | } 32 | 33 | /** 34 | * 读取app配置文件 35 | * 36 | * @throws CoreException 37 | * @throws FrontException 38 | */ 39 | function testReadAppConfig() 40 | { 41 | $result = $this->getAppResponse('Main:getAppConfig'); 42 | $ori_file = Loader::read(PROJECT_REAL_PATH . 'app/test/init.php'); 43 | $this->assertJsonStringEqualsJsonString($result, json_encode($ori_file['router'], true), 'read app/init.php error...'); 44 | } 45 | 46 | /** 47 | * 设置appConfig 48 | * 49 | * @throws CoreException 50 | * @throws FrontException 51 | */ 52 | function testSetAppConfig() 53 | { 54 | $params = array('a' => array(1, 2, 3, 'name' => array('a', 'b', 'c'))); 55 | $result = $this->getAppResponse('Main:setAppConfig', $params); 56 | 57 | $this->assertEquals($result, json_encode($params), 'set app config error...'); 58 | } 59 | 60 | /** 61 | * 测试注释配置 62 | * 使用get调用时, 注释配置依然生效 63 | * 64 | * @throws CoreException 65 | * @throws FrontException 66 | */ 67 | function testAnnotate() 68 | { 69 | $params = array('a' => 'ima', 'x' => 'imx'); 70 | $result = $this->getAppResponse('Main:annotate', $params); 71 | 72 | $this->assertEquals($result, array('a' => 'ima', 'b' => 2, 'c' => 3, 'x' => 'imx'), 'parse annotate error...'); 73 | } 74 | 75 | /** 76 | * 测试生成连接 77 | * 78 | * @throws CoreException 79 | * @throws FrontException 80 | */ 81 | function testMakeLink() 82 | { 83 | $dot = '/'; 84 | $ext = ''; 85 | $params = array('p1' => 1, 'p2' => 2, 'p3' => 3); 86 | $params['dot'] = $dot; 87 | $params['ext'] = $ext; 88 | $params['index'] = 'index.php'; 89 | 90 | for ($link_type = 1; $link_type <= 5; $link_type++) { 91 | $params['type'] = $link_type; 92 | $result = $this->getAppResponse('Main:makeLink', $params); 93 | 94 | switch ($link_type) { 95 | case 1: 96 | $this->assertEquals("/index.php/Main{$dot}getUrlSecurityParams{$dot}1{$dot}2{$dot}3{$ext}", $result, 'url->type=>1 make link error'); 97 | break; 98 | case 2: 99 | $this->assertEquals("/index.php/Main{$dot}getUrlSecurityParams{$dot}p1{$dot}1{$dot}p2{$dot}2{$dot}p3{$dot}3{$ext}", $result, 'url->type=>4 make link error'); 100 | break; 101 | default: 102 | $this->assertEquals("/index.php/Main{$dot}getUrlSecurityParams{$ext}?p1=1&p2=2&p3=3", $result, 'url->type=>2 make link error'); 103 | break; 104 | } 105 | } 106 | } 107 | 108 | /** 109 | * 生成加密连接测试 110 | * 111 | * @throws CoreException 112 | * @throws FrontException 113 | */ 114 | function testMakeEncryptLink() 115 | { 116 | $dot = '/'; 117 | $ext = ''; 118 | $params = array('p1' => 1, 'p2' => 2, 'p3' => 3); 119 | $params['dot'] = $dot; 120 | $params['ext'] = $ext; 121 | $params['index'] = 'index.php'; 122 | 123 | for ($link_type = 1; $link_type <= 5; $link_type++) { 124 | $params['type'] = $link_type; 125 | $result = $this->getAppResponse('Main:makeEncryptLink', $params); 126 | 127 | switch ($link_type) { 128 | case 1: 129 | $this->assertEquals("/index.php/Main{$dot}getUrlSecurityParams{$dot}5c38a0417051803{$ext}", $result, 'url->type=>1 make link error'); 130 | break; 131 | case 2: 132 | $this->assertEquals("/index.php/Main{$dot}getUrlSecurityParams{$dot}692ad450918061f435418041815561a03{$ext}", $result, 'url->type=>4 make link error'); 133 | break; 134 | default: 135 | $this->assertEquals("/index.php/Main{$dot}getUrlSecurityParams{$ext}?cd4b145090a061643540a041115560803", $result, 'url->type=>2 make link error'); 136 | break; 137 | } 138 | } 139 | } 140 | 141 | /** 142 | * url加密 参数解密测试 143 | * 144 | * @throws CoreException 145 | * @throws FrontException 146 | */ 147 | function testMakeEncryptLinkAndDecryptParams() 148 | { 149 | $dot = '/'; 150 | $ext = ''; 151 | $params = array('p1' => '1', 'p2' => '2', 'p3' => '3'); 152 | 153 | for ($link_type = 1; $link_type <= 3; $link_type++) { 154 | $result = $this->getAppResponse('Main:makeEncryptLinkAndDecryptParams', $params + array( 155 | 'dot' => $dot, 'ext' => $ext, 'link_type' => $link_type 156 | )); 157 | 158 | $this->assertEquals(json_encode($params), json_encode($result), "url type {$link_type} encrypt link failure!"); 159 | } 160 | } 161 | 162 | /** 163 | * SQL条件语句生成 164 | * 165 | * @throws CoreException 166 | */ 167 | function testSqlCondition() 168 | { 169 | $SQL = new SQLAssembler(); 170 | 171 | $p1 = array(); 172 | $r1 = $SQL->parseWhere(array('a' => 1, 'b' => 2), $p1); 173 | $this->assertEquals($r1, 'a = ? AND b = ?', 'condition 1 failure'); 174 | $this->assertEquals($p1, array(1, 2), 'condition 1 params failure'); 175 | 176 | $p2 = array(); 177 | $r2 = $SQL->parseWhere(array('a' => 1, 'b' => array('>=', 2)), $p2); 178 | $this->assertEquals($r2, 'a = ? AND b >= ?', 'condition 2 failure'); 179 | $this->assertEquals($p2, array(1, 2), 'condition 2 params failure'); 180 | 181 | $p3 = array(); 182 | $r3 = $SQL->parseWhere(array('a' => 1, '(b > ? OR b < ?)' => array(1, 2)), $p3); 183 | $this->assertEquals($r3, 'a = ? AND (b > ? OR b < ?)', 'condition 3 failure'); 184 | $this->assertEquals($p3, array(1, 1, 2), 'condition 3 params failure'); 185 | 186 | $p4 = array(); 187 | $r4 = $SQL->parseWhere(array( 188 | 'a' => array('AND', array( 189 | array('>=', 1), 190 | array('<=', 10), 191 | )) 192 | ), $p4); 193 | $this->assertEquals($r4, '(a >= ? AND a <= ?)', 'condition 4 failure'); 194 | $this->assertEquals($p4, array(1, 10), 'condition 4 params failure'); 195 | 196 | $p5 = array(); 197 | $r5 = $SQL->parseWhere(array( 198 | 'a' => array('between', array(1, 10)) 199 | ), $p5); 200 | $this->assertEquals($r5, 'a BETWEEN ? AND ?', 'condition 5 failure'); 201 | $this->assertEquals($p5, array(1, 10), 'condition 5 failure'); 202 | 203 | $p6 = array(); 204 | $r6 = $SQL->parseWhere(array( 205 | 'a' => array('or', array(1, 10)) 206 | ), $p6); 207 | $this->assertEquals($r6, '(a = ? OR a = ?)', 'condition 6 failure'); 208 | $this->assertEquals($p6, array(1, 10), 'condition 6 failure'); 209 | 210 | $p7 = array(); 211 | $r7 = $SQL->parseWhere(array( 212 | 'a' => array('or', array(1, 10)), 213 | 'b' => array('and', array( 214 | array('>=', 1), 215 | array('<=', 2) 216 | )), 217 | 'c' => array('between', array(1, 2)) 218 | ), $p7); 219 | $this->assertEquals($r7, '(a = ? OR a = ?) AND (b >= ? AND b <= ?) AND c BETWEEN ? AND ?', 'condition 7 failure'); 220 | $this->assertEquals($p7, array(1, 10, 1, 2, 1, 2), 'condition 7 failure'); 221 | 222 | $p8 = array(); 223 | $r8 = $SQL->parseWhere(array( 224 | '(a = ? OR a = ?) AND (b >= ? AND b <= ?) AND c BETWEEN ? AND ?', array(1, 10, 1, 2, 1, 2) 225 | ), $p8); 226 | $this->assertEquals($r8, '(a = ? OR a = ?) AND (b >= ? AND b <= ?) AND c BETWEEN ? AND ?', 'condition 8 failure'); 227 | $this->assertEquals($p8, array(1, 10, 1, 2, 1, 2), 'condition 8 failure'); 228 | } 229 | 230 | /** 231 | * 调用app指定controller 232 | * 233 | * @param $controller 234 | * @param array $params 235 | * @return array|mixed|string 236 | * @throws CoreException 237 | * @throws FrontException 238 | */ 239 | protected function getAppResponse($controller, $params = array()) 240 | { 241 | return Delegate::loadApp('test')->get($controller, $params, true); 242 | } 243 | } 244 | -------------------------------------------------------------------------------- /tests/README.md: -------------------------------------------------------------------------------- 1 | 进入命令行模式在框架根目录下使用`composer`安装phpunit 2 | 3 | composer install 4 | 5 | 待安装完成后, 进入`vendor/bin`目录, 执行以下命令 6 | 7 | phpunit ../../tests/MainTest.php 8 | -------------------------------------------------------------------------------- /tests/project/app/test/controllers/Main.php: -------------------------------------------------------------------------------- 1 | 4 | */ 5 | 6 | namespace app\test\controllers; 7 | 8 | use Cross\Exception\CoreException; 9 | use Cross\MVC\Controller; 10 | 11 | class Main extends Controller 12 | { 13 | /** 14 | * 取app配置文件 15 | */ 16 | function getAppConfig() 17 | { 18 | $config = $this->config->get('router'); 19 | return json_encode($config); 20 | } 21 | 22 | /** 23 | * 设置app配置文件项 24 | */ 25 | function setAppConfig() 26 | { 27 | $a = $this->params['a']; 28 | $this->config->set('a', $a); 29 | 30 | return json_encode(array('a' => $this->config->get('a'))); 31 | } 32 | 33 | /** 34 | * 测试注释配置 35 | * 36 | * @cp_params a=1, b=2, c=3 37 | */ 38 | function annotate() 39 | { 40 | return $this->params; 41 | } 42 | 43 | /** 44 | * 生成连接 45 | * 46 | * @return string 47 | * @throws CoreException 48 | */ 49 | function makeLink() 50 | { 51 | $params = $this->params; 52 | $type = $this->params['type']; 53 | $dot = $this->params['dot']; 54 | $ext = $this->params['ext']; 55 | $index = $this->params['index']; 56 | 57 | $this->view->cleanLinkCache(); 58 | $this->config->set('url', array( 59 | 'ext' => $ext, 60 | 'dot' => $dot, 61 | 'type' => $type, 62 | 'index' => $index, 63 | )); 64 | 65 | $this->view->setLinkBase(''); 66 | return $this->view->url('Main:getUrlSecurityParams', array( 67 | 'p1' => $params['p1'], 68 | 'p2' => $params['p2'], 69 | 'p3' => $params['p3'] 70 | )); 71 | } 72 | 73 | /** 74 | * 生成加密链接 75 | * 76 | * @return string 77 | * @throws CoreException 78 | */ 79 | function makeEncryptLink() 80 | { 81 | $params = $this->params; 82 | $type = $this->params['type']; 83 | $dot = $this->params['dot']; 84 | $ext = $this->params['ext']; 85 | $index = $this->params['index']; 86 | 87 | $this->view->cleanLinkCache(); 88 | $this->config->set('url', array( 89 | 'ext' => $ext, 90 | 'dot' => $dot, 91 | 'type' => $type, 92 | 'index' => $index, 93 | )); 94 | 95 | $this->view->setLinkBase(''); 96 | return $this->view->sUrl('Main:getUrlSecurityParams', [ 97 | 'p1' => $params['p1'], 98 | 'p2' => $params['p2'], 99 | 'p3' => $params['p3'] 100 | ]); 101 | } 102 | 103 | /** 104 | * url加密 参数解密 105 | * 106 | * @cp_params p1, p2, p3 107 | * @throws CoreException 108 | */ 109 | function makeEncryptLinkAndDecryptParams() 110 | { 111 | $params = $this->params; 112 | $linkType = $this->params['link_type']; 113 | $dot = $this->params['dot']; 114 | $ext = $this->params['ext']; 115 | 116 | $this->view->cleanLinkCache(); 117 | $this->config->set('url', array( 118 | 'rewrite' => false, 119 | 'ext' => $ext, 120 | 'dot' => $dot, 121 | 'type' => $linkType 122 | )); 123 | 124 | $controller = 'Main'; 125 | $action = 'getUrlSecurityParams'; 126 | 127 | $this->view->setLinkBase(''); 128 | $url = $this->view->sUrl(sprintf('%s:%s', $controller, $action), array( 129 | 'p1' => $params['p1'], 130 | 'p2' => $params['p2'], 131 | 'p3' => $params['p3'] 132 | )); 133 | 134 | $customRouterParams[] = $controller; 135 | $customRouterParams[] = $action; 136 | 137 | if ($linkType > 2) { 138 | list(, $paramsString) = explode('?', $url); 139 | } else { 140 | $urlArray = explode($dot, $url); 141 | $paramsString = end($urlArray); 142 | } 143 | 144 | return $this->sParams(true, $paramsString); 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /tests/project/app/test/init.php: -------------------------------------------------------------------------------- 1 | [ 4 | 'auth' => 'COOKIE', 5 | 'default_tpl_dir' => 'default', 6 | 'display' => 'JSON' 7 | ], 8 | 'encrypt' => [ 9 | 'uri' => 'crossphp', 10 | 'auth' => '' 11 | ], 12 | 'url' => [ 13 | '*' => 'Main:index', 14 | 'type' => 1, 15 | 'rewrite' => false, 16 | 'dot' => '/', 17 | 'ext' => '', 18 | ], 19 | 'router' => [ 20 | 'hi' => 'main:index', 21 | ] 22 | ]; 23 | 24 | 25 | -------------------------------------------------------------------------------- /tests/project/app/test/templates/default/default.layer.php: -------------------------------------------------------------------------------- 1 | 4 | */ 5 | namespace app\test\views; 6 | 7 | use Cross\MVC\View; 8 | 9 | class MainView extends View 10 | { 11 | function __call($func, $data = null) { 12 | 13 | } 14 | } 15 | --------------------------------------------------------------------------------