├── tests ├── Config │ ├── component.php │ └── app.php ├── TestCase.php ├── bootstrap.php ├── Lib │ ├── StringsTest.php │ └── HttpClientTest.php ├── Cache │ ├── ApcCacheTest.php │ ├── ArrayCacheTest.php │ ├── FileCacheTest.php │ ├── RedisCacheTest.php │ ├── MemCacheTest.php │ ├── RedisClusterTest.php │ └── TestCaseTrait.php ├── Mutex │ ├── MysqlMutexTest.php │ ├── FileMutexTest.php │ └── MemMutexTest.php ├── Http │ └── UriTest.php └── Router │ └── RouterTest.php ├── .gitignore ├── src ├── Core │ ├── Bootstrap │ │ └── BootstrapInterface.php │ ├── Exception │ │ ├── CoreException.php │ │ ├── AppException.php │ │ ├── ServiceException.php │ │ ├── HttpException.php │ │ ├── HttpNotFoundException.php │ │ ├── DBException.php │ │ └── ErrorHandler.php │ ├── Container │ │ ├── NotFoundException.php │ │ ├── ContainerException.php │ │ ├── ServiceProviderInterface.php │ │ ├── ServiceProvider.php │ │ └── Container.php │ ├── Web │ │ └── Debug │ │ │ ├── Lib │ │ │ ├── Config.php │ │ │ ├── Storage.php │ │ │ └── Profile.php │ │ │ ├── View │ │ │ ├── index.php │ │ │ └── layout.php │ │ │ ├── Controller │ │ │ └── DebugController.php │ │ │ └── Middleware │ │ │ └── DebuggerMiddleware.php │ ├── Cache │ │ ├── InvalidArgumentException.php │ │ ├── CacheException.php │ │ ├── RedisClusterCache.php │ │ ├── CacheInterface.php │ │ ├── NullCache.php │ │ ├── ApcCache.php │ │ ├── ArrayCache.php │ │ ├── RedisCache.php │ │ └── MemCache.php │ ├── Logger │ │ ├── InvalidArgumentException.php │ │ ├── Formatter │ │ │ ├── FormatterInterface.php │ │ │ ├── LineFormatter.php │ │ │ ├── ArrayFormatter.php │ │ │ ├── JsonFormatter.php │ │ │ ├── ConsoleFormatter.php │ │ │ └── AbstractFormatter.php │ │ ├── LoggerInterface.php │ │ ├── Handler │ │ │ ├── NullHandler.php │ │ │ ├── ConsoleHandler.php │ │ │ ├── HandlerInterface.php │ │ │ ├── AbstractHandler.php │ │ │ ├── DbHandler.php │ │ │ └── FileHandler.php │ │ └── Logger.php │ ├── View │ │ ├── ViewException.php │ │ ├── Smarty.php │ │ ├── Native.php │ │ └── ViewInterface.php │ ├── Router │ │ ├── MethodNotAllowedException.php │ │ ├── ConsoleRouter.php │ │ ├── RouterInterface.php │ │ └── AbstractRouter.php │ ├── Session │ │ └── Handler │ │ │ ├── HandlerInterface.php │ │ │ └── Memcached.php │ ├── Mutex │ │ ├── GetLockTimeoutException.php │ │ ├── MemMutex.php │ │ ├── MutexInterface.php │ │ ├── MysqlMutex.php │ │ ├── MutexFactory.php │ │ ├── FileMutex.php │ │ └── MutexAbstract.php │ ├── Middleware │ │ └── MiddlewareInterface.php │ ├── Common.php │ ├── Lib │ │ ├── Arrays.php │ │ ├── Validate.php │ │ ├── Pager.php │ │ ├── Upload.php │ │ ├── Form.php │ │ ├── Tree.php │ │ ├── Files.php │ │ └── Strings.php │ ├── Cipher │ │ ├── CipherInterface.php │ │ └── Cipher.php │ ├── AbstractService.php │ ├── Event │ │ ├── DbEvent.php │ │ └── Event.php │ ├── Command.php │ ├── Events.php │ ├── Component.php │ ├── Config.php │ ├── Http │ │ ├── Cookie.php │ │ ├── Message.php │ │ ├── Headers.php │ │ ├── UploadedFile.php │ │ └── Stream.php │ └── Environment.php ├── Const.php └── ClassLoader.php ├── phpunit.xml ├── composer.json ├── doc ├── cache.md ├── router.md └── log.md └── README.md /tests/Config/component.php: -------------------------------------------------------------------------------- 1 | 9 | * @package Core\Exception 10 | */ 11 | class CoreException extends \Exception 12 | { 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/Core/Container/NotFoundException.php: -------------------------------------------------------------------------------- 1 | 9 | * @package Core\Logger 10 | */ 11 | class InvalidArgumentException extends \InvalidArgumentException 12 | { 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/Core/View/ViewException.php: -------------------------------------------------------------------------------- 1 | 11 | * @package Core\View 12 | */ 13 | class ViewException extends CoreException 14 | { 15 | 16 | } 17 | -------------------------------------------------------------------------------- /tests/Lib/StringsTest.php: -------------------------------------------------------------------------------- 1 | assertEquals(Strings::base64DecodeURL(Strings::base64EncodeURL($src)), $src); 10 | } 11 | } -------------------------------------------------------------------------------- /src/Core/Exception/AppException.php: -------------------------------------------------------------------------------- 1 | 11 | * @package Core\Exception 12 | */ 13 | class AppException extends \Exception 14 | { 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/Core/Exception/ServiceException.php: -------------------------------------------------------------------------------- 1 | 13 | * @package App\Exception 14 | */ 15 | class ServiceException extends AppException 16 | { 17 | 18 | } -------------------------------------------------------------------------------- /src/Core/Router/MethodNotAllowedException.php: -------------------------------------------------------------------------------- 1 | 11 | * @package Core\Cache 12 | */ 13 | class CacheException extends CoreException implements CacheExceptionInterface 14 | { 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/Core/Exception/HttpException.php: -------------------------------------------------------------------------------- 1 | 9 | * @package Core\Exception 10 | */ 11 | class HttpException extends \Exception 12 | { 13 | public function __construct($code, $message = '') 14 | { 15 | parent::__construct($message, $code); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Core/Exception/HttpNotFoundException.php: -------------------------------------------------------------------------------- 1 | 9 | * @package Core\Exception 10 | */ 11 | class HttpNotFoundException extends HttpException 12 | { 13 | public function __construct() 14 | { 15 | parent::__construct(404, 'Page Not Found'); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /tests/Cache/ApcCacheTest.php: -------------------------------------------------------------------------------- 1 | 'test_']; 17 | $this->cache = new \Core\Cache\ApcCache($config); 18 | } 19 | } -------------------------------------------------------------------------------- /tests/Cache/ArrayCacheTest.php: -------------------------------------------------------------------------------- 1 | 'test_']; 17 | $this->cache = new \Core\Cache\ArrayCache($config); 18 | } 19 | } -------------------------------------------------------------------------------- /src/Core/Session/Handler/HandlerInterface.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | interface FormatterInterface 11 | { 12 | // 设置日期格式 13 | public function setDateFormat($format); 14 | 15 | // 获取日期格式 16 | public function getDateFormat(); 17 | 18 | // 格式化日志 19 | public function format(array $record); 20 | } -------------------------------------------------------------------------------- /src/Core/Mutex/GetLockTimeoutException.php: -------------------------------------------------------------------------------- 1 | 'test_', 'save_path' => '/tmp/cache']; 17 | $this->cache = new \Core\Cache\FileCache($config); 18 | } 19 | } -------------------------------------------------------------------------------- /tests/Cache/RedisCacheTest.php: -------------------------------------------------------------------------------- 1 | 'test_', 'host' => 'localhost']; 17 | $this->cache = new \Core\Cache\RedisCache($config); 18 | } 19 | } -------------------------------------------------------------------------------- /tests/Cache/MemCacheTest.php: -------------------------------------------------------------------------------- 1 | 'test_', 'servers' => [['localhost',11211]]]; 17 | $this->cache = new \Core\Cache\MemCache($config); 18 | } 19 | } -------------------------------------------------------------------------------- /src/Core/Middleware/MiddlewareInterface.php: -------------------------------------------------------------------------------- 1 | 11 | * @package Core\Middleware 12 | */ 13 | interface MiddlewareInterface 14 | { 15 | /** 16 | * 处理请求 17 | * 18 | * @param Request $request 19 | * @param callable $next 20 | * @return Response 21 | */ 22 | public function process(Request $request, callable $next); 23 | } -------------------------------------------------------------------------------- /src/Core/Logger/Handler/NullHandler.php: -------------------------------------------------------------------------------- 1 | 11 | * @package Core\Logger 12 | */ 13 | class NullHandler extends AbstractHandler 14 | { 15 | public function init() 16 | { 17 | 18 | } 19 | 20 | public function getDefaultFormatter() 21 | { 22 | 23 | } 24 | 25 | public function handleRecord(array $record) 26 | { 27 | return true; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tests/Cache/RedisClusterTest.php: -------------------------------------------------------------------------------- 1 | ['localhost:7001', 'localhost:7002', 'localhost:7003'] 18 | ]; 19 | $this->cache = new \Core\Cache\RedisClusterCache($config); 20 | } 21 | } -------------------------------------------------------------------------------- /src/Core/Logger/Formatter/LineFormatter.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class LineFormatter extends AbstractFormatter 11 | { 12 | public function format(array $record) 13 | { 14 | $this->parseContext($record); 15 | 16 | $message = $record['datetime']->format($this->getDateFormat()) . " [{$record['channel']}] [{$record['level_name'][0]}] [{$record['file']}:{$record['line']}] {$record['message']}"; 17 | 18 | return $message; 19 | } 20 | } -------------------------------------------------------------------------------- /src/Core/Common.php: -------------------------------------------------------------------------------- 1 | 6 | * @package core 7 | */ 8 | 9 | 10 | /** 11 | * 生成URL 12 | * 13 | * @param string $route 14 | * @param array $params 参数 15 | * @param bool $full 16 | * @return string 17 | */ 18 | function URL($route = '', $params = [], $full = false) 19 | { 20 | return App::router()->makeUrl($route, $params, $full); 21 | } 22 | 23 | /** 24 | * 语言包解析 25 | * @param string $lang 26 | * @param array $params 27 | * @return string 28 | */ 29 | function L($lang, $params = []) 30 | { 31 | return App::lang($lang, $params); 32 | } 33 | -------------------------------------------------------------------------------- /tests/Mutex/MysqlMutexTest.php: -------------------------------------------------------------------------------- 1 | lock = \Core\Mutex\MutexFactory::createMysqlMutex(App::db()); 13 | } 14 | 15 | /** 16 | * @expectedException \Core\Mutex\GetLockTimeoutException 17 | */ 18 | public function testLockException() 19 | { 20 | $db = App::Db(); 21 | $db->select("SELECT GET_LOCK(?, ?)", ['foo', 10], false); // 连接1 22 | $this->lock->lock('foo', 1); // 连接2 23 | } 24 | } -------------------------------------------------------------------------------- /tests/Mutex/FileMutexTest.php: -------------------------------------------------------------------------------- 1 | lock('foo'); 13 | $lock->lock('foo', 1); 14 | } 15 | 16 | public function testLock() 17 | { 18 | $lock = \Core\Mutex\MutexFactory::createFileMutex(); 19 | for ($i = 0; $i<10; $i++) { 20 | $lock->lock('foo2', 1); 21 | $lock->unlock('foo2'); 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /src/Core/Lib/Arrays.php: -------------------------------------------------------------------------------- 1 | 8 | * @package App\Util 9 | */ 10 | class Arrays 11 | { 12 | /** 13 | * 使用指定字段重新索引数组 14 | * 15 | * @param array $data 16 | * @param $idx 17 | * @return array 18 | */ 19 | public static function index(array $data, $idx) 20 | { 21 | if (empty($data) || !isset($data[0][$idx])) { 22 | return $data; 23 | } 24 | $result = []; 25 | foreach ($data as $row) { 26 | $result[$row[$idx]] = $row; 27 | } 28 | return $result; 29 | } 30 | } -------------------------------------------------------------------------------- /src/Core/Mutex/MemMutex.php: -------------------------------------------------------------------------------- 1 | cache = $cache; 20 | } 21 | 22 | protected function doUnlock($name) 23 | { 24 | return $this->cache->delete($this->prefix . $name); 25 | } 26 | 27 | public function tryLock($name) 28 | { 29 | return $this->cache->add($this->prefix . $name, time(), $this->lockTime); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Core/Mutex/MutexInterface.php: -------------------------------------------------------------------------------- 1 | assertEquals($uri->getScheme(), 'https'); 10 | $this->assertEquals($uri->getAuthority(), 'user:pass@localhost:81'); 11 | $this->assertEquals($uri->getHost(), 'localhost'); 12 | $this->assertEquals($uri->getPort(), 81); 13 | $this->assertEquals($uri->getUserInfo(), 'user:pass'); 14 | $this->assertEquals($uri->getQuery(), 'c=' . urlencode('中 国')); 15 | $this->assertEquals(rawurldecode($uri->getPath()), '/a b c/b'); 16 | 17 | } 18 | } -------------------------------------------------------------------------------- /src/Core/Cipher/CipherInterface.php: -------------------------------------------------------------------------------- 1 | 8 | * @package Core\Cipher 9 | */ 10 | interface CipherInterface 11 | { 12 | /** 13 | * 加密 14 | * 15 | * @param string $text 要加密的内容 16 | * @param string $key 密钥 17 | * @param bool $raw 是否返回原始数据,true则返回原始二进制格式,false则返回base64编码后的字符串 18 | * @return string 19 | */ 20 | public function encrypt($text, $key, $raw = false); 21 | 22 | /** 23 | * 解密 24 | * 25 | * @param string $text 密文 26 | * @param string $key 密钥 27 | * @param bool $raw 密文是否为原始二进制格式 28 | * @return string 29 | */ 30 | public function decrypt($text, $key, $raw = false); 31 | 32 | } -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | tests 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /tests/Config/app.php: -------------------------------------------------------------------------------- 1 | [ 5 | 'default' => [ 6 | 'slow_log' => 0, 7 | 'prefix' => '', 8 | 'charset' => 'utf8', 9 | 'timeout' => 3, 10 | 'write' => [ 11 | 'dsn' => "mysql:host=127.0.0.1;port=3306;dbname=test;charset=utf8", 12 | 'username' => 'root', 13 | 'password' => '', 14 | 'pconnect' => false, 15 | ], 16 | 'read' => [ 17 | 'dsn' => "mysql:host=127.0.0.1;port=3306;dbname=test;charset=utf8", 18 | 'username' => 'root', 19 | 'password' => '', 20 | 'pconnect' => false, 21 | ] 22 | ], 23 | ], 24 | ]; -------------------------------------------------------------------------------- /src/Core/Logger/Formatter/ArrayFormatter.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class ArrayFormatter extends AbstractFormatter 11 | { 12 | public function format(array $record) 13 | { 14 | $this->parseContext($record); 15 | 16 | $message = [ 17 | 'datetime' => $record['datetime']->format($this->getDateFormat()), 18 | 'channel' => $record['channel'], 19 | 'level' => $record['level_name'], 20 | 'message' => $record['message'], 21 | 'file' => $record['file'], 22 | 'line' => $record['line'], 23 | ]; 24 | 25 | return $message; 26 | } 27 | } -------------------------------------------------------------------------------- /src/Core/Container/ServiceProvider.php: -------------------------------------------------------------------------------- 1 | container = $container; 12 | $this->config = $config; 13 | } 14 | 15 | public function get($name) 16 | { 17 | if (isset($this->config[$name])) { 18 | $this->container->set($name, $this->config[$name], true); 19 | } 20 | return $this->container->get($name); 21 | } 22 | 23 | public function has($name) 24 | { 25 | if (isset($this->config[$name])) { 26 | return true; 27 | } 28 | return false; 29 | } 30 | } -------------------------------------------------------------------------------- /src/Core/Logger/Formatter/JsonFormatter.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class JsonFormatter extends AbstractFormatter 11 | { 12 | public function format(array $record) 13 | { 14 | $this->parseContext($record); 15 | 16 | $message = [ 17 | 'datetime' => $record['datetime']->format($this->getDateFormat()), 18 | 'channel' => $record['channel'], 19 | 'level' => $record['level_name'], 20 | 'message' => $record['message'], 21 | 'file' => $record['file'], 22 | 'line' => $record['line'], 23 | ]; 24 | 25 | return json_encode($message, JSON_UNESCAPED_UNICODE); 26 | } 27 | } -------------------------------------------------------------------------------- /src/Core/Logger/Handler/ConsoleHandler.php: -------------------------------------------------------------------------------- 1 | 10 | * @package core\logger\handler 11 | */ 12 | class ConsoleHandler extends AbstractHandler 13 | { 14 | private $isCli = false; 15 | 16 | public function init() 17 | { 18 | $this->isCli = (php_sapi_name() == 'cli'); 19 | } 20 | 21 | public function getDefaultFormatter() 22 | { 23 | return new ConsoleFormatter(); 24 | } 25 | 26 | public function handleRecord(array $record) 27 | { 28 | if ($this->isCli) { 29 | $message = $this->getFormatter()->format($record); 30 | fwrite(STDOUT, $message . "\n"); 31 | } 32 | return false; 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /tests/Mutex/MemMutexTest.php: -------------------------------------------------------------------------------- 1 | lock = \Core\Mutex\MutexFactory::createMemMutex($cache); 15 | } 16 | 17 | /** 18 | * @expectedException \Core\Mutex\GetLockTimeoutException 19 | */ 20 | public function testLockException() 21 | { 22 | $this->lock->lock('foo'); 23 | $this->lock->lock('foo', 1); 24 | } 25 | 26 | public function testLock() 27 | { 28 | for ($i = 0; $i<10; $i++) { 29 | $this->lock->lock('foo2', 1); 30 | $this->lock->unlock('foo2'); 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /src/Core/Mutex/MysqlMutex.php: -------------------------------------------------------------------------------- 1 | db = $db; 26 | } 27 | 28 | public function tryLock($name) 29 | { 30 | return $this->db->getOne("SELECT GET_LOCK(?, ?)", [$name, 0], 0, true); 31 | } 32 | 33 | protected function doUnlock($name) 34 | { 35 | return (bool)$this->db->getOne("SELECT RELEASE_LOCK(?)", [$name], 0, true); 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /src/Core/Cache/RedisClusterCache.php: -------------------------------------------------------------------------------- 1 | ['192.168.1.1:6379','192.168.1.2:6379', '192.168.1.3:6379'], 10 | * 'options' => [], 11 | * ] 12 | * 13 | * @author lisijie 14 | * @package Core\Cache 15 | */ 16 | class RedisClusterCache extends RedisCache 17 | { 18 | protected function init() 19 | { 20 | if (!class_exists('\\RedisCluster')) { 21 | throw new CacheException("当前环境不支持RedisCluster"); 22 | } 23 | $this->client = new \RedisCluster(null, $this->config['servers']); 24 | if (!empty($this->config['options'])) { 25 | foreach ($this->config['options'] as $name => $val) { // [\Redis::OPT_SERIALIZER => \Redis::SERIALIZER_PHP] 26 | $this->client->setOption($name, $val); 27 | } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Core/Logger/Handler/HandlerInterface.php: -------------------------------------------------------------------------------- 1 | 11 | * @package Core\Logger 12 | */ 13 | interface HandlerInterface 14 | { 15 | /** 16 | * 获取默认格式器 17 | * 18 | * @return FormatterInterface; 19 | */ 20 | public function getDefaultFormatter(); 21 | 22 | /** 23 | * 获取格式器 24 | * 25 | * @return FormatterInterface; 26 | */ 27 | public function getFormatter(); 28 | 29 | /** 30 | * 设置格式器 31 | * 32 | * @param FormatterInterface $formatter 33 | * @return mixed 34 | */ 35 | public function setFormatter(FormatterInterface $formatter); 36 | 37 | /** 38 | * 日志处理方法 39 | * 40 | * @param array $record 日志数据 41 | * @return mixed 42 | */ 43 | public function handle(array $record); 44 | 45 | } 46 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lisijie/framework", 3 | "description": "A PHP Framework", 4 | "keywords": [ 5 | "php", 6 | "framework" 7 | ], 8 | "homepage": "https://github.com/lisijie/php-framework", 9 | "type": "framework", 10 | "license": "BSD-3-Clause", 11 | "authors": [ 12 | { 13 | "name": "lisijie", 14 | "email": "lsj86@qq.com", 15 | "homepage": "https://github.com/lisijie" 16 | } 17 | ], 18 | "require": { 19 | "php": ">=5.4.0", 20 | "ext-mbstring": "*", 21 | "psr/simple-cache": "~1.0", 22 | "psr/container": "~1.0", 23 | "psr/http-message": "~1.0" 24 | }, 25 | "require-dev": { 26 | "phpunit/phpunit": "~4.0", 27 | "smarty/smarty": "~3.1" 28 | }, 29 | "autoload": { 30 | "files": ["src/App.php"] 31 | }, 32 | "extra": { 33 | "branch-alias": { 34 | "dev-master": "2.2.x-dev" 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Core/Router/ConsoleRouter.php: -------------------------------------------------------------------------------- 1 | 8 | * @package Core\Router 9 | */ 10 | class ConsoleRouter extends AbstractRouter implements RouterInterface 11 | { 12 | public function makeUrl($route, $params = [], $full = false) 13 | { 14 | $route = $this->normalizeRoute($route); 15 | foreach ($params as $k => $v) { 16 | $params[$k] = escapeshellarg($v); 17 | } 18 | return ($full ? $_SERVER['argv'][0] : '') . ' ' . $route . ' ' . implode(' ', $params); 19 | } 20 | 21 | /** 22 | * 解析 23 | * @param null $argv 24 | * @return mixed 25 | */ 26 | public function resolve($argv = null) 27 | { 28 | if (null === $argv) { 29 | $argv = $_SERVER['argv']; 30 | } 31 | array_shift($argv); 32 | if (!empty($argv)) { 33 | $routeName = array_shift($argv); 34 | $this->routeName = $this->normalizeRoute($routeName); 35 | } 36 | $this->params = $argv; 37 | return $this->resolveHandler(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Core/Cache/CacheInterface.php: -------------------------------------------------------------------------------- 1 | 12 | * @package Core\Cache 13 | */ 14 | interface CacheInterface extends SimpleCacheInterface 15 | { 16 | /** 17 | * 增加一个元素,如果元素已存在将返回false 18 | * 19 | * @param string $key 20 | * @param mixed $value 21 | * @param int $ttl 缓存保留时间/秒,0为永久 22 | * @return bool 成功返回true,失败返回false 23 | */ 24 | public function add($key, $value, $ttl = 0); 25 | 26 | /** 27 | * 增加数值元素的值 28 | * 29 | * 如果指定的 key 不存在,则自动创建。 30 | * 31 | * @param string $key 32 | * @param int $step 33 | * @return int|bool 返回新值,失败返回false 34 | */ 35 | public function increment($key, $step = 1); 36 | 37 | /** 38 | * 减小数值元素的值 39 | * 40 | * 如果指定的 key 不存在,则自动创建。 41 | * 42 | * @param string $key 43 | * @param int $step 44 | * @return int|bool 返回新值,失败返回false 45 | */ 46 | public function decrement($key, $step = 1); 47 | } 48 | -------------------------------------------------------------------------------- /src/Core/Exception/DBException.php: -------------------------------------------------------------------------------- 1 | sql = $sql; 21 | $this->params = $params; 22 | } 23 | 24 | public function getSql() 25 | { 26 | return $this->sql; 27 | } 28 | 29 | public function getParams() 30 | { 31 | return $this->params; 32 | } 33 | 34 | public function __toString() 35 | { 36 | $message = sprintf("exception '%s' with message '%s' in %s:%s\n", get_class($this), $this->message, $this->file, $this->line); 37 | $message .= "Sql: {$this->sql}\n"; 38 | $message .= "Params: " . VarDumper::export($this->params) . "\n"; 39 | $message .= "Stack trace:\n"; 40 | $message .= $this->getTraceAsString(); 41 | return $message; 42 | } 43 | } -------------------------------------------------------------------------------- /src/Core/Web/Debug/View/index.php: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 |
请求地址请求方法时间执行时间内存占用操作
formatTime($r['exec_time']) ?>formatSize($r['memory']) ?>查看
27 | 28 |
29 |
30 | -------------------------------------------------------------------------------- /src/Const.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class ConsoleFormatter extends AbstractFormatter 11 | { 12 | public function format(array $record) 13 | { 14 | $this->parseContext($record); 15 | 16 | $message = $record['datetime']->format($this->getDateFormat()) . " [{$record['channel']}] [" . $this->colorLevelName($record['level_name'][0]) . "] [{$record['file']}:{$record['line']}] {$record['message']}"; 17 | 18 | return $message; 19 | } 20 | 21 | private function colorLevelName($levelName) 22 | { 23 | switch ($levelName) { 24 | case 'D': 25 | return "\033[1;32m{$levelName}\033[0m"; 26 | break; 27 | case 'I': 28 | return "\033[1;34m{$levelName}\033[0m"; 29 | break; 30 | case 'W': 31 | return "\033[1;33m{$levelName}\033[0m"; 32 | break; 33 | case 'E': 34 | return "\033[1;31m{$levelName}\033[0m"; 35 | break; 36 | case 'F': 37 | return "\033[1;35m{$levelName}\033[0m"; 38 | break; 39 | } 40 | return $levelName; 41 | } 42 | } -------------------------------------------------------------------------------- /src/Core/AbstractService.php: -------------------------------------------------------------------------------- 1 | 16 | * @package Core 17 | */ 18 | abstract class AbstractService extends Component 19 | { 20 | /** 21 | * @var LoggerInterface 22 | */ 23 | protected $logger; 24 | 25 | /** 26 | * @var array 27 | */ 28 | protected static $instances = []; 29 | 30 | /** 31 | * 不允许直接实例化 32 | */ 33 | private final function __construct() 34 | { 35 | $this->logger = App::logger(); 36 | $this->init(); 37 | } 38 | 39 | /** 40 | * 获取实例 41 | * @return static 42 | */ 43 | public final static function getInstance() 44 | { 45 | $cls = get_called_class(); 46 | if (!array_key_exists($cls, static::$instances)) { 47 | static::$instances[$cls] = new static(); 48 | } 49 | return static::$instances[$cls]; 50 | } 51 | 52 | /** 53 | * 初始化方法 54 | */ 55 | protected function init() 56 | { 57 | // 初始化方法 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Core/Mutex/MutexFactory.php: -------------------------------------------------------------------------------- 1 | lockTime = $lockTime; 39 | $mu->setCache($cacheObject); 40 | return $mu; 41 | } 42 | 43 | /** 44 | * 创建基于MySQL的锁 45 | * 46 | * @param Db $db 47 | * @param bool|true $autoUnlock 是否自动释放锁 48 | * @return MysqlMutex 49 | */ 50 | public static function createMysqlMutex(Db $db, $autoUnlock = true) 51 | { 52 | $mu = new MysqlMutex($autoUnlock); 53 | $mu->setDb($db); 54 | return $mu; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Core/Mutex/FileMutex.php: -------------------------------------------------------------------------------- 1 | path = DATA_PATH . '/mutex'; 23 | if (!is_dir($this->path)) { 24 | Files::makeDir($this->path); 25 | } 26 | } 27 | 28 | protected function doUnlock($name) 29 | { 30 | if (!isset($this->files[$name]) || !flock($this->files[$name], LOCK_UN)) { 31 | return false; 32 | } 33 | fclose($this->files[$name]); 34 | unlink($this->getLockFile($name)); 35 | unset($this->files[$name]); 36 | return true; 37 | } 38 | 39 | private function getLockFile($name) 40 | { 41 | return $this->path . '/'. md5($name) . '.lock'; 42 | } 43 | 44 | public function tryLock($name) 45 | { 46 | $lockFile = $this->getLockFile($name); 47 | $fp = fopen($lockFile, 'w+'); 48 | if (flock($fp, LOCK_EX | LOCK_NB)) { 49 | $this->files[$name] = $fp; 50 | return true; 51 | } 52 | fclose($fp); 53 | return false; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Core/Router/RouterInterface.php: -------------------------------------------------------------------------------- 1 | array( 11 | * array('mem1.domain.com', 11211, 33), 12 | * array('mem2.domain.com', 11211, 67) 13 | * ), 14 | * ); 15 | * 16 | * @author lisijie 17 | * @package Core\Session 18 | */ 19 | class Memcached implements HandlerInterface 20 | { 21 | 22 | private $handler; 23 | private $ttl; 24 | 25 | public function __construct(array $options) 26 | { 27 | if (empty($options['servers'])) { 28 | throw new \InvalidArgumentException('缺少Memcached服务器配置'); 29 | } 30 | $this->handler = new \Memcached(); 31 | $this->handler->addServers($options['servers']); 32 | $this->ttl = intval(ini_get('session.cookie_lifetime')); 33 | $this->handler->setOptions([ 34 | \Memcached::OPT_DISTRIBUTION => \Memcached::DISTRIBUTION_CONSISTENT, 35 | ]); 36 | } 37 | 38 | public function open($save_path, $session_id) 39 | { 40 | 41 | } 42 | 43 | public function close() 44 | { 45 | 46 | } 47 | 48 | public function read($session_id) 49 | { 50 | return $this->handler->get($session_id); 51 | } 52 | 53 | public function write($session_id, $session_data) 54 | { 55 | return $this->handler->set($session_id, $session_data, 0, $this->ttl); 56 | } 57 | 58 | public function destroy($session_id) 59 | { 60 | return $this->handler->delete($session_id); 61 | } 62 | 63 | public function gc($maxlifetime) 64 | { 65 | 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Core/Event/DbEvent.php: -------------------------------------------------------------------------------- 1 | 8 | * @package Core\Event; 9 | */ 10 | class DbEvent extends Event 11 | { 12 | /** 13 | * 查询SQL 14 | * @var string 15 | */ 16 | private $sql; 17 | 18 | /** 19 | * 查询参数 20 | * @var array 21 | */ 22 | private $params = []; 23 | 24 | /** 25 | * 是否主库查询 26 | * @var bool 27 | */ 28 | private $fromMaster = false; 29 | 30 | /** 31 | * 查询耗时 32 | * @var float 33 | */ 34 | private $time = 0; 35 | 36 | /** 37 | * 查询结果 38 | * @var mixed 39 | */ 40 | private $result = null; 41 | 42 | public function __construct($sql, $params, $fromMaster, $time, $result = null) 43 | { 44 | $this->sql = $sql; 45 | $this->params = $params; 46 | $this->fromMaster = (bool)$fromMaster; 47 | $this->time = $time; 48 | $this->result = $result; 49 | } 50 | 51 | /** 52 | * @return string 53 | */ 54 | public function getSql() 55 | { 56 | return $this->sql; 57 | } 58 | 59 | /** 60 | * @return array 61 | */ 62 | public function getParams() 63 | { 64 | return $this->params; 65 | } 66 | 67 | /** 68 | * @return boolean 69 | */ 70 | public function isFromMaster() 71 | { 72 | return $this->fromMaster; 73 | } 74 | 75 | /** 76 | * @return float 77 | */ 78 | public function getTime() 79 | { 80 | return $this->time; 81 | } 82 | 83 | /** 84 | * @return mixed 85 | */ 86 | public function getResult() 87 | { 88 | return $this->result; 89 | } 90 | } -------------------------------------------------------------------------------- /src/Core/View/Smarty.php: -------------------------------------------------------------------------------- 1 | 8 | * @package Core\View 9 | */ 10 | class Smarty extends AbstractView 11 | { 12 | private $smarty; 13 | 14 | protected function init() 15 | { 16 | if (!isset($this->config['ext'])) { 17 | $this->config['ext'] = '.html'; 18 | } 19 | $defaults = [ 20 | 'template_dir' => VIEW_PATH, 21 | 'config_dir' => VIEW_PATH . '/config', 22 | 'compile_dir' => DATA_PATH . '/cache/smarty_complied', 23 | 'cache_dir' => DATA_PATH . '/cache/smarty_cache', 24 | ]; 25 | $this->config = array_merge($defaults, $this->config); 26 | if (!class_exists('\Smarty')) { 27 | throw new ViewException('Smarty 类不存在,请使用composer安装'); 28 | } 29 | $this->smarty = new \Smarty(); 30 | foreach ($defaults as $key => $value) { 31 | $this->smarty->{$key} = $this->config[$key]; 32 | } 33 | 34 | // 注册smarty模板函数,以支持布局模板 35 | $this->smarty->registerPlugin('function', 'layout_section', [$this, 'section']); 36 | $this->smarty->registerPlugin('function', 'layout_content', [$this, 'content']); 37 | } 38 | 39 | public function section($params) 40 | { 41 | if (isset($params['name'])) { 42 | return parent::section($params['name']); 43 | } 44 | } 45 | 46 | protected function beforeRender() 47 | { 48 | $this->smarty->assign($this->data); 49 | } 50 | 51 | protected function renderFile($filename) 52 | { 53 | return $this->smarty->fetch($filename); 54 | } 55 | 56 | public function __call($method, $args) 57 | { 58 | return call_user_func_array([$this->smarty, $method], $args); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Core/Mutex/MutexAbstract.php: -------------------------------------------------------------------------------- 1 | autoUnlock = $autoUnlock; 23 | if ($this->autoUnlock) { 24 | $locks = &$this->locks; 25 | register_shutdown_function(function () use (&$locks) { 26 | foreach ($locks as $lock => $count) { 27 | $this->unlock($lock); 28 | } 29 | }); 30 | } 31 | $this->init(); 32 | } 33 | 34 | protected function init() 35 | { 36 | } 37 | 38 | /** 39 | * 获取锁 40 | * 41 | * 获取名称为$name的锁,成功返回true,失败返回false。 42 | * 43 | * @param string $name 锁名称 44 | * @param int $timeout 等待时间 45 | * @throws GetLockTimeoutException 46 | * @return bool 47 | */ 48 | public function lock($name, $timeout = 0) 49 | { 50 | $waitTime = 0; 51 | while (!$this->tryLock($name)) { 52 | if ($timeout && ++$waitTime > $timeout) { 53 | throw new GetLockTimeoutException($name, $timeout); 54 | } 55 | sleep(1); 56 | } 57 | $this->locks[$name] = true; 58 | return true; 59 | } 60 | 61 | /** 62 | * 释放锁 63 | * 64 | * @param string $name 名称 65 | * @return bool 66 | */ 67 | public function unlock($name) 68 | { 69 | if ($this->doUnlock($name)) { 70 | unset($this->locks[$name]); 71 | return true; 72 | } 73 | return false; 74 | } 75 | 76 | abstract public function tryLock($name); 77 | 78 | abstract protected function doUnlock($name); 79 | 80 | } -------------------------------------------------------------------------------- /src/Core/Cache/NullCache.php: -------------------------------------------------------------------------------- 1 | 8 | * @package Core\Cache 9 | */ 10 | class NullCache implements CacheInterface 11 | { 12 | /** 13 | * {@inheritdoc} 14 | */ 15 | public function add($key, $value, $ttl = 0) 16 | { 17 | return false; 18 | } 19 | 20 | /** 21 | * {@inheritdoc} 22 | */ 23 | public function get($key, $default = null) 24 | { 25 | return $default; 26 | } 27 | 28 | /** 29 | * {@inheritdoc} 30 | */ 31 | public function set($key, $value, $ttl = null) 32 | { 33 | return false; 34 | } 35 | 36 | /** 37 | * {@inheritdoc} 38 | */ 39 | public function delete($key) 40 | { 41 | return true; 42 | } 43 | 44 | /** 45 | * {@inheritdoc} 46 | */ 47 | public function clear() 48 | { 49 | return true; 50 | } 51 | 52 | /** 53 | * {@inheritdoc} 54 | */ 55 | public function getMultiple($keys, $default = null) 56 | { 57 | $result = []; 58 | foreach ($keys as $key) { 59 | $result[$key] = $default; 60 | } 61 | return $result; 62 | } 63 | 64 | /** 65 | * {@inheritdoc} 66 | */ 67 | public function setMultiple($values, $ttl = null) 68 | { 69 | return false; 70 | } 71 | 72 | /** 73 | * {@inheritdoc} 74 | */ 75 | public function deleteMultiple($keys) 76 | { 77 | return true; 78 | } 79 | 80 | /** 81 | * {@inheritdoc} 82 | */ 83 | public function increment($key, $step = 1) 84 | { 85 | return false; 86 | } 87 | 88 | /** 89 | * {@inheritdoc} 90 | */ 91 | public function decrement($key, $step = 1) 92 | { 93 | return false; 94 | } 95 | 96 | /** 97 | * {@inheritdoc} 98 | */ 99 | public function has($key) 100 | { 101 | return false; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/Core/Logger/Formatter/AbstractFormatter.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | abstract class AbstractFormatter implements FormatterInterface 13 | { 14 | private $dateFormat = 'Y-m-d H:i:s'; 15 | 16 | /** 17 | * 返回日期格式 18 | * 19 | * @return string 20 | */ 21 | public function getDateFormat() 22 | { 23 | return $this->dateFormat; 24 | } 25 | 26 | /** 27 | * 设置日期格式 28 | * 29 | * @param string $dateFormat 30 | */ 31 | public function setDateFormat($dateFormat) 32 | { 33 | if (is_string($dateFormat)) { 34 | $this->dateFormat = $dateFormat; 35 | } 36 | } 37 | 38 | /** 39 | * 解析和替换消息中的变量 40 | * @param array $record 41 | * @return string 42 | */ 43 | protected function parseContext(array &$record) 44 | { 45 | if (false !== strpos($record['message'], '{')) { 46 | $replacements = []; 47 | foreach ($record['context'] as $key => $val) { 48 | if (is_null($val) || is_scalar($val) || (is_object($val) && method_exists($val, "__toString"))) { 49 | $replacements['{' . $key . '}'] = $val; 50 | } elseif (is_object($val)) { 51 | $replacements['{' . $key . '}'] = '[object ' . get_class($val) . ']'; 52 | } elseif (is_array($val)) { 53 | $replacements['{' . $key . '}'] = VarDumper::dumpAsString($val); 54 | } else { 55 | $replacements['{' . $key . '}'] = '[' . gettype($val) . ']'; 56 | } 57 | } 58 | $record['message'] = strtr($record['message'], $replacements); 59 | } 60 | } 61 | 62 | /** 63 | * 日志格式化 64 | * 65 | * @param array $record 66 | * @return mixed 67 | */ 68 | abstract function format(array $record); 69 | } -------------------------------------------------------------------------------- /src/Core/Event/Event.php: -------------------------------------------------------------------------------- 1 | 9 | * @package Core\Event 10 | */ 11 | class Event 12 | { 13 | /** 14 | * 事件名称 15 | * @var string 16 | */ 17 | private $name = ''; 18 | 19 | /** 20 | * 事件产生对象,在静态方法触发的类全局事件该值为触发的类名字符串 21 | * @var object|string 22 | */ 23 | private $sender = null; 24 | 25 | /** 26 | * 附加数据 27 | * @var null 28 | */ 29 | private $data = null; 30 | 31 | /** 32 | * 已处理标识,设为true的话则忽略掉后面的事件 33 | * @var bool 34 | */ 35 | private $handled = false; 36 | 37 | /** 38 | * @return string 39 | */ 40 | public function getName() 41 | { 42 | return $this->name; 43 | } 44 | 45 | /** 46 | * @param string $name 47 | */ 48 | public function setName($name) 49 | { 50 | $this->name = $name; 51 | } 52 | 53 | /** 54 | * @return object 55 | */ 56 | public function getSender() 57 | { 58 | return $this->sender; 59 | } 60 | 61 | /** 62 | * @param object $sender 63 | */ 64 | public function setSender($sender) 65 | { 66 | $this->sender = $sender; 67 | } 68 | 69 | /** 70 | * @return null 71 | */ 72 | public function getData() 73 | { 74 | return $this->data; 75 | } 76 | 77 | /** 78 | * @param null $data 79 | */ 80 | public function setData($data) 81 | { 82 | $this->data = $data; 83 | } 84 | 85 | /** 86 | * @return boolean 87 | */ 88 | public function isHandled() 89 | { 90 | return $this->handled; 91 | } 92 | 93 | /** 94 | * @param boolean $handled 95 | */ 96 | public function setHandled($handled) 97 | { 98 | $this->handled = $handled; 99 | } 100 | } -------------------------------------------------------------------------------- /src/Core/Logger/Handler/AbstractHandler.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | abstract class AbstractHandler implements HandlerInterface 15 | { 16 | /** 17 | * @var array 18 | */ 19 | protected $config = []; 20 | 21 | /** 22 | * @var int 23 | */ 24 | private $level = 0; 25 | 26 | /** 27 | * 格式器 28 | * 29 | * @var \Core\Logger\Formatter\FormatterInterface; 30 | */ 31 | protected $formatter; 32 | 33 | public function __construct(array $config = []) 34 | { 35 | if (isset($config['formatter'])) { 36 | $formatterClass = $config['formatter']; 37 | if (!class_exists($formatterClass)) { 38 | throw new InvalidArgumentException('找不到日志格式化类: ' . $formatterClass); 39 | } 40 | $this->formatter = new $formatterClass(); 41 | } 42 | if (isset($config['date_format'])) { 43 | $this->getFormatter()->setDateFormat($config['date_format']); 44 | } 45 | if (isset($config['level'])) { 46 | $this->level = (int)$config['level']; 47 | } 48 | $this->config = $config; 49 | $this->init(); 50 | } 51 | 52 | abstract function init(); 53 | 54 | abstract function getDefaultFormatter(); 55 | 56 | public function handle(array $record) 57 | { 58 | if ($record['level'] < $this->level) { 59 | return false; 60 | } 61 | return $this->handleRecord($record); 62 | } 63 | 64 | abstract function handleRecord(array $record); 65 | 66 | public function setFormatter(FormatterInterface $formatter) 67 | { 68 | $this->formatter = $formatter; 69 | } 70 | 71 | public function getFormatter() 72 | { 73 | if (!$this->formatter) { 74 | $this->formatter = $this->getDefaultFormatter(); 75 | } 76 | return $this->formatter; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /doc/cache.md: -------------------------------------------------------------------------------- 1 | # 缓存 2 | 3 | 支持以下缓存类型: 4 | - File 5 | - Apc 6 | - Memcached 7 | - Redis 8 | - RedisCluster 9 | 10 | ## 配置 11 | 12 | 需要在 app.php 配置文件中增加以下cache配置项,如 13 | 14 | ```php 15 | 'cache' => [ 16 | 'default' => [ 17 | 'class' => \Core\Cache\NullCache::class, 18 | 'config' => [], 19 | ], 20 | 'node2' => [ 21 | 'class' => \Core\Cache\RedisCache::class, 22 | 'config' => [ 23 | 'host' => 'localhost', 24 | 'port' => 6379, 25 | 'auth' => '123456', 26 | ], 27 | ], 28 | ], 29 | ``` 30 | 31 | 其中 `default` 为默认节点配置,当不加参数调用 `App::cache()` 时,返回默认节点的缓存对象。 后面的 `node2` 为其他缓存节点的配置,在节点的配置中,`class` 为实现 Core\Cache\CacheInterface 接口的缓存驱动类名,可以自己扩展,`config` 则是驱动的配置信息。 32 | 33 | 下面列出各种缓存的配置选项: 34 | 35 | **FileCache配置** 36 | 37 | - save_path 缓存文件存放目录 38 | 39 | **ApcCache配置** 40 | 41 | 无配置项,需要安装APC扩展:https://pecl.php.net/package/apcu 42 | 43 | **MemCache配置** 44 | 45 | 需要安装memcached扩展:https://pecl.php.net/package/memcached 46 | 47 | - servers 服务器列表,如 48 | ```php 49 | [ 50 | ['192.168.1.1', 11211], // memcached服务器1 51 | ['192.168.1.2', 11211], // memcached服务器2 52 | ], 53 | ``` 54 | - options 可选。Memcached的一些配置项,为关联数组,其中键是要设置的选项,而值是选项的新值。例如: 55 | ```php 56 | [ 57 | \Memcached::OPT_DISTRIBUTION => \Memcached::DISTRIBUTION_CONSISTENT, 58 | \Memcached::OPT_SERIALIZER => \Memcached::SERIALIZER_JSON, 59 | ] 60 | ``` 61 | 62 | **Redis配置** 63 | 64 | 需要安装redis c扩展:https://pecl.php.net/package/redis 65 | 66 | - host Redis服务器IP,默认为127.0.0.1 67 | - port 端口,默认为6379 68 | - timeout 超时设置,默认0 69 | - auth 授权密码 70 | - options Redis的其他配置项,为关联数组。 71 | 72 | **RedisCluster配置** 73 | 74 | Redis 3.x支持的集群模式,需要确保你的Redis集群是使用这种方式搭建。 75 | 76 | - servers 服务器列表,如: 77 | ```php 78 | ['192.168.1.1:6379','192.168.1.2:6379', '192.168.1.3:6379'] 79 | ``` 80 | - options Redis的其他配置项,为关联数组。 81 | 82 | ## 使用 83 | 84 | 使用 `App::cache()` 方法获取一个缓存对象。 85 | 86 | ```php 87 | $cache = \App::cache(); 88 | $cache->set('foo', 'bar'); 89 | echo $cache->get('foo'); 90 | ``` 91 | 92 | 要获取某个节点的对象,需要传入节点参数。 93 | 94 | ```php 95 | $cache = \App::cache('node1'); 96 | ``` 97 | 98 | 支持的操作方法参照 `CacheInterface` 接口。 99 | -------------------------------------------------------------------------------- /src/Core/Cache/ApcCache.php: -------------------------------------------------------------------------------- 1 | 8 | * @link http://www.php.net/manual/zh/book.apc.php 9 | * @package Core\Cache 10 | */ 11 | class ApcCache extends AbstractCache 12 | { 13 | private $apcu = false; 14 | 15 | public function init() 16 | { 17 | $this->apcu = function_exists('apcu_fetch'); 18 | if (!$this->apcu && !function_exists('apc_store')) { 19 | throw new CacheException("当前环境不支持 APC 缓存"); 20 | } 21 | } 22 | 23 | protected function doAdd($key, $value, $ttl = 0) 24 | { 25 | return $this->apcu ? apcu_add($key, $value, $ttl) : apc_add($key, $value, $ttl); 26 | } 27 | 28 | protected function doSetMultiple(array $values, $ttl = 0) 29 | { 30 | // 返回错误的key 31 | $this->apcu ? apcu_store($values, null, $ttl) : apc_store($values, null, $ttl); 32 | return true; 33 | } 34 | 35 | protected function doGetMultiple(array $keys, $default = null) 36 | { 37 | $data = $this->apcu ? apcu_fetch($keys) : apc_fetch($keys); 38 | return $data; 39 | } 40 | 41 | protected function doDeleteMultiple(array $keys) 42 | { 43 | $this->apcu ? apc_delete($keys) : apcu_delete($keys); 44 | return true; 45 | } 46 | 47 | protected function doIncrement($key, $step = 1) 48 | { 49 | if (!$this->doHas($key)) { 50 | $this->apcu ? apcu_store($key, 0) : apc_store($key, 0); 51 | } 52 | return $this->apcu ? apcu_inc($key, $step) : apc_inc($key, $step); 53 | } 54 | 55 | protected function doDecrement($key, $step = 1) 56 | { 57 | if (!$this->doHas($key)) { 58 | $this->apcu ? apcu_store($key, 0) : apc_store($key, 0); 59 | } 60 | return $this->apcu ? apcu_dec($key, $step) : apc_dec($key, $step); 61 | } 62 | 63 | protected function doClear() 64 | { 65 | return $this->apcu ? apcu_clear_cache() : apc_clear_cache('user'); 66 | } 67 | 68 | protected function doHas($key) 69 | { 70 | return $this->apcu ? apcu_exists($key) : apc_exists($key); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /tests/Router/RouterTest.php: -------------------------------------------------------------------------------- 1 | addConfig($config); 16 | $request = new \Core\Http\Request(); 17 | 18 | $request->setRequestUri('/'); 19 | $router->resolve($request); 20 | $this->assertTrue($router->getRoute() == 'home/index'); 21 | 22 | $request->setRequestUri('/users'); 23 | $router->resolve($request); 24 | $this->assertTrue($router->getRoute() == 'user/list'); 25 | 26 | $request->setRequestUri('/user/123'); 27 | $router->resolve($request); 28 | $this->assertTrue($router->getRoute() == 'user/info'); 29 | $this->assertTrue($router->getParam('id') == 123); 30 | } 31 | 32 | /** 33 | * @expectedException \Core\Router\MethodNotAllowedException 34 | */ 35 | public function testNotAllowedRoute() 36 | { 37 | $config = [ 38 | ['/register', 'user/register', 'post'], 39 | ]; 40 | $router = Core\Router\Router::factory('rewrite'); 41 | $router->addConfig($config); 42 | $request = new \Core\Http\Request(); 43 | $request->setRequestUri('/register'); 44 | $router->resolve($request); 45 | } 46 | 47 | public function testNotExistsRoute() 48 | { 49 | $config = []; 50 | $router = Core\Router\Router::factory('rewrite', ['default_route' => 'home/index']); 51 | $router->addConfig($config); 52 | $request = new \Core\Http\Request(); 53 | $request->setRequestUri('/not/exists/path'); 54 | $router->resolve($request); 55 | $this->assertTrue($router->getRoute() == 'not/exists/path'); 56 | 57 | // default 58 | $request->setRequestUri('/'); 59 | $router->resolve($request); 60 | $this->assertTrue($router->getRoute() == 'home/index'); 61 | } 62 | } -------------------------------------------------------------------------------- /src/Core/View/Native.php: -------------------------------------------------------------------------------- 1 | 8 | * @package Core\View 9 | */ 10 | class Native extends AbstractView 11 | { 12 | 13 | protected $funcMap = []; 14 | 15 | /** 16 | * 注册模板函数 17 | * 18 | * @param string $name 函数名 19 | * @param callable $func 回调函数 20 | */ 21 | public function registerFunc($name, $func) 22 | { 23 | $this->funcMap[$name] = $func; 24 | } 25 | 26 | /** 27 | * 渲染单个文件 28 | * 29 | * @param $_file_ 30 | * @return string 31 | */ 32 | protected function renderFile($_file_) 33 | { 34 | ob_start(); 35 | @extract($this->data, EXTR_OVERWRITE); 36 | include $_file_; 37 | return ob_get_clean(); 38 | } 39 | 40 | /** 41 | * 在布局模板中输出页面主体 42 | */ 43 | public function content() 44 | { 45 | echo parent::content(); 46 | } 47 | 48 | /** 49 | * 输出布局下的子模板占位符 50 | * 51 | * @param string $name 名称 52 | * @return string|void 53 | */ 54 | public function section($name) 55 | { 56 | echo parent::section($name); 57 | } 58 | 59 | /** 60 | * 渲染页面组件 61 | * 62 | * @param string $tplFile 组件模板文件地址 63 | * @param array $data 模板变量 64 | * @param bool $return 是否返回结果, false 表示直接输出 65 | * @return string 66 | */ 67 | public function widget($tplFile, $data = [], $return = false) 68 | { 69 | $oldData = $this->data; 70 | $this->data = $data; 71 | $result = $this->renderFile($this->getViewFile($tplFile)); 72 | $this->data = $oldData; 73 | if ($return) { 74 | return $result; 75 | } 76 | echo $result; 77 | } 78 | 79 | /** 80 | * 调用注册的模板函数 81 | * 82 | * @param string $method 方法名 83 | * @param array $args 参数 84 | * @return mixed 85 | * @throws ViewException 86 | */ 87 | public function __call($method, $args) 88 | { 89 | if (isset($this->funcMap[$method])) { 90 | return call_user_func_array($this->funcMap[$method], $args); 91 | } 92 | throw new ViewException(__CLASS__ . '::' . $method . ' 方法不存在!'); 93 | } 94 | } -------------------------------------------------------------------------------- /doc/router.md: -------------------------------------------------------------------------------- 1 | # 路由 2 | 3 | 默认的路由地址是通过请求中的 `r` 参数来定位的,如 4 | 5 | ``` 6 | http://yourdomain/?r=user/list&page=2 7 | ``` 8 | 9 | 对应的控制器方法是 `App\Controller\UserController::listAction()`。通过web服务器的rewrite功能,可以将URL美化为 10 | 11 | ```php 12 | http://yourdomain/user/list?page=2 13 | ``` 14 | 15 | ### 开启URL美化 16 | 17 | Apache 服务器在网站根目录创建 .htaccess文件,内容为: 18 | 19 | ``` 20 | 21 | RewriteEngine on 22 | RewriteBase / 23 | RewriteCond %{REQUEST_FILENAME} !-f 24 | RewriteCond %{REQUEST_FILENAME} !-d 25 | RewriteRule ^.*$ index.php?$0 [L] 26 | 27 | ``` 28 | 29 | Nginx 服务器在 server {} 内加入: 30 | 31 | ``` 32 | location / { 33 | try_files $uri $uri/ /index.php?$args; 34 | } 35 | ``` 36 | 37 | 最后在项目配置文件 `app.php` 中,修改 router.pretty_url 为 `true`。 38 | 39 | ```php 40 | // app.php 41 | return [ 42 | ... 43 | //路由配置 44 | 'router' => [ 45 | 'pretty_url' => true, 46 | 'default_route' => 'home/index', //默认路由 47 | ], 48 | ... 49 | ] 50 | ``` 51 | 52 | default_route 表示直接输入项目地址访问时的默认路由地址。例如访问 http://yourdomain/ 时,对应的控制器方法是 `App\Controller\HomeController::indexAction()`。 53 | 54 | ### 自定义路由 55 | 56 | 配置文件 `route.php` 用来配置自定义的路由规则,你可以指定的URL规则映射到不同的控制器方法上。 57 | 58 | ```php 59 | return [ 60 | ['/welcome', 'home/index'], 61 | ['/login', 'home/login', 'POST'], 62 | '/v1' => [ 63 | ['/users', 'user/list'], 64 | ['/info/:id', 'user/info'], 65 | ], 66 | ]; 67 | ``` 68 | 69 | 上面配置将 http://yourdomain/welcome 映射到 app\Controller\HomeController::indexAction() 方法。路由配置项的格式为 70 | 71 | ```php 72 | ['URL规则', '路由地址', 'HTTP方法'] 73 | ``` 74 | 75 | URL规则:可以使用变量或者通配符,变量语法是`:var`,如: /user/:id,使用通配符如:/home/*。在控制器中可以使用 `$this->request->getAttribute('id')` 获取到URL规则中的变量值,使用通配符的可以用 `$this->request->getAttribute(0)` 获取,`0` 表示第一个值。 76 | 77 | 路由地址:根据控制器的类名转换而来,格式为: 类名/方法名,省略 Controller 和 Action,例如: Admin/User/UserList 表示 App\Controller\Admin\UserController::UserListAction(),你也可以使用全小写的方式 admin/user/user-list,不管用哪种,最终都统一转换为全小写的地址。 78 | 79 | HTTP方法:用来限制特定HTTP请求方式,只支持一个,如: GET、POST。 80 | 81 | 当多个路由地址使用同样的前缀时,可以使用组路由,例如上面例子中的: 82 | 83 | ``` 84 | '/v1' => [ 85 | ['/users', 'user/list'], 86 | ['/info/:id', 'user/info'], 87 | ], 88 | ``` 89 | 90 | 对应的访问地址为 http://yourdomian/v1/users 和 http://yourdomian/v1/info/123 。 -------------------------------------------------------------------------------- /src/Core/Logger/Handler/DbHandler.php: -------------------------------------------------------------------------------- 1 | 18 | * @package Core\Logger 19 | */ 20 | class DbHandler extends AbstractHandler 21 | { 22 | /** 23 | * @var PDO 24 | */ 25 | private $db; 26 | 27 | public function init() 28 | { 29 | if (empty($this->config['dsn'])) { 30 | throw new \RuntimeException('缺少配置项: dsn'); 31 | } 32 | if (empty($this->config['username'])) { 33 | throw new \RuntimeException('缺少配置项: username'); 34 | } 35 | if (empty($this->config['table'])) { 36 | throw new \RuntimeException('缺少配置项: table'); 37 | } 38 | if (!isset($this->config['timeout'])) { 39 | $this->config['timeout'] = 3; 40 | } 41 | if (!isset($this->config['password'])) { 42 | $this->config['password'] = ''; 43 | } 44 | } 45 | 46 | public function getDefaultFormatter() 47 | { 48 | return new ArrayFormatter(); 49 | } 50 | 51 | public function handleRecord(array $record) 52 | { 53 | $table = $this->config['table']; 54 | $row = $this->getFormatter()->format($record); 55 | $sql = "INSERT INTO {$table} (`datetime`, `channel`, `level`, `file`, `line`, `message`) VALUES (?, ?, ?, ?, ?, ?)"; 56 | $sth = $this->getDb()->prepare($sql); 57 | return $sth->execute([ 58 | $row['datetime'], 59 | $row['channel'], 60 | $row['level'], 61 | $row['file'], 62 | $row['line'], 63 | $row['message'], 64 | ]); 65 | } 66 | 67 | private function getDb() 68 | { 69 | if (!$this->db) { 70 | $this->db = new PDO($this->config['dsn'], $this->config['username'], $this->config['password'], [ 71 | PDO::ATTR_TIMEOUT => $this->config['timeout'], 72 | PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, 73 | ]); 74 | } 75 | return $this->db; 76 | } 77 | 78 | } 79 | -------------------------------------------------------------------------------- /doc/log.md: -------------------------------------------------------------------------------- 1 | # 日志 2 | 3 | ## 配置 4 | 5 | 日志在 `app.php` 配置文件中配置,如 6 | 7 | ```php 8 | ... 9 | 'logger' => [ 10 | 'default' => [ 11 | [ 12 | 'class' => \Core\Logger\Handler\ConsoleHandler::class, 13 | 'config' => [ 14 | 'level' => \Core\Logger\Logger::DEBUG, 15 | ], 16 | ], 17 | [ 18 | 'class' => \Core\Logger\Handler\FileHandler::class, 19 | 'config' => [ 20 | 'level' => \Core\Logger\Logger::WARN, 21 | 'formatter' => \Core\Logger\Formatter\JsonFormatter::class, 22 | 'save_path' => DATA_PATH . '/logs/', 23 | 'file_size' => 0, 24 | 'filename' => '{level}-{Y}{m}{d}.log', 25 | ], 26 | ] 27 | ], 28 | 'channel2' => [ 29 | [ 30 | 'class' => \Core\Logger\Handler\DbHandler::class, 31 | 'config' => [ 32 | 'level' => \Core\Logger\Logger::ERROR, 33 | 'dsn' => 'mysql:host=localhost;port=3306;dbname=test;charset=utf8', 34 | 'username' => 'root', 35 | 'password' => '', 36 | 'table' => 'sys_log', 37 | ], 38 | ] 39 | ], 40 | ], 41 | ... 42 | ``` 43 | 44 | 上面的日志配置了 `default`、`channel2` 两个通道,`default` 通道配置了2个日志处理器,分别是 ConsoleHandler 和 FileHandler,ConsoleHandler 用于在控制台输出日志内容,而 FileHandler 则是将日志写到磁盘文件中。每个日志处理器的 `config` 数组内容将会传给对应 handler 的构造方法,不同的 handler 所需的配置项会有一些差异,但有几个配置项是固定的: 45 | 46 | * level:日志的级别 47 | * formatter:格式化器,用于格式化日志内容的格式,如JSON格式的JsonFormatter。 48 | * date_format:日志的日期格式 49 | 50 | ## 使用 51 | 52 | 使用 App::logger() 获取对象,默认是返回 default 通道的日志对象,可以使用 App::logger('channel2') 获取 channel2 通道的日志对象。 53 | 54 | ```php 55 | App::logger()->debug('debug log...'); 56 | App::logger()->info("info log..."); 57 | App::logger()->warn("warn log..."); 58 | App::logger()->error("error log..."); 59 | App::logger()->fatal("fatal log..."); 60 | ``` 61 | 62 | 输出 63 | 64 | ``` 65 | 2017-09-22 14:29:47 [console] [D] [DemoCommand.php:11] debug log... 66 | 2017-09-22 14:29:47 [console] [I] [DemoCommand.php:12] info log... 67 | 2017-09-22 14:29:47 [console] [W] [DemoCommand.php:13] warn log... 68 | 2017-09-22 14:29:47 [console] [E] [DemoCommand.php:14] error log... 69 | 2017-09-22 14:29:47 [console] [F] [DemoCommand.php:15] fatal log... 70 | ``` -------------------------------------------------------------------------------- /src/Core/Web/Debug/Lib/Storage.php: -------------------------------------------------------------------------------- 1 | savePath = DATA_PATH . '/debug'; 15 | if (!is_dir($this->savePath)) { 16 | mkdir($this->savePath, 0755, true); 17 | } 18 | $this->idxFile = $this->savePath . '/index.dat'; 19 | } 20 | 21 | public function save(array $data) 22 | { 23 | $fileKey = uniqid(); 24 | 25 | $idx = $fileKey . '|' . $data['meta']['requestTime'] . '|' . $data['meta']['method'] . '|' . $data['meta']['execTime'] . 26 | '|' . $data['meta']['memoryUsage'] . '|' . $data['meta']['url']; 27 | file_put_contents($this->idxFile, $idx . "\n", FILE_APPEND | LOCK_EX); 28 | 29 | $data = json_encode($data); 30 | $filename = $this->savePath . '/' . $fileKey . '.dat'; 31 | file_put_contents($filename, $data); 32 | 33 | return $fileKey; 34 | } 35 | 36 | public function get($fileKey) 37 | { 38 | $filename = $this->savePath . '/' . $fileKey . '.dat'; 39 | if (!is_file($filename)) { 40 | return false; 41 | } 42 | $data = file_get_contents($filename); 43 | $data = json_decode($data, true); 44 | return $data; 45 | } 46 | 47 | public function getList($start, $size, &$total) 48 | { 49 | if (!is_file($this->idxFile)) { 50 | $total = 0; 51 | return []; 52 | } 53 | $lines = file($this->idxFile); 54 | $total = count($lines); 55 | $slice = array_slice($lines, $start, $size); 56 | $result = []; 57 | for ($i = count($slice) - 1; $i >= 0; $i --) { 58 | $line = $slice[$i]; 59 | list($id, $time, $method, $execTime, $memory, $url) = explode('|', $line); 60 | $result[] = [ 61 | 'id' => $id, 62 | 'time' => $time, 63 | 'method' => $method, 64 | 'exec_time' => $execTime, 65 | 'memory' => $memory, 66 | 'url' => $url, 67 | ]; 68 | } 69 | return $result; 70 | } 71 | 72 | public function clear() 73 | { 74 | Files::removeDir($this->savePath); 75 | } 76 | } -------------------------------------------------------------------------------- /src/Core/Command.php: -------------------------------------------------------------------------------- 1 | 13 | * @package Core 14 | */ 15 | abstract class Command 16 | { 17 | /** 18 | * 默认动作 19 | * @var string 20 | */ 21 | protected $defaultAction = 'index'; 22 | 23 | public final function __construct() 24 | { 25 | $this->init(); 26 | } 27 | 28 | public function init() 29 | { 30 | 31 | } 32 | 33 | /** 34 | * 提示消息 35 | * 36 | * @param string $message 提示消息 37 | * @param int $code 消息号 38 | */ 39 | public function message($message, $code = MSG_ERR) 40 | { 41 | if ($code == MSG_OK) { 42 | echo $message . "\n"; 43 | } else { 44 | echo Console::ansiFormat("错误提示:", Console::FG_RED) . $message . "\n"; 45 | exit(1); 46 | } 47 | } 48 | 49 | 50 | /** 51 | * 执行控制器方法 52 | * 53 | * @param string $actionName 方法名 54 | * @param array $params 参数列表 55 | * @return Response|mixed 56 | * @throws AppException 57 | */ 58 | public function execute($actionName, $params = []) 59 | { 60 | if (empty($actionName)) { 61 | $actionName = $this->defaultAction; 62 | } 63 | $actionName .= 'Action'; 64 | if (!method_exists($this, $actionName)) { 65 | throw new \BadMethodCallException("方法不存在: " . get_class($this) . "::{$actionName}"); 66 | } 67 | 68 | $method = new \ReflectionMethod($this, $actionName); 69 | if (!$method->isPublic()) { 70 | throw new \BadMethodCallException("调用非公有方法: " . get_class($this) . "::{$actionName}"); 71 | } 72 | 73 | $args = []; 74 | $methodParams = $method->getParameters(); 75 | if (!empty($methodParams)) { 76 | foreach ($methodParams as $k => $p) { 77 | $default = $p->isOptional() ? $p->getDefaultValue() : null; 78 | $value = array_key_exists($k, $params) ? $params[$k] : $default; 79 | if (null === $value && !$p->isOptional()) { 80 | throw new AppException('缺少请求参数:' . $p->getName()); 81 | } 82 | $args[] = $value; 83 | } 84 | } 85 | return $method->invokeArgs($this, $args); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/Core/Events.php: -------------------------------------------------------------------------------- 1 | 11 | * @package Core 12 | */ 13 | class Events 14 | { 15 | private static $events = []; 16 | 17 | public static function on($className, $name, $handler, $data = null, $append = true) 18 | { 19 | if (!isset(self::$events[$className])) { 20 | self::$events[$className] = []; 21 | } 22 | if ($append || empty(self::$events[$className][$name])) { 23 | self::$events[$className][$name][] = [$handler, $data]; 24 | } else { 25 | array_unshift(self::$events[$className][$name], [$handler, $data]); 26 | } 27 | return true; 28 | } 29 | 30 | public static function off($className, $name, $handler = null) 31 | { 32 | if (empty(self::$events[$className][$name])) { 33 | return false; 34 | } 35 | if (null === $handler) { 36 | unset(self::$events[$className][$name]); 37 | return true; 38 | } else { 39 | $removed = false; 40 | foreach (self::$events[$className][$name] as $key => $value) { 41 | if ($value[0] === $handler) { 42 | unset(self::$events[$className][$name][$key]); 43 | $removed = true; 44 | } 45 | } 46 | if ($removed) { 47 | self::$events[$className][$name] = array_values(self::$events[$className][$name]); 48 | } 49 | return $removed; 50 | } 51 | } 52 | 53 | public static function trigger($className, $name, Event $event = null) 54 | { 55 | if (!empty(self::$events[$className][$name])) { 56 | if ($event === null) { 57 | $event = new Event(); 58 | } 59 | $event->setName($name); 60 | if ($event->getSender() === null) { 61 | $event->setSender($className); 62 | } 63 | $event->setHandled(false); 64 | foreach (self::$events[$className][$name] as $handler) { 65 | $event->setData($handler[1]); 66 | call_user_func($handler[0], $event); 67 | if ($event->isHandled()) { 68 | return; 69 | } 70 | } 71 | } 72 | } 73 | } -------------------------------------------------------------------------------- /src/Core/View/ViewInterface.php: -------------------------------------------------------------------------------- 1 | 9 | * @package Core\View 10 | */ 11 | interface ViewInterface 12 | { 13 | 14 | /** 15 | * 获取设置项的值 16 | * 17 | * @param string $name 18 | * @return mixed 19 | */ 20 | public function getOption($name); 21 | 22 | /** 23 | * 设置配置项的值 24 | * 25 | * @param $name 26 | * @param $value 27 | * @return mixed 28 | */ 29 | public function setOption($name, $value); 30 | 31 | /** 32 | * 渲染模板 33 | * 34 | * @param $filename 35 | * @param $data 36 | * @return string 37 | */ 38 | public function render($filename, array $data); 39 | 40 | /** 41 | * 返回模板文件路径 42 | * 43 | * @param string $filename 44 | * @return mixed 45 | */ 46 | public function getViewFile($filename); 47 | 48 | /** 49 | * 设置布局文件 50 | * @param $filename 51 | * @return mixed 52 | */ 53 | public function setLayout($filename); 54 | 55 | /** 56 | * 设置子布局文件 57 | * @param $name 58 | * @param $filename 59 | * @return mixed 60 | */ 61 | public function setLayoutSection($name, $filename); 62 | 63 | /** 64 | * 重置 65 | * @return mixed 66 | */ 67 | public function reset(); 68 | 69 | /** 70 | * 注册CSS文件(在控制器中使用) 71 | * 72 | * @param $url 73 | * @param array $options 74 | * @return mixed 75 | */ 76 | public function registerCssFile($url, $options = []); 77 | 78 | /** 79 | * 注册JS文件(在控制器中使用) 80 | * 81 | * @param $url 82 | * @param array $options 83 | * @param bool $head 84 | * @return mixed 85 | */ 86 | public function registerJsFile($url, $options = [], $head = true); 87 | 88 | /** 89 | * 引入CSS文件(在模板中使用) 90 | * 91 | * @param $url 92 | * @param array $options 93 | * @return mixed 94 | */ 95 | public function requireCssFile($url, $options = []); 96 | 97 | /** 98 | * 引入JS文件(在模板中使用) 99 | * 100 | * @param $url 101 | * @param array $options 102 | * @return mixed 103 | */ 104 | public function requireJsFile($url, $options = []); 105 | 106 | /** 107 | * 返回资源的实际URL 108 | * 109 | * @param $url 110 | * @return mixed 111 | */ 112 | public function getAssetUrl($url); 113 | } -------------------------------------------------------------------------------- /src/Core/Lib/Validate.php: -------------------------------------------------------------------------------- 1 | 8 | * @package Core\Lib 9 | */ 10 | class Validate 11 | { 12 | 13 | static $rules = [ 14 | 'email' => '/^[\w\-\.]+@[\w\-\.]+(\.\w+)+$/', 15 | 'telephone' => '/^(86)?(\d{3,4}-)?(\d{7,8})$/', 16 | 'mobile' => '/^1\d{10}$/', 17 | 'zipcode' => '/^[1-9]\d{5}$/', 18 | 'qq' => '/^[1-9]\d{4,}$/', 19 | 'date' => '/^(\d{4})(-|\/)(\d{1,2})\2(\d{1,2})$/', 20 | 'datetime' => '/^(\d{4})(-|\/)(\d{1,2})\2(\d{1,2})\s(\d{1,2}):(\d{1,2}):(\d{1,2})$/', 21 | 'chinese' => '/^[\u4e00-\u9fa5]+$/', 22 | 'english' => '/^[A-Za-z]+$/', 23 | 'varname' => '/^[a-zA-Z][\w]{0,254}$/', //变量名,函数名,控制器名等 24 | 'integer' => '/^[\d]+$/', //整数验证 25 | ]; 26 | 27 | /** 28 | * 验证 29 | * 30 | * @param string $rule 验证规则,可以是现有规则名称,或者正则表达式 31 | * @param string $string 待验证的字符串 32 | * @return bool 33 | */ 34 | public static function valid($rule, $string) 35 | { 36 | if (method_exists(__CLASS__, $rule)) { 37 | return call_user_func([__CLASS__, $rule], $string); 38 | } elseif (isset(self::$rules[$rule])) { 39 | return strlen($string) == 0 || preg_match(self::$rules[$rule], (string)$string); 40 | } else { 41 | return strlen($string) == 0 || preg_match($rule, (string)$string); 42 | } 43 | } 44 | 45 | /** 46 | * 是否合法的用户名 47 | * 48 | * @param string $string 49 | * @return boolean 50 | */ 51 | public static function username($string) 52 | { 53 | if (empty($string)) return FALSE; 54 | $badchars = ["\\", '&', ' ', "'", '"', '/', '*', ',', '<', '>', "\r", "\t", "\n", '#', '$', '(', ')', '%', '@', '+', '?', ';', '^']; 55 | foreach ($badchars as $char) { 56 | if (strpos($string, $char) !== false) { 57 | return false; 58 | } 59 | } 60 | return true; 61 | } 62 | 63 | /** 64 | * 验证URL是否合法 65 | * 66 | * @param $string 67 | * @return mixed 68 | */ 69 | public static function url($string) 70 | { 71 | return filter_var($string, FILTER_VALIDATE_URL); 72 | } 73 | 74 | /** 75 | * 验证EMAIL是否合法 76 | * 77 | * @param $string 78 | * @return mixed 79 | */ 80 | public static function email($string) 81 | { 82 | return filter_var($string, FILTER_VALIDATE_EMAIL); 83 | } 84 | 85 | /** 86 | * 验证IP地址是否合法 87 | * 88 | * @param $string 89 | * @return mixed 90 | */ 91 | public static function ip($string) 92 | { 93 | return filter_var($string, FILTER_VALIDATE_IP); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/Core/Component.php: -------------------------------------------------------------------------------- 1 | 10 | * @package Core 11 | */ 12 | class Component 13 | { 14 | /** 15 | * 保存当前对象各个事件对应的处理器列表 16 | * @var array 17 | */ 18 | private $events = []; 19 | 20 | /** 21 | * 添加一个事件处理器到某个事件 22 | * 23 | * @param string $name 事件名称 24 | * @param callable $handler 事件处理器 25 | * @param null $data 附加数据 26 | * @param bool|true $append 追加到末尾 27 | * @return bool 28 | */ 29 | public function on($name, $handler, $data = null, $append = true) 30 | { 31 | if ($append || empty($this->events[$name])) { 32 | $this->events[$name][] = [$handler, $data]; 33 | } else { 34 | array_unshift($this->events[$name], [$handler, $data]); 35 | } 36 | return true; 37 | } 38 | 39 | /** 40 | * 移除事件处理器 41 | * 42 | * @param string $name 事件名称 43 | * @param null $handler 需要移除的事件处理器,默认移除所有 44 | * @return bool 45 | */ 46 | public function off($name, $handler = null) 47 | { 48 | if (empty($this->events[$name])) { 49 | return false; 50 | } 51 | if (null === $handler) { 52 | unset($this->events[$name]); 53 | return true; 54 | } else { 55 | $removed = false; 56 | foreach ($this->events[$name] as $key => $value) { 57 | if ($value[0] === $handler) { 58 | unset($this->events[$name][$key]); 59 | $removed = true; 60 | } 61 | } 62 | if ($removed) { 63 | $this->events[$name] = array_values($this->events[$name]); 64 | } 65 | return $removed; 66 | } 67 | } 68 | 69 | /** 70 | * 触发事件 71 | * 72 | * @param $name 73 | * @param Event|null $event 74 | */ 75 | public function trigger($name, Event $event = null) 76 | { 77 | if (!empty($this->events[$name])) { 78 | if ($event === null) { 79 | $event = new Event(); 80 | } 81 | $event->setName($name); 82 | if ($event->getSender() === null) { 83 | $event->setSender($this); 84 | } 85 | $event->setHandled(false); 86 | foreach ($this->events[$name] as $handler) { 87 | $event->setData($handler[1]); 88 | call_user_func($handler[0], $event); 89 | if ($event->isHandled()) { 90 | return; 91 | } 92 | } 93 | } 94 | Events::trigger(get_class($this), $name, $event); 95 | } 96 | } -------------------------------------------------------------------------------- /src/Core/Config.php: -------------------------------------------------------------------------------- 1 | 8 | * @package Core 9 | */ 10 | class Config 11 | { 12 | private $data = []; 13 | private $configPath = ''; 14 | private $env = ''; 15 | 16 | /** 17 | * 构造函数 18 | * 19 | * @param string $configPath 配置文件目录 20 | * @param string $env 运行环境 21 | */ 22 | public function __construct($configPath, $env) 23 | { 24 | if (is_string($configPath)) { 25 | $this->configPath = rtrim($configPath, DS); 26 | } 27 | if (is_string($env)) { 28 | $this->env = $env; 29 | } 30 | } 31 | 32 | /** 33 | * 加载配置文件 34 | * 35 | * 配置文件位于 $configPath 定义的目录下,支持主配置和各个环境的差异配置。 36 | * 主配置位于 $configPath 根目录,差异配置位于 $env 子目录。 37 | * 如果同时存在主配置和差异配置,将先加载主配置,然后加载差异配置覆盖主配置中相同的键。 38 | * 39 | * @param $file 40 | */ 41 | private function load($file) 42 | { 43 | $mainFile = $this->configPath . "/{$file}.php"; 44 | $diffFile = $this->configPath . "/{$this->env}/{$file}.php"; 45 | if (!is_file($mainFile) && !is_file($diffFile)) { 46 | die("配置文件不存在: {$file}"); 47 | } 48 | $config = []; 49 | if (is_file($mainFile)) { 50 | $config = include $mainFile; 51 | } 52 | if (is_file($diffFile)) { 53 | $diff = include $diffFile; 54 | if (is_array($diff) && !empty($diff)) { 55 | $config = array_merge($config, $diff); 56 | } 57 | } 58 | $this->data[$file] = $config; 59 | } 60 | 61 | /** 62 | * 获取配置信息 63 | * 64 | * @param string $file 配置文件名,不带扩展名 65 | * @param string $name 配置键名 66 | * @param null $default 默认值 67 | * @return mixed|null 68 | */ 69 | public function get($file, $name = '', $default = null) 70 | { 71 | if (!preg_match('/^[a-z0-9\_]+$/i', $file)) return null; 72 | if (!isset($this->data[$file])) { 73 | $this->load($file); 74 | } 75 | if (empty($name)) { 76 | return $this->data[$file]; 77 | } else { 78 | return isset($this->data[$file][$name]) ? $this->data[$file][$name] : $default; 79 | } 80 | } 81 | 82 | /** 83 | * 修改配置信息 84 | * 85 | * 用于在运行时动态修改配置信息,如: 86 | * 根据用户的系统语言动态加载不同的语言包 87 | * 88 | * @param string $file 配置文件名,不带扩展名 89 | * @param string $name 配置键名 90 | * @param mixed $value 值 91 | */ 92 | public function set($file, $name, $value) 93 | { 94 | if (!isset($this->data[$file])) { 95 | $this->load($file); 96 | } 97 | $this->data[$file][$name] = $value; 98 | } 99 | } -------------------------------------------------------------------------------- /tests/Lib/HttpClientTest.php: -------------------------------------------------------------------------------- 1 | setParams([ 11 | 'foo' => 'bar', 12 | ]); 13 | $result = $request->getJsonBody(); 14 | $this->assertTrue($result['args']['foo'] == 'bar'); 15 | } 16 | 17 | public function testSimplePost() 18 | { 19 | $request = new HttpClient('http://httpbin.org/post', HttpClient::HTTP_POST); 20 | $postData = ['username' => 'test']; 21 | $request->setParams($postData); 22 | $result = $request->getJsonBody(); 23 | $this->assertTrue($result['form']['username'] == 'test'); 24 | } 25 | 26 | public function testUploadFile() 27 | { 28 | $tmpFile = tempnam(sys_get_temp_dir(), 'test_'); 29 | $content = 'hello world'; 30 | file_put_contents($tmpFile, $content); 31 | $request = new HttpClient('http://httpbin.org/post', HttpClient::HTTP_POST); 32 | $request->setFile('file', $tmpFile); 33 | $result = $request->getJsonBody(); 34 | $this->assertArrayHasKey('file', $result['files']); 35 | $this->assertEquals($content, $result['files']['file']); 36 | unlink($tmpFile); 37 | } 38 | 39 | public function testCookie() 40 | { 41 | $request = new HttpClient('http://httpbin.org/cookies/set?k2=v2&k1=v1'); 42 | $cookies = $request->getCookies(); 43 | $this->assertTrue((isset($cookies['k1']) && $cookies['k1'] == 'v1')); 44 | $this->assertTrue((isset($cookies['k2']) && $cookies['k2'] == 'v2')); 45 | 46 | $request = new HttpClient('http://httpbin.org/cookies'); 47 | $request->setCookie('foo', 'bar'); 48 | $result = $request->getJsonBody(); 49 | $this->assertTrue($result['cookies']['foo'] == 'bar'); 50 | } 51 | 52 | public function testCookieJar() 53 | { 54 | $tmpFile = tempnam(sys_get_temp_dir(), 'test_'); 55 | $request = new HttpClient('http://httpbin.org/cookies/set?k2=v2&k1=v1'); 56 | $request->setCookieJar($tmpFile); 57 | $request->getResponse(); 58 | $request->reset(); 59 | $request->setUrl('http://httpbin.org/cookies'); 60 | $cookies = $request->getJsonBody()['cookies']; 61 | $this->assertArrayHasKey('k1', $cookies); 62 | $this->assertArrayHasKey('k2', $cookies); 63 | unlink($tmpFile); 64 | } 65 | 66 | public function testBaseAuth() 67 | { 68 | $request = new HttpClient('http://httpbin.org/basic-auth/user/passwd'); 69 | $request->setBasicAuth('user', 'passwd'); 70 | $this->assertTrue($request->getResponseCode() == 200); 71 | } 72 | } -------------------------------------------------------------------------------- /tests/Cache/TestCaseTrait.php: -------------------------------------------------------------------------------- 1 | cache->delete('foo'); 8 | $this->assertTrue($this->cache->add('foo', 123)); 9 | $this->assertFalse($this->cache->add('foo', 456)); 10 | $this->assertEquals(123, $this->cache->get('foo')); 11 | $this->cache->delete('foo'); 12 | } 13 | 14 | public function testSet() 15 | { 16 | $this->cache->delete('foo'); 17 | $this->cache->set('foo', 123); 18 | $this->assertEquals(123, $this->cache->get('foo')); 19 | 20 | $this->cache->set('foo', 456); 21 | $this->assertEquals(456, $this->cache->get('foo')); 22 | 23 | $arr1 = ['a' => 1, 'b' => 2, 'c' => 3]; 24 | $this->cache->set('arr1', $arr1); 25 | $this->assertEquals($arr1, $this->cache->get('arr1')); 26 | $this->cache->delete('arr1'); 27 | } 28 | 29 | public function testMulti() 30 | { 31 | $items = ['a' => 'hello', 'b' => 123, 'c' => ['k1' => 'v1', 'k2' => 'v2']]; 32 | $this->assertEquals(true, $this->cache->setMultiple($items)); 33 | $this->assertEquals($items, $this->cache->getMultiple(array_keys($items))); 34 | // 读取不存在的key 35 | $data = $this->cache->getMultiple(['a', 'b', 'd']); 36 | $this->assertEquals($items['a'], $data['a']); 37 | $this->assertEquals($items['b'], $data['b']); 38 | $this->assertEquals(null, $data['d']); 39 | } 40 | 41 | public function testGet() 42 | { 43 | $this->cache->delete('no_exist'); 44 | $this->assertNull($this->cache->get('no_exist')); 45 | } 46 | 47 | public function testDel() 48 | { 49 | $items = ['a' => 1, 'b' => 2, 'c' => 3]; 50 | // 第1种语法 51 | $this->cache->setMultiple($items); 52 | $this->assertEquals(count($items), $this->cache->deleteMultiple(array_keys($items))); 53 | // 第2种语法 54 | $this->cache->setMultiple($items); 55 | $this->assertEquals(count($items), $this->cache->delete('a', 'b', 'c')); 56 | } 57 | 58 | public function testIncrement() 59 | { 60 | $this->cache->delete('inc'); 61 | for ($i = 1; $i < 100; $i++) { 62 | $this->assertEquals($i, $this->cache->increment('inc')); 63 | } 64 | $this->cache->delete('inc'); 65 | for ($i = 2; $i < 100; $i += 2) { 66 | $this->assertEquals($i, $this->cache->increment('inc', 2)); 67 | } 68 | } 69 | 70 | public function testDecrement() 71 | { 72 | $this->cache->set('inc', 101); 73 | for ($i = 100; $i > -100; $i--) { 74 | $this->assertEquals($i, $i); 75 | } 76 | $this->cache->delete('inc'); 77 | for ($i = 2; $i < 100; $i += 2) { 78 | $this->assertEquals(0-$i, $this->cache->decrement('inc', 2)); 79 | } 80 | } 81 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 一个轻量级PHP框架 # 2 | 3 | 一个优雅、简洁、高效的PHP框架,用于快速开发扩展性强、可维护性强的PHP项目,零学习成本。 4 | 5 | ## 安装说明 6 | 7 | 只安装框架: 8 | 9 | ```bash 10 | $ composer require lisijie/framework 11 | ``` 12 | 13 | 创建一个带有基本应用程序模板的项目**(推荐)**: 14 | 15 | ```bash 16 | $ composer create-project lisijie/framework-app 17 | ``` 18 | 19 | ## 使用说明 20 | 21 | ### 一、目录结构 22 | 23 | 一个基本的应用目录结构如下: 24 | 25 | |- app 应用目录 26 | | |- Command 命令行脚本控制器(可选) 27 | | |- Config 配置文件 28 | | |- Controller Web控制器 29 | | |- Exception 自定义异常类型和异常处理器(可选) 30 | | |- Model 数据模型,提供数据的读写接口,一张表对应一个类(可选) 31 | | |- Service Service模块,封装业务逻辑,操作Model(可选) 32 | | |- View 视图模板文件 33 | |- data 运行时数据目录(日志、缓存文件等) 34 | |- public 发布目录,存放外部可访问的资源和index.php入口文件 35 | |- vendor composer第三方包目录 36 | 37 | 框架使用符合PSR规范的自动加载机制,可以在 `app` 目录下创建其他包。如工具包 `Util`,使用的命名空间为 `App\Util`。 38 | 39 | ### 二、配置文件 40 | 41 | 配置文件统一放置在 `app/Config` 目录下,其下又分了 development、testing、pre_release、production 子目录,分别用于开发环境、测试环境、预发布环境、生产环境的配置。放在 Config 根目录下的配置文件表示全局配置,如路由配置。放在环境目录下的为差异配置。加载方式是:首先读取全局配置,然后读取差异配置,然后将差异配置的配置项覆盖到全局配置。 42 | 43 | 配置的获取方式为: 44 | 45 | ```php 46 | \App::config()->get('app', 'varname', 'default'); 47 | ``` 48 | 49 | 其中 app 表示配置文件名,varname 表示配置项, default 表示当不存在该配置项时,使用它作为返回值。 50 | 51 | ### 三、命令行脚本 52 | 53 | 很多项目都会有在命令行模式下执行PHP脚本的需求,例如结合crontab做定时数据统计、数据清理等。在本框架中,命令行脚本控制器统一放在Command目录下,需要继承自 Core\Command,如果需要参数,则必须在方法中声明。示例代码: 54 | 55 | ```php 56 | stdout("hello, {$name}\n"); 66 | } 67 | } 68 | ``` 69 | 70 | 执行命令行脚本方法: 71 | 72 | 直接在终端使用以下命令执行 73 | 74 | ```bash 75 | php index.php 路由地址 参数1 参数2... 76 | ``` 77 | 78 | 例如以上代码的执行命令为 79 | 80 | ```Bash 81 | [demo@localhost public]$ php index.php demo/test world 82 | hello, world 83 | ``` 84 | 85 | 如果你需要定时执行以上命令,把它添加到crontab配置中即可。 86 | 87 | ### 四、控制器 88 | 89 | 控制器位于 `app\Controller` 目录下,可以在该目录下创建多个包,每个控制器类的名称以 `Controller` 为后缀。如:MainController.php。一个控制器类大致如下: 90 | 91 | ```php 92 | assign('foo', 'bar'); 106 | return $this->render(); 107 | } 108 | 109 | } 110 | ``` 111 | 112 | 每个动作方法必须加上 `Action` 后缀。对应的访问地址为:http://domain.com/?r=main/index。其中 `r` 参数为路由变量,路由地址是 `main/index`。路由地址为全小写形式,多个单词将被转换成`小写+减号`的形式,例如 UserInfoController::indexAction() 对应的路由地址为 `user-info/index`。为了让url看起来更加美观,建议在web服务器配置url rewrite。 113 | 114 | 115 | ### 五、服务器配置 116 | 117 | #### Nginx 118 | 119 | 要启用Url重写,请在Nginx网站配置中增加以下配置: 120 | 121 | location / { 122 | try_files $uri $uri/ /index.php?$args; 123 | } 124 | 125 | -------------------------------------------------------------------------------- /src/Core/Web/Debug/Controller/DebugController.php: -------------------------------------------------------------------------------- 1 | view = new Native([ 28 | 'template_dir' => dirname(__DIR__) . '/View', 29 | 'ext' => '.php', 30 | ]); 31 | $this->view->registerFunc('formatTime', function ($v) { 32 | return round($v / 1000, 1) . ' ms'; 33 | }); 34 | $this->view->registerFunc('formatSize', function ($v) { 35 | return round($v / 1024, 2) . ' KB'; 36 | }); 37 | $this->view->setLayout('layout'); 38 | 39 | $this->storage = new Storage(); 40 | } 41 | 42 | public function indexAction() 43 | { 44 | $page = $this->get('page', 1); 45 | $size = 20; 46 | 47 | $list = $this->storage->getList(($page - 1) * $size, $size, $total); 48 | $pager = new Pager($page, $size, $total); 49 | $data = [ 50 | 'status' => $this->getCookie(Config::$startCookieName) ? 'on' : 'off', 51 | 'list' => $list, 52 | 'pager' => $pager->makeHtml(), 53 | ]; 54 | return new Response(200, $this->view->render('index', $data)); 55 | } 56 | 57 | public function viewAction() 58 | { 59 | $traceId = $this->getQuery('id', $this->getCookie(Config::$traceCookieName)); 60 | 61 | $content = $this->storage->get($traceId); 62 | 63 | if (!$content) { 64 | return new Response(200, '调试日志不存在。'); 65 | } 66 | 67 | $profile = new Profile($content); 68 | 69 | $data = [ 70 | 'status' => $this->getCookie(Config::$startCookieName) ? 'on' : 'off', 71 | 'meta' => $profile->getMeta(), 72 | 'sqlLogs' => $profile->getSqlLogs(), 73 | 'profile' => $profile->sort('ewt', $profile->getProfile()), 74 | ]; 75 | 76 | return new Response(200, $this->view->render('view', $data)); 77 | } 78 | 79 | public function startAction() 80 | { 81 | $this->setCookie(new Cookie(Config::$startCookieName, 1)); 82 | return $this->goBack(); 83 | } 84 | 85 | public function stopAction() 86 | { 87 | $this->setCookie(new Cookie(Config::$startCookieName, 0, -1)); 88 | return $this->goBack(); 89 | } 90 | 91 | public function clearAction() 92 | { 93 | $this->storage->clear(); 94 | return $this->redirect(URL('debug/index')); 95 | } 96 | } -------------------------------------------------------------------------------- /src/Core/Logger/Handler/FileHandler.php: -------------------------------------------------------------------------------- 1 | 16 | * @package Core\Logger 17 | */ 18 | class FileHandler extends AbstractHandler 19 | { 20 | 21 | /** 22 | * 日志文件存放目录,默认: DATA_PATH/logs 23 | * @var string 24 | */ 25 | private $savePath; 26 | 27 | /** 28 | * 单个日志文件大小/MB,默认不限制 29 | * @var int 30 | */ 31 | private $fileSize = 0; 32 | 33 | /** 34 | * 日志文件格式 35 | * 36 | * 默认为每天一个文件,如要按日志级别和日期分文件,可配置为:{level}_{Y}{m}{d}.php 37 | * 38 | * 可用变量: 39 | * - {level} 日志级别 40 | * - {Y} 年份,如 2014 41 | * - {m} 数字表示的月份,有前导零,01 到 31 42 | * - {d} 月份中的第几天,有前导零的 2 位数字,01 到 31 43 | * - {H} 小时,24 小时格式,有前导零 44 | * 45 | * @var string 46 | */ 47 | private $fileName = '{Y}{m}{d}.log'; 48 | 49 | public function init() 50 | { 51 | $config = $this->config; 52 | if (empty($config['save_path'])) { 53 | $config['save_path'] = DATA_PATH . '/logs'; 54 | } 55 | $this->savePath = rtrim($config['save_path'], DIRECTORY_SEPARATOR); 56 | if (!is_dir($this->savePath) && !@mkdir($this->savePath, 0755, true)) { 57 | throw new \RuntimeException('Lib\Logger\Handler\FileHandler 日志目录创建失败: ' . $this->savePath); 58 | } 59 | if (isset($config['file_size']) && is_numeric($config['file_size'])) { 60 | $this->fileSize = max(1, intval($config['file_size'])) * 1024 * 1024; 61 | } 62 | if (isset($config['filename']) && $config['filename']) { 63 | $this->fileName = $config['filename']; 64 | } 65 | } 66 | 67 | public function getDefaultFormatter() 68 | { 69 | return new LineFormatter(); 70 | } 71 | 72 | public function handleRecord(array $record) 73 | { 74 | $message = $this->getFormatter()->format($record) . "\n"; 75 | 76 | $fileName = str_replace( 77 | ['{level}', '{Y}', '{m}', '{d}', '{H}'], 78 | [strtolower($record['level_name']), date('Y'), date('m'), date('d'), date('H')], 79 | $this->fileName); 80 | 81 | $fileName = $this->savePath . DIRECTORY_SEPARATOR . $fileName; 82 | 83 | if ($this->fileSize > 0 && is_file($fileName) && filesize($fileName) > $this->fileSize) { 84 | $info = pathinfo($fileName); 85 | $newName = $info['dirname'] . DIRECTORY_SEPARATOR . $info['filename'] . '_' . date('Ymd_His') . '.' . $info['extension']; 86 | rename($fileName, $newName); 87 | } 88 | 89 | if (!is_file($fileName) && substr($fileName, -4) == '.php') { 90 | $message = "\n{$message}"; 91 | } 92 | 93 | return error_log($message, 3, $fileName); 94 | } 95 | 96 | } 97 | -------------------------------------------------------------------------------- /src/Core/Cache/ArrayCache.php: -------------------------------------------------------------------------------- 1 | 10 | * @package Core\Cache 11 | */ 12 | class ArrayCache extends AbstractCache 13 | { 14 | private $data = []; 15 | private $expires = []; 16 | 17 | protected function init() 18 | { 19 | } 20 | 21 | public function getCount() 22 | { 23 | return count($this->data); 24 | } 25 | 26 | protected function doAdd($key, $value, $ttl = 0) 27 | { 28 | if ($this->doHas($key)) { 29 | return false; 30 | } 31 | $this->data[$key] = $value; 32 | if ($ttl) { 33 | $this->expires[$key] = time() + $ttl; 34 | } 35 | return true; 36 | } 37 | 38 | protected function doSetMultiple(array $values, $ttl = 0) 39 | { 40 | $expire = time() + $ttl; 41 | foreach ($values as $key => $value) { 42 | $this->data[$key] = $value; 43 | if ($ttl) { 44 | $this->expires[$key] = $expire; 45 | } 46 | } 47 | return true; 48 | } 49 | 50 | protected function doGetMultiple(array $keys, $default = null) 51 | { 52 | $data = []; 53 | foreach ($keys as $key) { 54 | $data[$key] = array_key_exists($key, $this->data) ? $this->data[$key] : $default; 55 | } 56 | return $data; 57 | } 58 | 59 | protected function doDeleteMultiple(array $keys) 60 | { 61 | foreach ($keys as $key) { 62 | unset($this->data[$key]); 63 | unset($this->expires[$key]); 64 | } 65 | return true; 66 | } 67 | 68 | protected function doIncrement($key, $step = 1) 69 | { 70 | if (!$this->doHas($key) || $this->isExpire($key)) { 71 | $this->data[$key] = $step; 72 | return $step; 73 | } else { 74 | $this->data[$key] = (int)$this->data[$key] + $step; 75 | return $this->data[$key]; 76 | } 77 | } 78 | 79 | protected function doDecrement($key, $step = 1) 80 | { 81 | if (!$this->doHas($key) || $this->isExpire($key)) { 82 | $this->data[$key] = 0 - $step; 83 | } else { 84 | $this->data[$key] = (int)$this->data[$key] - $step; 85 | return $this->data[$key]; 86 | } 87 | return $this->data[$key]; 88 | } 89 | 90 | protected function doClear() 91 | { 92 | $this->data = $this->expires = []; 93 | return true; 94 | } 95 | 96 | protected function doHas($key) 97 | { 98 | return array_key_exists($key, $this->data) && !$this->isExpire($key); 99 | } 100 | 101 | private function isExpire($key) 102 | { 103 | if (isset($this->expires[$key]) && $this->expires[$key] > time()) { 104 | return true; 105 | } 106 | return false; 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/Core/Cipher/Cipher.php: -------------------------------------------------------------------------------- 1 | 10 | * @package Core\Cipher 11 | */ 12 | class Cipher implements CipherInterface 13 | { 14 | 15 | const AES_128_CBC = 'AES-128-CBC'; 16 | const AES_256_CBC = 'AES-256-CBC'; 17 | 18 | // 向量长度 19 | private $ivLen; 20 | 21 | // 加密算法类型 22 | private $method; 23 | 24 | // 加密模式 25 | public $mode; 26 | 27 | // 是否对密文加上签名 28 | public $sign = true; 29 | 30 | /** 31 | * @param string $method 加密方式 32 | * @param bool $sign 是否加上签名 33 | */ 34 | public function __construct($method = self::AES_256_CBC, $sign = true) 35 | { 36 | $this->method = $method; 37 | $this->ivLen = openssl_cipher_iv_length($method); 38 | $this->sign = $sign; 39 | } 40 | 41 | /** 42 | * 创建用于简单文本加密的Cipher对象 43 | * 44 | * 适用于加密cookies、简短文本 45 | * 46 | * @return Cipher 47 | */ 48 | public static function createSimple() 49 | { 50 | return new self(self::AES_128_CBC, false); 51 | } 52 | 53 | /** 54 | * 加密 55 | * 56 | * @param string $text 要加密的内容 57 | * @param string $key 密钥 58 | * @param bool $raw 是否返回原始数据,true则返回原始二进制格式,false则返回base64编码后的字符串 59 | * @return string 60 | */ 61 | public function encrypt($text, $key, $raw = false) 62 | { 63 | $iv = openssl_random_pseudo_bytes($this->ivLen); 64 | $cipherText = openssl_encrypt($text, $this->method, $key, OPENSSL_RAW_DATA, $iv); 65 | $result = $iv . $cipherText; 66 | if ($this->sign) { 67 | $sign = hash_hmac('sha256', $result, $key, true); 68 | $result = $sign . $result; 69 | } 70 | if (!$raw) { 71 | $result = base64_encode($result); 72 | } 73 | return $result; 74 | } 75 | 76 | 77 | /** 78 | * 解密 79 | * 80 | * @param string $text 密文 81 | * @param string $key 密钥 82 | * @param bool $raw 密文是否为原始二进制格式 83 | * @return string 84 | */ 85 | public function decrypt($text, $key, $raw = false) 86 | { 87 | if (!$raw) { 88 | $text = base64_decode($text); 89 | } 90 | if ($this->sign) { 91 | $sign = substr($text, 0, 32); 92 | $text = substr($text, 32); 93 | $sign2 = hash_hmac('sha256', $text, $key, true); 94 | if (!$this->hashEquals($sign, $sign2)) { 95 | return false; 96 | } 97 | } 98 | $iv = substr($text, 0, $this->ivLen); 99 | $cipherRaw = substr($text, $this->ivLen); 100 | return openssl_decrypt($cipherRaw, $this->method, $key, OPENSSL_RAW_DATA, $iv); 101 | } 102 | 103 | /** 104 | * hash检查 105 | * 106 | * @param string $str1 107 | * @param string $str2 108 | * @return bool 109 | */ 110 | private function hashEquals($str1 , $str2) 111 | { 112 | if (function_exists('hash_equals')) { 113 | return hash_equals($str1, $str2); 114 | } 115 | return substr_count($str1 ^ $str2, "\0") * 2 === strlen($str1 . $str2); 116 | } 117 | 118 | } 119 | -------------------------------------------------------------------------------- /src/Core/Web/Debug/View/layout.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 性能分析工具 5 | 6 | 7 | 31 | 32 | 33 | 63 |
64 | content(); ?> 65 |
66 | 67 | 68 | 69 | 74 | 75 | -------------------------------------------------------------------------------- /src/Core/Cache/RedisCache.php: -------------------------------------------------------------------------------- 1 | 主机地址, 12 | * 'port' => 端口, 13 | * 'auth' => 密码, 14 | * 'timeout' => 超时时间, 15 | * 'options' => [], // redis设置选项 16 | * ] 17 | * 18 | * @author lisijie 19 | * @package Core\Cache 20 | */ 21 | class RedisCache extends AbstractCache 22 | { 23 | /** 24 | * @var \Redis 25 | */ 26 | protected $client; 27 | 28 | protected function init() 29 | { 30 | if (!class_exists('\\Redis')) { 31 | throw new CacheException("当前环境不支持Redis"); 32 | } 33 | $host = isset($this->config['host']) ? $this->config['host'] : '127.0.0.1'; 34 | $port = isset($this->config['port']) ? intval($this->config['port']) : 6379; 35 | $timeout = isset($this->config['timeout']) ? floatval($this->config['timeout']) : 0.0; 36 | 37 | $this->client = new \Redis(); 38 | $this->client->connect($host, $port, $timeout); 39 | if (isset($this->config['auth'])) { 40 | $this->client->auth($this->config['auth']); 41 | } 42 | if (!empty($this->config['options'])) { 43 | foreach ($this->config['options'] as $name => $val) { 44 | $this->client->setOption($name, $val); 45 | } 46 | } 47 | $this->client->setOption(\Redis::OPT_SERIALIZER, \Redis::SERIALIZER_PHP); 48 | } 49 | 50 | protected function doAdd($key, $value, $ttl = 0) 51 | { 52 | if ($this->client->setnx($key, $value)) { 53 | if ($ttl > 0) { 54 | $this->client->expire($key, $ttl); 55 | } 56 | return true; 57 | } 58 | return false; 59 | } 60 | 61 | protected function doSetMultiple(array $values, $ttl = 0) 62 | { 63 | if ($this->client->mset($values)) { 64 | if ($ttl > 0) { 65 | foreach ($values as $key => $val) { 66 | $this->client->expire($key, $ttl); 67 | } 68 | } 69 | return true; 70 | } 71 | return false; 72 | } 73 | 74 | protected function doGetMultiple(array $keys, $default = null) 75 | { 76 | $values = $this->client->getMultiple($keys); 77 | $result = []; 78 | foreach ($keys as $idx => $key) { 79 | if (false === $values[$idx]) { 80 | $result[$key] = $default; 81 | } else { 82 | $result[$key] = $values[$idx]; 83 | } 84 | } 85 | return $result; 86 | } 87 | 88 | protected function doDeleteMultiple(array $keys) 89 | { 90 | return $this->client->delete($keys) > 0; 91 | } 92 | 93 | protected function doClear() 94 | { 95 | if ($this->client instanceof \RedisCluster) { 96 | return false; 97 | } 98 | return $this->client->flushDB(); 99 | } 100 | 101 | protected function doHas($key) 102 | { 103 | return $this->client->exists($key); 104 | } 105 | 106 | protected function doIncrement($key, $step = 1) 107 | { 108 | return $this->client->incrBy($key, $step); 109 | } 110 | 111 | protected function doDecrement($key, $step = 1) 112 | { 113 | return $this->client->decrBy($key, $step); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/ClassLoader.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | class ClassLoader 14 | { 15 | 16 | protected $namespaces = []; 17 | protected $classes = []; 18 | protected $paths = []; 19 | private static $instance; 20 | 21 | /** 22 | * 获取类的单一实例 23 | * 24 | * @return static 25 | */ 26 | public static function getInstance() 27 | { 28 | if (!is_object(self::$instance)) { 29 | self::$instance = new static(); 30 | } 31 | 32 | return self::$instance; 33 | } 34 | 35 | /** 36 | * 注册命名空间 37 | * 38 | * @param string $namespace 命名空间前缀 39 | * @param array|string $paths 目录列表 40 | * @return ClassLoader 41 | */ 42 | public function registerNamespace($namespace, $paths) 43 | { 44 | $this->namespaces[$namespace] = (array)$paths; 45 | return $this; 46 | } 47 | 48 | /** 49 | * 获取已注册的命名空间对应目录 50 | * 51 | * @param string $namespace 命名空间前缀 52 | * @return array 53 | */ 54 | public function getNamespacePaths($namespace) 55 | { 56 | if (isset($this->namespaces[$namespace])) { 57 | return $this->namespaces[$namespace]; 58 | } 59 | return []; 60 | } 61 | 62 | /** 63 | * 注册单个类 64 | * 65 | * @param string $className 类名 66 | * @param string $path 类文件的完整路径 67 | * @return ClassLoader 68 | */ 69 | public function registerClass($className, $path) 70 | { 71 | $this->classes[$className] = (string)$path; 72 | return $this; 73 | } 74 | 75 | /** 76 | * 注册自动加载 77 | * 78 | * @param bool $prepend 是否优先 79 | */ 80 | public function register($prepend = false) 81 | { 82 | spl_autoload_register([$this, 'loadClass'], true, $prepend); 83 | } 84 | 85 | public function unregister() 86 | { 87 | spl_autoload_unregister([$this, 'loadClass']); 88 | } 89 | 90 | /** 91 | * 加载类 92 | * 93 | * @param $className 94 | * @return bool 95 | */ 96 | public function loadClass($className) 97 | { 98 | $className = preg_replace('/([^a-z0-9\_\\\\])/i', '', $className); 99 | if ($file = $this->findFile($className)) { 100 | include $file; 101 | return true; 102 | } 103 | } 104 | 105 | /** 106 | * 类文件查找 107 | * 108 | * @param $class 109 | * @return bool|string 110 | */ 111 | protected function findFile($class) 112 | { 113 | if ($class[0] == '\\') { 114 | $class = substr($class, 1); 115 | } 116 | if (isset($this->classes[$class]) && is_file($this->classes[$class])) { 117 | return $this->classes[$class]; 118 | } 119 | 120 | if (false !== strrpos($class, '\\')) { 121 | foreach ($this->namespaces as $ns => $paths) { 122 | if (0 === strpos($class, $ns . '\\')) { 123 | foreach ($paths as $path) { 124 | $file = $path . DIRECTORY_SEPARATOR . str_replace('\\', DIRECTORY_SEPARATOR, substr($class, strlen($ns . '\\'))) . '.php'; 125 | if (is_file($file)) { 126 | return $file; 127 | } 128 | } 129 | } 130 | } 131 | } 132 | return false; 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/Core/Http/Cookie.php: -------------------------------------------------------------------------------- 1 | 8 | * @package Core\Http 9 | */ 10 | class Cookie 11 | { 12 | /** 13 | * @var string 14 | */ 15 | private $name; 16 | 17 | /** 18 | * @var string 19 | */ 20 | private $value; 21 | 22 | /** 23 | * @var int 24 | */ 25 | private $expire = 0; 26 | 27 | /** 28 | * @var string 29 | */ 30 | private $domain = ''; 31 | 32 | /** 33 | * @var string 34 | */ 35 | private $path = ''; 36 | 37 | /** 38 | * @var bool 39 | */ 40 | private $secure = false; 41 | 42 | /** 43 | * @var bool 44 | */ 45 | private $httpOnly = true; 46 | 47 | public function __construct($name, $value, $expire = 0, $domain = '', $path = '/', $secure = false, $httpOnly = true) 48 | { 49 | $name = (string)$name; 50 | if (preg_match('/[=,; \t\r\n\013\014]+/', $name)) { 51 | throw new \InvalidArgumentException('Cookie names cannot contain any of the following \'=,; \t\r\n\013\014\''); 52 | } 53 | $this->name = $name; 54 | $this->value = (string)$value; 55 | $this->expire = (int)$expire; 56 | $this->domain = $domain; 57 | $this->path = $path; 58 | $this->secure = $secure; 59 | $this->httpOnly = $httpOnly; 60 | } 61 | 62 | /** 63 | * @return string 64 | */ 65 | public function getValue() 66 | { 67 | return $this->value; 68 | } 69 | 70 | /** 71 | * @param string $value 72 | */ 73 | public function setValue($value) 74 | { 75 | $this->value = (string)$value; 76 | } 77 | 78 | /** 79 | * @param int $expire 80 | */ 81 | public function setExpire($expire) 82 | { 83 | $this->expire = (int)$expire; 84 | } 85 | 86 | /** 87 | * @param string $domain 88 | */ 89 | public function setDomain($domain) 90 | { 91 | $this->domain = $domain; 92 | } 93 | 94 | /** 95 | * @param string $path 96 | */ 97 | public function setPath($path) 98 | { 99 | $this->path = $path; 100 | } 101 | 102 | /** 103 | * @param bool $secure 104 | */ 105 | public function setSecure($secure) 106 | { 107 | $this->secure = $secure; 108 | } 109 | 110 | /** 111 | * @param bool $httpOnly 112 | */ 113 | public function setHttpOnly($httpOnly) 114 | { 115 | $this->httpOnly = $httpOnly; 116 | } 117 | 118 | /** 119 | * 格式化 120 | * 121 | * @return string 122 | */ 123 | public function toHeader() 124 | { 125 | $values = []; 126 | if ($this->expire !== 0) { 127 | $values[] = '; expires=' . gmdate('D, d-M-Y H:i:s e', $this->expire); 128 | } 129 | if ($this->path !== '') { 130 | $values[] = '; path=' . $this->path; 131 | } 132 | if ($this->domain != '') { 133 | $values[] = '; domain=' . $this->domain; 134 | } 135 | if ($this->secure) { 136 | $values[] = '; secure'; 137 | } 138 | if ($this->httpOnly) { 139 | $values[] = '; HttpOnly'; 140 | } 141 | return sprintf("%s=%s", $this->name, urlencode($this->value) . implode('', $values)); 142 | } 143 | 144 | public function __toString() 145 | { 146 | return sprintf("Set-Cookie: %s", $this->toHeader()); 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/Core/Cache/MemCache.php: -------------------------------------------------------------------------------- 1 | [ 10 | * ['192.168.1.1', 11211], //memcached服务器1 11 | * ['192.168.1.2', 11211], //memcached服务器2 12 | * ], 13 | * ] 14 | * 15 | * @author lisijie 16 | * @package Core\Cache 17 | */ 18 | class MemCache extends AbstractCache 19 | { 20 | /** 21 | * @var \Memcached 22 | */ 23 | private $client; 24 | 25 | public function init() 26 | { 27 | if (empty($this->config['servers'])) { 28 | throw new CacheException(__CLASS__ . " 缺少参数: servers"); 29 | } 30 | if (!class_exists('\\Memcached')) { 31 | throw new CacheException("当前环境不支持: memcached"); 32 | } 33 | $this->client = new \Memcached(); 34 | $this->client->addServers($this->config['servers']); 35 | if (!isset($this->config['options'])) { 36 | $this->config['options'] = []; 37 | } 38 | $this->config['options'][\Memcached::OPT_DISTRIBUTION] = \Memcached::DISTRIBUTION_CONSISTENT; 39 | $this->client->setOptions($this->config['options']); 40 | } 41 | 42 | protected function doSetMultiple(array $values, $ttl = 0) 43 | { 44 | return $this->checkStatusCode($this->client->setMulti($values, $ttl)); 45 | } 46 | 47 | protected function doGetMultiple(array $keys, $default = null) 48 | { 49 | return $this->checkStatusCode($this->client->getMulti($keys)); 50 | } 51 | 52 | protected function doDeleteMultiple(array $keys) 53 | { 54 | $ok = true; 55 | $result = $this->client->deleteMulti($keys); 56 | foreach ($this->checkStatusCode($result) as $code) { 57 | if (true !== $code && $code !== \Memcached::RES_SUCCESS && $code !== \Memcached::RES_NOTFOUND) { 58 | $ok = false; 59 | } 60 | } 61 | return $ok; 62 | } 63 | 64 | protected function doClear() 65 | { 66 | return $this->client->flush(); 67 | } 68 | 69 | protected function doHas($key) 70 | { 71 | if (false !== $this->client->get($key)) { 72 | return true; 73 | } 74 | if ($this->client->getResultCode() == \Memcached::RES_NOTFOUND) { 75 | return false; 76 | } elseif ($this->client->getResultCode() == \Memcached::RES_SUCCESS) { 77 | return true; 78 | } else { 79 | throw new CacheException('memcached client error: ' . $this->client->getResultMessage()); 80 | } 81 | } 82 | 83 | protected function doIncrement($key, $step = 1) 84 | { 85 | if ($this->client->add($key, $step)) { 86 | return $step; 87 | } 88 | return $this->client->increment($key, $step); 89 | } 90 | 91 | protected function doDecrement($key, $step = 1) 92 | { 93 | if ($this->client->add($key, 0 - $step)) { 94 | return 0 - $step; 95 | } 96 | return $this->client->decrement($key, $step); 97 | } 98 | 99 | protected function doAdd($key, $value, $ttl = 0) 100 | { 101 | $ok = $this->client->add($key, $value, $ttl); 102 | if (false === $ok && $this->client->getResultCode() != \Memcached::RES_NOTSTORED) { 103 | throw new CacheException('memcached client error: ' . $this->client->getResultMessage()); 104 | } 105 | return $ok; 106 | } 107 | 108 | /** 109 | * 检查Memcached的状态码 110 | * 111 | * 如果状态码不是成功或key不存在的状态,可能服务器有问题,抛出异常。 112 | * 113 | * @param $result 114 | * @return mixed 115 | * @throws CacheException 116 | */ 117 | private function checkStatusCode($result) 118 | { 119 | $code = $this->client->getResultCode(); 120 | if ($code == \Memcached::RES_SUCCESS || $code == \Memcached::RES_NOTFOUND) { 121 | return $result; 122 | } 123 | throw new CacheException('memcached client error: ' . $this->client->getResultMessage()); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/Core/Http/Message.php: -------------------------------------------------------------------------------- 1 | 11 | * @package Core\Http 12 | */ 13 | abstract class Message implements MessageInterface 14 | { 15 | 16 | /** 17 | * @var string 18 | */ 19 | protected $protocol = '1.1'; 20 | 21 | /** 22 | * @var Headers 23 | */ 24 | protected $headers; 25 | 26 | /** 27 | * @var StreamInterface 28 | */ 29 | protected $body; 30 | 31 | /** 32 | * 返回HTTP协议版本 33 | * 34 | * @return string 35 | */ 36 | public function getProtocolVersion() 37 | { 38 | return $this->protocol; 39 | } 40 | 41 | /** 42 | * 返回带有指定版本号的新对象 43 | * 44 | * @param string $version HTTP protocol version 45 | * @return static 46 | */ 47 | public function withProtocolVersion($version) 48 | { 49 | if ($version === $this->protocol) { 50 | return $this; 51 | } 52 | $obj = clone $this; 53 | $obj->protocol = (string)$version; 54 | return $obj; 55 | } 56 | 57 | /** 58 | * 返回所有header信息 59 | * 60 | * @return string[][] 61 | */ 62 | public function getHeaders() 63 | { 64 | return $this->headers->all(); 65 | } 66 | 67 | /** 68 | * 检查指定header是否存在 69 | * 70 | * @param string $name 大小写不敏感的名称 71 | * @return bool 72 | */ 73 | public function hasHeader($name) 74 | { 75 | return $this->headers->has($name); 76 | } 77 | 78 | /** 79 | * 返回指定header信息 80 | * 81 | * @param string $name 大小写不敏感的header字段名 82 | * @return string[] 83 | */ 84 | public function getHeader($name) 85 | { 86 | return $this->headers->get($name); 87 | } 88 | 89 | /** 90 | * 返回值为逗号分隔的字符串的header信息 91 | * 92 | * @param string $name 大小写不敏感的header字段名 93 | * @return string 如果header没有设置,将返回空字符串 94 | */ 95 | public function getHeaderLine($name) 96 | { 97 | return implode(', ', $this->headers->get($name)); 98 | } 99 | 100 | /** 101 | * 返回包含指定header的新实例 102 | * 103 | * @param string $name 大小写不敏感的header字段名 104 | * @param string|string[] $value header值 105 | * @return static 106 | * @throws \InvalidArgumentException 名称或值无效时抛出 107 | */ 108 | public function withHeader($name, $value) 109 | { 110 | $obj = clone $this; 111 | $obj->headers->set($name, $value); 112 | return $obj; 113 | } 114 | 115 | /** 116 | * 返回包含指定header附加值的新实例 117 | * 118 | * @param string $name 大小写不敏感的header字段名 119 | * @param string|string[] $value 要附加的header值 120 | * @return static 121 | * @throws \InvalidArgumentException 名称或值无效时抛出 122 | */ 123 | public function withAddedHeader($name, $value) 124 | { 125 | $obj = clone $this; 126 | $obj->headers->add($name, $value); 127 | return $obj; 128 | } 129 | 130 | /** 131 | * 返回去掉指定header的新实例 132 | * 133 | * @param string $name 大小写不敏感的header字段名 134 | * @return static 135 | */ 136 | public function withoutHeader($name) 137 | { 138 | $obj = clone $this; 139 | $obj->headers->remove($name); 140 | return $obj; 141 | } 142 | 143 | /** 144 | * 返回消息体的Stream对象 145 | * 146 | * @return StreamInterface 147 | */ 148 | public function getBody() 149 | { 150 | // TODO body未设置时创建默认值 151 | return $this->body; 152 | } 153 | 154 | /** 155 | * 返回指定消息体的新实例 156 | * 157 | * @param StreamInterface $body 消息体 158 | * @return static 159 | * @throws \InvalidArgumentException body无效时 160 | */ 161 | public function withBody(StreamInterface $body) 162 | { 163 | if ($body === $this->body) { 164 | return $this; 165 | } 166 | $obj = clone $this; 167 | $obj->body = $body; 168 | return $obj; 169 | } 170 | 171 | /** 172 | * 克隆时需要将headers对象一并克隆 173 | */ 174 | public function __clone() 175 | { 176 | $this->headers = clone $this->headers; 177 | } 178 | } -------------------------------------------------------------------------------- /src/Core/Web/Debug/Middleware/DebuggerMiddleware.php: -------------------------------------------------------------------------------- 1 | profileExtension = 'xhprof'; 25 | xhprof_enable(XHPROF_FLAGS_CPU | XHPROF_FLAGS_MEMORY | XHPROF_FLAGS_NO_BUILTINS); 26 | } elseif (extension_loaded('tideways_xhprof')) { 27 | $this->profileExtension = 'tideways_xhprof'; 28 | tideways_xhprof_enable(TIDEWAYS_XHPROF_FLAGS_MEMORY | TIDEWAYS_XHPROF_FLAGS_MEMORY_MU | TIDEWAYS_XHPROF_FLAGS_MEMORY_PMU | TIDEWAYS_XHPROF_FLAGS_CPU); 29 | } 30 | Events::on(Db::class, Db::EVENT_QUERY, function (DbEvent $event) { 31 | $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 10); 32 | $service = $model = $controller = ''; 33 | foreach ($backtrace as $item) { 34 | if (isset($item['file'])) { 35 | if (strpos($item['file'], 'lisijie/framework') !== false) { 36 | continue; 37 | } 38 | if (!$service && substr($item['file'], -11) == 'Service.php') { 39 | $service = basename($item['file']) . ":{$item['line']}"; 40 | } elseif (!$model && substr($item['file'], -9) == 'Model.php') { 41 | $model = basename($item['file']) . ":{$item['line']}"; 42 | } elseif (!$controller && substr($item['file'], -14) == 'Controller.php') { 43 | $controller = basename($item['file']) . ":{$item['line']}"; 44 | } 45 | } 46 | } 47 | $this->sqlLogs[] = [ 48 | 'service' => $service, 49 | 'model' => $model, 50 | 'controller' => $controller, 51 | 'time' => $event->getTime() * 1000000, // 微秒 52 | 'sql' => $event->getSql(), 53 | 'params' => $event->getParams(), 54 | ]; 55 | }); 56 | } 57 | 58 | /** 59 | * 处理请求 60 | * 61 | * @param Request $request 62 | * @param callable $next 63 | * @return Response 64 | */ 65 | public function process(Request $request, callable $next) 66 | { 67 | 68 | if (substr(CUR_ROUTE, 0, 6) == 'debug/' 69 | || (!$request->getCookieParam(Config::$startCookieName) && !$request->getQueryParam(Config::$debugParamName)) 70 | ) { 71 | return $next(); 72 | } 73 | 74 | $response = $next(); 75 | 76 | $meta = [ 77 | 'route' => CUR_ROUTE, 78 | 'url' => (string)$request->getUri()->getPath(), 79 | 'requestTime' => $request->getServerParam('REQUEST_TIME'), 80 | 'method' => $request->getMethod(), 81 | 'responseHeaders' => $response->getHeaders(), 82 | 'get' => $_GET, 83 | 'post' => $_POST, 84 | 'files' => $_FILES, 85 | 'cookies' => $_COOKIE, 86 | 'server' => $_SERVER, 87 | 'startTime' => START_TIME, 88 | 'execTime' => round((microtime(true) - START_TIME) * 1000000), // 微秒 89 | 'memoryUsage' => memory_get_peak_usage(true), 90 | 'sqlLogs' => $this->sqlLogs, 91 | ]; 92 | 93 | $profile = []; 94 | if ($this->profileExtension == 'xhprof') { 95 | $profile = xhprof_disable(); 96 | } elseif ($this->profileExtension == 'tideways_xhprof') { 97 | $profile = tideways_xhprof_disable(); 98 | } 99 | 100 | $data = [ 101 | 'meta' => $meta, 102 | 'sql' => $this->sqlLogs, 103 | 'profile' => $profile, 104 | ]; 105 | 106 | $fileKey = (new Storage())->save($data); 107 | 108 | $response = $response->withCookie(new Cookie(Config::$traceCookieName, $fileKey)); 109 | return $response; 110 | } 111 | } -------------------------------------------------------------------------------- /src/Core/Router/AbstractRouter.php: -------------------------------------------------------------------------------- 1 | 8 | * @package Core\Router 9 | */ 10 | class AbstractRouter 11 | { 12 | /** 13 | * 当前路由参数 14 | * 15 | * @var array 16 | */ 17 | protected $params = []; 18 | 19 | /** 20 | * 当前路由地址 21 | * 22 | * @var string 23 | */ 24 | protected $routeName; 25 | 26 | /** 27 | * 默认路由 28 | * 29 | * @var string 30 | */ 31 | protected $defaultRoute = ''; 32 | 33 | 34 | /** 35 | * 查找的命名空间 36 | * 37 | * @var array 38 | */ 39 | protected $findNamespaces = []; 40 | 41 | /** 42 | * 设置默认路由 43 | * @param $route 44 | */ 45 | public function setDefaultRoute($route) 46 | { 47 | $this->defaultRoute = $route; 48 | } 49 | 50 | /** 51 | * 返回默认路由 52 | * @return string 53 | */ 54 | public function getDefaultRoute() 55 | { 56 | return $this->defaultRoute; 57 | } 58 | 59 | /** 60 | * 获取路由地址 61 | * @return string 62 | */ 63 | public function getRoute() 64 | { 65 | return $this->routeName ?: $this->defaultRoute; 66 | } 67 | 68 | /** 69 | * 获取路由参数列表 70 | * @return array 71 | */ 72 | public function getParams() 73 | { 74 | return $this->params; 75 | } 76 | 77 | /** 78 | * 标准化路由地址 79 | * 80 | * 全部转成小写,每个单词用"-"分隔,例如 Admin/UserList 转换为 admin/user-list 81 | * 82 | * @param $route 83 | * @return string 84 | */ 85 | public function normalizeRoute($route) 86 | { 87 | $route = preg_replace_callback('#[A-Z]#', function ($m) { 88 | return '-' . strtolower($m[0]); 89 | }, $route); 90 | return ltrim(strtr($route, ['/-' => '/']), '-'); 91 | } 92 | 93 | /** 94 | * 注册查找命名空间前缀 95 | * 96 | * @param string $namespace 命名空间前缀 97 | * @param string $classSuffix 类名后缀 98 | */ 99 | public function registerNamespace($namespace, $classSuffix) 100 | { 101 | $this->findNamespaces[] = [$namespace, $classSuffix]; 102 | } 103 | 104 | /** 105 | * 返回查找命名空间 106 | * 107 | * @return array 108 | */ 109 | public function getNamespaces() 110 | { 111 | return $this->findNamespaces; 112 | } 113 | 114 | /** 115 | * 解析路由地址返回控制器名和方法名 116 | * 117 | * 路由地址由英文字母、斜杠和减号组成,如:/foo/bar/say-hello。 118 | * 解析步骤如下: 119 | * 1. 首先将路由地址转换为 Foo\Bar\SayHello。 120 | * 2. 则将路由地址分割为两部分,Foo\Bar 为控制器名,SayHello 为方法名。检查是否存在 121 | * 名为 Foo\BarController 的控制器,如果存在,则解析成功。 122 | * 3. 如果控制器不存在,检查是否存在名为 Foo\Bar\SayHelloController 的控制器是否存在,存在则解析完成。 123 | * 4. 如果控制器不存在,则返回的控制器名称为空。 124 | * 125 | * @return array 返回 [控制器名称, 方法名] 126 | */ 127 | protected function resolveHandler() 128 | { 129 | $route = $this->getRoute(); 130 | $pos = strrpos($route, '/'); 131 | if ($pos !== false) { 132 | // 转换成首字母大写+反斜杠形式 133 | $route = str_replace(' ', '\\', ucwords(str_replace('/', ' ', $route))); 134 | } else { 135 | $route = ucfirst($route); 136 | } 137 | // 将减号分割的单词转换为首字母大写的驼峰形式 138 | if (strpos($route, '-') !== false && strpos($route, '--') === false) { 139 | $route = str_replace(' ', '', ucwords(str_replace('-', ' ', $route))); 140 | } 141 | $namespaces = $this->findNamespaces; 142 | $controllerName = $actionName = ''; 143 | if ($pos > 0) { 144 | $pos = strrpos($route, '\\'); 145 | $tmpControl = substr($route, 0, $pos); 146 | foreach ($namespaces as $item) { 147 | list($nsPrefix, $classSuffix) = $item; 148 | if (($class = "{$nsPrefix}\\{$route}{$classSuffix}") && class_exists($class)) { 149 | $controllerName = $class; 150 | break; 151 | } elseif (($class = "{$nsPrefix}\\{$tmpControl}{$classSuffix}") && class_exists($class)) { 152 | $controllerName = $class; 153 | $actionName = lcfirst(substr($route, $pos + 1)); 154 | break; 155 | } 156 | } 157 | } 158 | if (!$controllerName) { 159 | foreach ($namespaces as $item) { 160 | list($nsPrefix, $classSuffix) = $item; 161 | $class = "{$nsPrefix}\\{$route}{$classSuffix}"; 162 | if (class_exists($class)) { 163 | $controllerName = $class; 164 | break; 165 | } 166 | } 167 | } 168 | 169 | return [$controllerName, $actionName]; 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /src/Core/Http/Headers.php: -------------------------------------------------------------------------------- 1 | 8 | * @package Core\Http 9 | */ 10 | class Headers 11 | { 12 | /** 13 | * @var array 14 | */ 15 | protected $headers = []; 16 | 17 | /** 18 | * @var array 19 | */ 20 | protected $headerNames = []; 21 | 22 | /** 23 | * @var array 24 | */ 25 | protected static $specials = [ 26 | 'CONTENT_TYPE', 27 | 'CONTENT_LENGTH', 28 | 'PHP_AUTH_USER', 29 | 'PHP_AUTH_PW', 30 | 'PHP_AUTH_DIGEST', 31 | 'AUTH_TYPE' 32 | ]; 33 | 34 | public function __construct(array $headers = []) 35 | { 36 | foreach ($headers as $name => $value) { 37 | $this->set($name, $value); 38 | } 39 | } 40 | 41 | /** 42 | * 根据请求的环境变量创建对象 43 | * 44 | * @return Headers 45 | */ 46 | public static function createFromGlobals() 47 | { 48 | $header = new self(); 49 | foreach ($_SERVER as $key => $value) { 50 | if (strpos($key, 'X_') === 0 || strpos($key, 'HTTP_') === 0 || in_array($key, self::$specials)) { 51 | $key = strtolower($key); 52 | $key = str_replace(['-', '_'], ' ', $key); 53 | $key = preg_replace('#^http #', '', $key); 54 | $key = ucwords($key); 55 | $key = str_replace(' ', '-', $key); 56 | $header->set($key, $value); 57 | } 58 | } 59 | return $header; 60 | } 61 | 62 | /** 63 | * 设置header 64 | * 65 | * @param string $name 大小写不敏感的header字段名 66 | * @param string|string[] $value header值 67 | * @return static 68 | * @throws \InvalidArgumentException 名称或值无效时抛出 69 | */ 70 | public function set($name, $value) 71 | { 72 | if (empty($name)) { 73 | throw new \InvalidArgumentException('Header name cannot be empty'); 74 | } 75 | if (!is_array($value)) { 76 | $value = [$value]; 77 | } 78 | $normalized = strtolower($name); 79 | if (isset($this->headerNames[$normalized])) { 80 | unset($this->headers[$this->headerNames[$normalized]]); 81 | } 82 | $this->headers[$name] = $value; 83 | $this->headerNames[$normalized] = $name; 84 | return $this; 85 | } 86 | 87 | /** 88 | * 给指定header附加新值 89 | * 90 | * @param string $name 大小写不敏感的header字段名 91 | * @param string|string[] $value 要附加的header值 92 | * @return static 93 | * @throws \InvalidArgumentException 名称或值无效时抛出 94 | */ 95 | public function add($name, $value) 96 | { 97 | $normalized = strtolower($name); 98 | if (!is_array($value)) { 99 | $value = [$value]; 100 | } 101 | if (!isset($this->headerNames[$normalized])) { 102 | $this->headerNames[$normalized] = $name; 103 | $this->headers[$name] = []; 104 | } 105 | foreach ($value as $item) { 106 | $this->headers[$name][] = (string)$item; 107 | } 108 | return $this; 109 | } 110 | 111 | /** 112 | * 返回指定header信息 113 | * 114 | * @param string $name 大小写不敏感的header字段名 115 | * @return string[] 116 | */ 117 | public function get($name) 118 | { 119 | $name = strtolower($name); 120 | if (!isset($this->headerNames[$name])) { 121 | return []; 122 | } 123 | return $this->headers[$this->headerNames[$name]]; 124 | } 125 | 126 | /** 127 | * 移除指定header 128 | * 129 | * @param string $name 大小写不敏感的header字段名 130 | * @return static 131 | */ 132 | public function remove($name) 133 | { 134 | $normalized = strtolower($name); 135 | if (!isset($this->headerNames[$normalized])) { 136 | return $this; 137 | } 138 | unset($this->headers[$this->headerNames[$normalized]]); 139 | unset($this->headerNames[$normalized]); 140 | return $this; 141 | } 142 | 143 | /** 144 | * 返回所有header信息 145 | * 146 | * @return string[][] 147 | */ 148 | public function all() 149 | { 150 | return $this->headers; 151 | } 152 | 153 | /** 154 | * 检查指定header是否存在 155 | * 156 | * @param string $name 大小写不敏感的名称 157 | * @return bool 158 | */ 159 | public function has($name) 160 | { 161 | return isset($this->headerNames[strtolower($name)]); 162 | } 163 | 164 | public function __toString() 165 | { 166 | $str = ''; 167 | foreach ($this->headers as $key => $value) { 168 | $str .= "{$key}: {$value}\n"; 169 | } 170 | return $str; 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /src/Core/Lib/Pager.php: -------------------------------------------------------------------------------- 1 | 8 | * @package Core\Lib 9 | */ 10 | class Pager 11 | { 12 | // 配置信息 13 | private $config = []; 14 | 15 | // 输出样式模版 16 | private $templates = [ 17 | 'prev_page' => '
  • «
  • ', 18 | 'prev_page_disabled' => '
  • «
  • ', 19 | 'next_page' => '
  • »
  • ', 20 | 'next_page_disabled' => '
  • »
  • ', 21 | 'page_item' => '
  • %s
  • ', 22 | 'page_item_active' => '
  • %s
  • ', 23 | 'wrapper' => '
      %s
    ', // 外层 24 | ]; 25 | 26 | /** 27 | * 构造方法 28 | * 29 | * @param int $curPage 当前页码 30 | * @param int $pageSize 每页数量 31 | * @param int $totalNum 总记录数 32 | * @param string $route 路由地址 33 | * @param array $params 路由参数 34 | */ 35 | public function __construct($curPage, $pageSize, $totalNum, $route = CUR_ROUTE, $params = []) 36 | { 37 | $this->config = [ 38 | 'curPage' => $curPage, 39 | 'pageSize' => $pageSize, 40 | 'totalNum' => $totalNum, 41 | 'linkNum' => 10, 42 | 'offset' => 5, 43 | 'route' => $route, 44 | 'params' => $params, 45 | ]; 46 | } 47 | 48 | /** 49 | * 设置参数 50 | * 51 | * @param string $name 参数名 52 | * @param mixed $value 参数值 53 | */ 54 | public function setParam($name, $value) 55 | { 56 | $this->config[$name] = $value; 57 | } 58 | 59 | /** 60 | * 获取参数值 61 | * 62 | * @param string $name 63 | * @return mixed 64 | */ 65 | public function getParam($name) 66 | { 67 | return isset($this->config[$name]) ? $this->config[$name] : null; 68 | } 69 | 70 | /** 71 | * 设置模板 72 | * 73 | * @param array $templates 74 | */ 75 | public function setTemplates(array $templates) 76 | { 77 | $this->templates = array_merge($this->templates, $templates); 78 | } 79 | 80 | /** 81 | * 生成URL 82 | * @param int $page 页码 83 | * @return string 84 | */ 85 | private function makeUrl($page) 86 | { 87 | $params = $this->config['params']; 88 | $params['page'] = intval($page); 89 | return URL($this->config['route'], $params); 90 | } 91 | 92 | /** 93 | * 返回显示分页HTML 94 | */ 95 | public function makeHtml() 96 | { 97 | $totalNum = intval($this->config['totalNum']); 98 | $pageSize = intval($this->config['pageSize']); 99 | $linkNum = intval($this->config['linkNum']); 100 | $curPage = intval($this->config['curPage']); 101 | $pageHtml = ''; 102 | 103 | if ($totalNum > $pageSize) { 104 | $totalPage = @ceil($totalNum / $pageSize); 105 | if ($totalPage < $linkNum) { 106 | $from = 1; 107 | $to = $totalPage; 108 | } else { 109 | $from = $curPage - $this->config['offset']; 110 | $to = $from + $linkNum; 111 | if ($from < 1) { 112 | $from = 1; 113 | $to = $from + $linkNum - 1; 114 | } elseif ($to > $totalPage) { 115 | $to = $totalPage; 116 | $from = $totalPage - $linkNum + 1; 117 | } 118 | } 119 | if ($curPage > 1) { 120 | $pageHtml .= sprintf($this->templates['prev_page'], $this->makeUrl($curPage - 1)); 121 | } else { 122 | $pageHtml .= $this->templates['prev_page_disabled']; 123 | } 124 | if ($curPage > $linkNum) $pageHtml .= sprintf($this->templates['page_item'], $this->makeUrl(1), '1...'); 125 | for ($i = $from; $i <= $to; $i++) { 126 | if ($i == $curPage) { 127 | $pageHtml .= sprintf($this->templates['page_item_active'], $i); 128 | } else { 129 | $pageHtml .= sprintf($this->templates['page_item'], $this->makeUrl($i), $i); 130 | } 131 | } 132 | if ($totalPage > $to) $pageHtml .= sprintf($this->templates['page_item'], $this->makeUrl($totalPage), '...' . $totalPage); 133 | if ($curPage < $totalPage) { 134 | $pageHtml .= sprintf($this->templates['next_page'], $this->makeUrl($curPage + 1)); 135 | } else { 136 | $pageHtml .= sprintf($this->templates['next_page_disabled']); 137 | } 138 | $pageHtml = sprintf($this->templates['wrapper'], $pageHtml); 139 | } 140 | 141 | return $pageHtml; 142 | } 143 | 144 | public function __toString() 145 | { 146 | return $this->makeHtml(); 147 | } 148 | 149 | } 150 | -------------------------------------------------------------------------------- /src/Core/Http/UploadedFile.php: -------------------------------------------------------------------------------- 1 | 13 | * @package Core\Http 14 | */ 15 | class UploadedFile implements UploadedFileInterface 16 | { 17 | 18 | private static $errors = [ 19 | UPLOAD_ERR_OK, 20 | UPLOAD_ERR_CANT_WRITE, 21 | UPLOAD_ERR_EXTENSION, 22 | UPLOAD_ERR_FORM_SIZE, 23 | UPLOAD_ERR_INI_SIZE, 24 | UPLOAD_ERR_NO_FILE, 25 | UPLOAD_ERR_NO_TMP_DIR, 26 | UPLOAD_ERR_PARTIAL, 27 | ]; 28 | 29 | /** 30 | * 上传到服务器的文件路径 31 | * @var string 32 | */ 33 | private $file; 34 | 35 | /** 36 | * 文件名 37 | * @var string 38 | */ 39 | private $name; 40 | 41 | /** 42 | * 文件类型 43 | * @var string 44 | */ 45 | private $type; 46 | 47 | /** 48 | * 文件大小 49 | * @var int 50 | */ 51 | private $size; 52 | 53 | /** 54 | * 错误码 55 | * @var int 56 | */ 57 | private $error; 58 | 59 | /** 60 | * 是否SAPI 61 | * @var bool 62 | */ 63 | private $sapi; 64 | 65 | /** 66 | * @var StreamInterface 67 | */ 68 | private $stream; 69 | 70 | /** 71 | * 是否已经移走 72 | * @var bool 73 | */ 74 | private $moved = false; 75 | 76 | public function __construct($file, $name = null, $type = null, $size = null, $error = UPLOAD_ERR_OK, $sapi = false) 77 | { 78 | if (!in_array($error, self::$errors)) { 79 | throw new InvalidArgumentException('Invalid error status.'); 80 | } 81 | if (null !== $size && false === is_int($size)) { 82 | throw new InvalidArgumentException('Upload file size must be an integer'); 83 | } 84 | $this->file = $file; 85 | $this->name = $name; 86 | $this->type = $type; 87 | $this->size = $size; 88 | $this->error = $error; 89 | $this->sapi = $sapi; 90 | } 91 | 92 | /** 93 | * 获取上传文件的流对象 94 | * 95 | * @return StreamInterface 96 | * @throws RuntimeException 97 | */ 98 | public function getStream() 99 | { 100 | if ($this->moved) { 101 | throw new RuntimeException(sprintf('Uploaded file %1s has already been moved', $this->name)); 102 | } 103 | if ($this->stream === null) { 104 | $this->stream = new Stream(fopen($this->file, 'r')); 105 | } 106 | return $this->stream; 107 | } 108 | 109 | /** 110 | * 移动上传文件 111 | * 112 | * @param string $targetPath 113 | */ 114 | public function moveTo($targetPath) 115 | { 116 | if ($this->moved) { 117 | throw new RuntimeException('Uploaded file already moved'); 118 | } 119 | $targetIsStream = strpos($targetPath, '://') > 0; 120 | if (!$targetIsStream && !is_writable(dirname($targetPath))) { 121 | throw new RuntimeException('Upload target path is not writable'); 122 | } 123 | if ($targetIsStream) { 124 | if (!copy($this->file, $targetPath)) { 125 | throw new RuntimeException("Error moving uploaded file {$this->name} to {$targetPath}"); 126 | } 127 | if (!unlink($this->file)) { 128 | throw new RuntimeException("Error removing uploaded file {$this->file}"); 129 | } 130 | } elseif ($this->sapi) { 131 | if (!is_uploaded_file($this->file)) { 132 | throw new RuntimeException("{$this->file} is not a valid uploaded file"); 133 | } 134 | if (!move_uploaded_file($this->file, $targetPath)) { 135 | throw new RuntimeException("Error moving uploaded file {$this->name} to {$targetPath}"); 136 | } 137 | } else { 138 | if (!rename($this->file, $targetPath)) { 139 | throw new RuntimeException("Error moving uploaded file {$this->name} to {$targetPath}"); 140 | } 141 | } 142 | $this->moved = true; 143 | } 144 | 145 | /** 146 | * 获取上传文件大小 147 | * 148 | * @return int|null 149 | */ 150 | public function getSize() 151 | { 152 | return $this->size; 153 | } 154 | 155 | /** 156 | * 获取错误码 157 | * 158 | * @return int 159 | */ 160 | public function getError() 161 | { 162 | return $this->error; 163 | } 164 | 165 | /** 166 | * 获取源文件名 167 | * 168 | * @return string|null 169 | */ 170 | public function getClientFilename() 171 | { 172 | return $this->name; 173 | } 174 | 175 | /** 176 | * 获取媒体类型 177 | * 178 | * @return string|null 179 | */ 180 | public function getClientMediaType() 181 | { 182 | return $this->type; 183 | } 184 | } -------------------------------------------------------------------------------- /src/Core/Web/Debug/Lib/Profile.php: -------------------------------------------------------------------------------- 1 | data = $data; 25 | $this->process(); 26 | $this->calculateSelf(); 27 | } 28 | 29 | public function getMeta() 30 | { 31 | return $this->data['meta']; 32 | } 33 | 34 | /** 35 | * 返回SQL日志 36 | * 37 | * @return array|mixed 38 | */ 39 | public function getSqlLogs() 40 | { 41 | return isset($this->data['sql']) ? $this->data['sql'] : []; 42 | } 43 | 44 | /** 45 | * 返回分析结果 46 | * 47 | * @return array 48 | */ 49 | public function getProfile() 50 | { 51 | return $this->collapsed; 52 | } 53 | 54 | /** 55 | * 根据指标排序 56 | * 57 | * @param string $key 指标 58 | * @param array $data 59 | * @return mixed 60 | */ 61 | public function sort($key, $data) 62 | { 63 | $sorter = function ($a, $b) use ($key) { 64 | if ($a[$key] == $b[$key]) { 65 | return 0; 66 | } 67 | return $a[$key] > $b[$key] ? -1 : 1; 68 | }; 69 | uasort($data, $sorter); 70 | return $data; 71 | } 72 | 73 | private function process() 74 | { 75 | $result = []; 76 | foreach ($this->data['profile'] as $name => $values) { 77 | list($parent, $func) = $this->splitName($name); 78 | 79 | if (isset($result[$func])) { 80 | $result[$func] = $this->sumKeys($result[$func], $values); 81 | $result[$func]['parents'][] = $parent; 82 | } else { 83 | $result[$func] = $values; 84 | $result[$func]['parents'] = [$parent]; 85 | } 86 | 87 | // main()函数 88 | if ($parent === null) { 89 | $parent = self::NO_PARENT; 90 | } 91 | if (!isset($this->indexed[$parent])) { 92 | $this->indexed[$parent] = []; 93 | } 94 | $this->indexed[$parent][$func] = $values; 95 | } 96 | $this->collapsed = $result; 97 | } 98 | 99 | private function splitName($name) 100 | { 101 | $a = explode("==>", $name); 102 | if (isset($a[1])) { 103 | return $a; 104 | } 105 | return [null, $a[0]]; 106 | } 107 | 108 | private function sumKeys($a, $b) 109 | { 110 | foreach ($this->keys as $key) { 111 | if (!isset($a[$key])) { 112 | $a[$key] = 0; 113 | } 114 | $a[$key] += isset($b[$key]) ? $b[$key] : 0; 115 | } 116 | return $a; 117 | } 118 | 119 | protected function getChildren($symbol, $metric = null, $threshold = 0) 120 | { 121 | $children = []; 122 | if (!isset($this->indexed[$symbol])) { 123 | return $children; 124 | } 125 | 126 | $total = 0; 127 | if (isset($metric)) { 128 | $top = $this->indexed[self::NO_PARENT]; 129 | // Not always 'main()' 130 | $mainFunc = current($top); 131 | $total = $mainFunc[$metric]; 132 | } 133 | 134 | foreach ($this->indexed[$symbol] as $name => $data) { 135 | if ( 136 | $metric && $total > 0 && $threshold > 0 && 137 | ($this->indexed[$name][$metric] / $total) < $threshold 138 | ) { 139 | continue; 140 | } 141 | $children[] = $data + ['function' => $name]; 142 | } 143 | return $children; 144 | } 145 | 146 | private function calculateSelf() 147 | { 148 | // 初始化函数自身的各项执行指标 149 | foreach ($this->collapsed as &$data) { 150 | $data['ewt'] = $data['wt']; 151 | $data['emu'] = $data['mu']; 152 | $data['ecpu'] = $data['cpu']; 153 | $data['ect'] = $data['ct']; 154 | $data['epmu'] = $data['pmu']; 155 | } 156 | unset($data); 157 | 158 | // 总值 - 所有子函数的值 = 自身的值 159 | foreach ($this->collapsed as $name => $data) { 160 | $children = $this->getChildren($name); 161 | foreach ($children as $child) { 162 | $this->collapsed[$name]['ewt'] -= $child['wt']; 163 | $this->collapsed[$name]['emu'] -= $child['mu']; 164 | $this->collapsed[$name]['ecpu'] -= $child['cpu']; 165 | $this->collapsed[$name]['ect'] -= $child['ct']; 166 | $this->collapsed[$name]['epmu'] -= $child['pmu']; 167 | } 168 | } 169 | } 170 | } -------------------------------------------------------------------------------- /src/Core/Environment.php: -------------------------------------------------------------------------------- 1 | 完成开发提交给测试人员测试 -> 发布到预发布环境进行功能检查 -> 上线 9 | * 10 | * 对应的运行环境为: 11 | * - development 开发环境 12 | * - testing 测试环境 13 | * - pre_release 预发布环境 14 | * - production 生产环境 15 | * 16 | * 为了让运行环境的配置不入侵到代码中,这里支持两种形式的设置: 17 | * 1. 通过配置服务器环境变量来指定,代码中通过 $_SERVER[var] 获取。适用于运行在http模式。 18 | * 在nginx中可以使用 `fastcgi_param ENVIRONMENT development;` 指令来配置一个名为 ENVIRONMENT,值 19 | * 为 development 的环境变量。apache服务器可以在VirtualHost配置中使用 `SetEnv ENVIRONMENT development` 进行设置。 20 | * 2. 将当前的环境名称写入到一个外部文件中,然后通过读取这个外部文件内容来获取当前的运行环境。这个适用于运行在命令行模式。 21 | * 22 | * @author lisijie 23 | * @package Core 24 | */ 25 | class Environment 26 | { 27 | // 开发环境 28 | const DEVELOPMENT = 'development'; 29 | 30 | // 测试环境 31 | const TESTING = 'testing'; 32 | 33 | // 预发布环境 34 | const PRE_RELEASE = 'pre_release'; 35 | 36 | // 生产环境 37 | const PRODUCTION = 'production'; 38 | 39 | // 环境变量名称 40 | private static $envVar = 'ENVIRONMENT'; 41 | 42 | // 当前环境 43 | private static $environment; 44 | 45 | // 环境文件名 46 | private static $envFile = ''; 47 | 48 | // 自定义环境 49 | private static $customEnvs = []; 50 | 51 | /** 52 | * 设置环境配置文件 53 | * @param $filename 54 | */ 55 | public static function setEnvFile($filename) 56 | { 57 | self::$envFile = $filename; 58 | } 59 | 60 | /** 61 | * 设置服务器环境变量名 62 | * @param $varName 63 | */ 64 | public static function setEnvVar($varName) 65 | { 66 | self::$envVar = $varName; 67 | } 68 | 69 | /** 70 | * 添加自定义环境名称 71 | * @param $envName 72 | */ 73 | public static function addEnvironment($envName) 74 | { 75 | self::$customEnvs[] = $envName; 76 | } 77 | 78 | /** 79 | * 检查当前是否是某个环境 80 | * @param $env 81 | * @return bool 82 | */ 83 | public static function isEnvironment($env) 84 | { 85 | return self::getEnvironment() === $env; 86 | } 87 | 88 | /** 89 | * 检查是否开发环境 90 | * @return bool 91 | */ 92 | public static function isDevelopment() 93 | { 94 | return self::getEnvironment() == self::DEVELOPMENT; 95 | } 96 | 97 | /** 98 | * 检查是否测试环境 99 | * @return bool 100 | */ 101 | public static function isTesting() 102 | { 103 | return self::getEnvironment() == self::TESTING; 104 | } 105 | 106 | /** 107 | * 检查是否预发布环境 108 | * @return bool 109 | */ 110 | public static function isPreRelease() 111 | { 112 | return self::getEnvironment() == self::PRE_RELEASE; 113 | } 114 | 115 | /** 116 | * 检查是否生产环境 117 | * @return bool 118 | */ 119 | public static function isProduction() 120 | { 121 | return self::getEnvironment() == self::PRODUCTION; 122 | } 123 | 124 | /** 125 | * 设置当前环境 126 | * @param $env 127 | */ 128 | public static function setEnvironment($env) 129 | { 130 | if (self::isValid($env)) { 131 | self::$environment = $env; 132 | } else { 133 | exit('invalid env: ' . $env); 134 | } 135 | } 136 | 137 | /** 138 | * 返回当前的环境名称 139 | * 140 | * 首先检查是否指定了环境配置文件,如果存在则优先获取配置文件内容来设置当前运行环境。 141 | * 否则将检查$_SERVER环境变量,如果存在则使用该值进行设置。 142 | * 如果都没设置,则设为默认的production环境。 143 | * @return string 144 | */ 145 | public static function getEnvironment() 146 | { 147 | if (!self::$environment) { 148 | if (!empty(self::$envFile) && is_file(self::$envFile)) { // 指定环境变量文件 149 | self::setEnvironment(trim(file_get_contents(self::$envFile))); 150 | } elseif (isset($_SERVER[self::$envVar]) && !empty($_SERVER[self::$envVar])) { // 检查$_SERVER环境变量 151 | self::setEnvironment($_SERVER[self::$envVar]); 152 | } else { // 默认环境 153 | self::setEnvironment(self::getDefaultEnvironment()); 154 | } 155 | if (!empty(self::$envFile) && !is_file(self::$envFile)) { 156 | file_put_contents(self::$envFile, self::$environment); 157 | } 158 | } 159 | return self::$environment; 160 | } 161 | 162 | /** 163 | * 返回默认环境名称 164 | * @return string 165 | */ 166 | private static function getDefaultEnvironment() 167 | { 168 | return self::PRODUCTION; 169 | } 170 | 171 | /** 172 | * 验证是否有效的环境名称 173 | * @param $env 174 | * @return bool 175 | */ 176 | private static function isValid($env) 177 | { 178 | $ref = new \ReflectionClass(get_called_class()); 179 | $environments = array_merge(array_values($ref->getConstants()), self::$customEnvs); 180 | return in_array($env, $environments); 181 | } 182 | } -------------------------------------------------------------------------------- /src/Core/Container/Container.php: -------------------------------------------------------------------------------- 1 | 12 | * @package Core 13 | */ 14 | class Container implements ContainerInterface 15 | { 16 | /** 17 | * 对象定义 18 | * @var array 19 | */ 20 | private $definitions = []; 21 | 22 | /** 23 | * 共享对象 24 | * @var array 25 | */ 26 | private $sharedInstances = []; 27 | 28 | /** 29 | * @var []ServiceProviderInterface 30 | */ 31 | private $serviceProviders = []; 32 | 33 | /** 34 | * 设置服务提供者 35 | * 36 | * @param ServiceProviderInterface $provider 37 | */ 38 | public function addServiceProvider(ServiceProviderInterface $provider) 39 | { 40 | $this->serviceProviders[] = $provider; 41 | } 42 | 43 | /** 44 | * 添加对象 45 | * 46 | * $definition 可以是一个配置信息,格式为: 47 | * ['class' => className, 'param1' => 'value1', 'param2' => 'value2' ...] 48 | * 示例化时,如果构造函数有定义了对应名称的参数,则传给构造函数,否则通过 setter 函数赋值给对象。 49 | * 50 | * @param string $name 名称 51 | * @param mixed $definition 定义 52 | * @param bool $shared 是否共享实例 53 | * @return bool 54 | */ 55 | public function set($name, $definition, $shared) 56 | { 57 | $this->definitions[$name] = $definition; 58 | if ($shared) { 59 | $this->sharedInstances[$name] = null; 60 | } else { 61 | unset($this->sharedInstances[$name]); 62 | } 63 | return true; 64 | } 65 | 66 | /** 67 | * 从容器获取 68 | * 69 | * @param string $name 70 | * @return mixed 71 | * @throws CoreException 72 | */ 73 | public function get($name) 74 | { 75 | if (!isset($this->definitions[$name])) { 76 | foreach ($this->serviceProviders as $provider) { 77 | if ($provider->has($name)) { 78 | return $provider->get($name); 79 | } 80 | } 81 | throw new NotFoundException("对象名称未注册: {$name}"); 82 | } 83 | 84 | if (array_key_exists($name, $this->sharedInstances) && (null !== $this->sharedInstances[$name])) { 85 | return $this->sharedInstances[$name]; 86 | } 87 | $definition = $this->definitions[$name]; 88 | $object = null; 89 | switch ($definition) { 90 | case is_callable($definition): // 注册的是一个函数或闭包 91 | $object = $definition(); 92 | break; 93 | case is_object($definition): // 已经实例化的对象 94 | $object = $definition; 95 | break; 96 | case is_string($definition) && class_exists($definition): // 注册的是类名 97 | $refClass = new ReflectionClass($definition); 98 | $object = $refClass->newInstance(); 99 | break; 100 | case is_array($definition) && isset($definition['class']) && class_exists($definition['class']): // 指定配置格式 101 | $refClass = new ReflectionClass($definition['class']); 102 | unset($definition['class']); 103 | $args = []; 104 | if ($method = $refClass->getConstructor()) { 105 | foreach ($method->getParameters() as $parameter) { 106 | $paramName = $parameter->getName(); 107 | if (isset($definition[$paramName])) { 108 | $args[] = $definition[$paramName]; 109 | unset($definition[$paramName]); 110 | } elseif (!$parameter->isOptional()) { 111 | throw new ContainerException("构建 {$refClass->getName()} 失败,构造函数缺少参数: {$parameter->getName()}"); 112 | } 113 | } 114 | } 115 | $object = $refClass->newInstanceArgs($args); 116 | // 剩余的参数传给 setter 方法(如果有的话) 117 | foreach ([$definition, $args] as $params) { 118 | foreach ($params as $key => $value) { 119 | if (is_string($key) && method_exists($object, "set{$key}")) { 120 | call_user_func_array([$object, "set{$key}"], [$value]); 121 | } 122 | } 123 | } 124 | break; 125 | default: 126 | $object = $definition; 127 | break; 128 | } 129 | if (array_key_exists($name, $this->sharedInstances)) { 130 | $this->sharedInstances[$name] = $object; 131 | } 132 | return $object; 133 | } 134 | 135 | /** 136 | * 检查容器中是否存在某个名称 137 | * 138 | * @param string $name 139 | * @return bool 140 | */ 141 | public function has($name) 142 | { 143 | $ok = array_key_exists($name, $this->definitions); 144 | if (!$ok) { 145 | foreach ($this->serviceProviders as $provider) { 146 | if ($provider->has($name)) { 147 | return true; 148 | } 149 | } 150 | } 151 | return $ok; 152 | } 153 | } -------------------------------------------------------------------------------- /src/Core/Lib/Upload.php: -------------------------------------------------------------------------------- 1 | 13 | * @package Core\Lib 14 | */ 15 | class Upload 16 | { 17 | 18 | // 上传目录 19 | private $savePath = 'upload/'; 20 | // 允许类型 21 | private $allowTypes = []; 22 | // 文件大小上限 23 | private $maxsize = 0; 24 | //错误消息 25 | private $message = [ 26 | 0 => '上传成功', 27 | 1 => '上传的文件超过了 php.ini 中 upload_max_filesize 选项限制的值', 28 | 2 => '上传文件的大小超过了 HTML 表单中 MAX_FILE_SIZE 选项指定的值', 29 | 3 => '文件只有部分被上传', 30 | 4 => '文件没有被上传', 31 | 5 => '找不到临时文件夹', 32 | 6 => '文件写入失败', 33 | -1 => '文件大小超出限制', 34 | -2 => '文件类型不允许', 35 | ]; 36 | 37 | public function __construct($options = []) 38 | { 39 | if (isset($options['allow_types'])) { 40 | if (!is_array($options['allow_types'])) { 41 | $options['allow_types'] = explode('|', $options['allow_types']); 42 | } 43 | $this->allowTypes = $options['allow_types']; 44 | } 45 | if (isset($options['save_path'])) { 46 | $this->savePath = $options['save_path']; 47 | } 48 | if (isset($options['maxsize'])) { 49 | $this->maxsize = intval($options['maxsize']); 50 | } 51 | $this->savePath = str_replace(array('{y}', '{Y}', '{m}', '{d}', '\\', '..'), array(date('y'), date('Y'), date('m'), date('d'), '/', ''), $this->savePath); 52 | if (substr($this->savePath, -1) != '/') $this->savePath .= '/'; 53 | $this->maxsize *= 1024; //最大允许上传的文件大小/byte 54 | if (!is_dir($this->savePath)) { 55 | mkdir($this->savePath, 0755, true); //创建目录 56 | } 57 | } 58 | 59 | /** 60 | * 返回本次上传的附件保存目录 61 | */ 62 | public function getSavePath() 63 | { 64 | return $this->savePath; 65 | } 66 | 67 | /** 68 | * 生成上传文件保存路径 69 | * 70 | * @param string $ext 扩展名 71 | * @return string 72 | */ 73 | public function makeSaveFile($ext) 74 | { 75 | return $this->savePath . date('YmdHis') . '_' . mt_rand(1, 99999) . ".{$ext}"; 76 | } 77 | 78 | /** 79 | * 执行上传动作 80 | * 81 | * @param string $field 表单项名称 82 | * @param boolean $multi 是否同时上传多个,默认false 83 | * @return array 返回上传结果 84 | */ 85 | public function execute($field, $multi = FALSE) 86 | { 87 | @set_time_limit(0); 88 | if ($multi && is_array($_FILES[$field]['name'])) { 89 | $files = array(); 90 | $_FILES[$field]['name'] = array_unique($_FILES[$field]['name']); //去除重复 91 | foreach ($_FILES[$field]['name'] as $key => $value) { 92 | if (!empty($value)) { 93 | $files[] = "{$field}_{$key}"; 94 | $_FILES["{$field}_{$key}"] = array( 95 | 'name' => $_FILES[$field]['name'][$key], 96 | 'type' => $_FILES[$field]['type'][$key], 97 | 'tmp_name' => $_FILES[$field]['tmp_name'][$key], 98 | 'error' => $_FILES[$field]['error'][$key], 99 | 'size' => $_FILES[$field]['size'][$key], 100 | ); 101 | } 102 | } 103 | unset($_FILES[$field]); 104 | $result = array(); 105 | foreach ($files as $file) { 106 | $result[] = $this->execute($file, FALSE); 107 | } 108 | return $result; 109 | } 110 | 111 | $file = $_FILES[$field]; 112 | $fileext = $this->fileext($file['name']); 113 | $result = array( 114 | 'error' => '', 115 | 'name' => $file['name'], 116 | 'path' => '', 117 | 'size' => $file['size'], 118 | 'type' => $file['type'], 119 | 'ext' => $fileext, 120 | ); 121 | if ($file['error']) { 122 | $result['error'] = $this->errmsg($file['error']); 123 | } elseif ($file['size'] > $this->maxsize) { 124 | $result['error'] = $this->errmsg(-1); 125 | } elseif (!in_array($fileext, $this->allowTypes)) { 126 | $result['error'] = $this->errmsg(-2); 127 | } else { 128 | $file['tmp_name'] = str_replace('\\\\', '\\', $file['tmp_name']); 129 | if (is_uploaded_file($file['tmp_name'])) { 130 | $filepath = $this->makeSaveFile($fileext); 131 | if (move_uploaded_file($file['tmp_name'], $filepath)) { 132 | $result['path'] = $filepath; 133 | } 134 | } 135 | } 136 | 137 | return $result; 138 | } 139 | 140 | /** 141 | * 根据错误号返回错误消息 142 | * 143 | * @param int $code 144 | * @return string 145 | */ 146 | private function errmsg($code) 147 | { 148 | return isset($this->message[$code]) ? $this->message[$code] : '未知错误'; 149 | } 150 | 151 | /** 152 | * 返回小写文件扩展名 153 | * 154 | * @param $file 155 | * @return string 156 | */ 157 | private function fileext($file) 158 | { 159 | return strtolower(pathinfo($file, PATHINFO_EXTENSION)); 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /src/Core/Logger/Logger.php: -------------------------------------------------------------------------------- 1 | 12 | * @package Core\Logger 13 | */ 14 | class Logger implements LoggerInterface 15 | { 16 | const FATAL = 5; 17 | const ERROR = 4; 18 | const WARN = 3; 19 | const INFO = 2; 20 | const DEBUG = 1; 21 | 22 | /** 23 | * 日志等级对应名称映射 24 | * 25 | * @var array 26 | */ 27 | protected $levels = [ 28 | self::FATAL => 'FATAL', 29 | self::ERROR => 'ERROR', 30 | self::WARN => 'WARN', 31 | self::INFO => 'INFO', 32 | self::DEBUG => 'DEBUG', 33 | ]; 34 | 35 | /** 36 | * 日志通道名称 37 | * @var string 38 | */ 39 | protected $channel; 40 | 41 | /** 42 | * 时区 43 | * @var \DateTimeZone 44 | */ 45 | protected $timeZone; 46 | 47 | /** 48 | * 日志处理器 49 | * @var array 50 | */ 51 | protected $handlers = []; 52 | 53 | public function __construct($channel) 54 | { 55 | $this->channel = $channel; 56 | } 57 | 58 | /** 59 | * 获取名称 60 | * 61 | * @return string 62 | */ 63 | public function getName() 64 | { 65 | return $this->channel; 66 | } 67 | 68 | /** 69 | * 设置日志处理器 70 | * 71 | * @param HandlerInterface $handler 72 | */ 73 | public function addHandler(HandlerInterface $handler) 74 | { 75 | $this->handlers[] = $handler; 76 | } 77 | 78 | /** 79 | * 设置时区 80 | * 81 | * @param \DateTimeZone $timeZone 82 | */ 83 | public function setTimeZone(\DateTimeZone $timeZone) 84 | { 85 | $this->timeZone = $timeZone; 86 | } 87 | 88 | /** 89 | * 发生危险错误 90 | * 91 | * 例如: 应用程序组件不可用,意想不到的异常。 92 | * 93 | * @param string $message 94 | * @param array $context 95 | * @return null|void 96 | */ 97 | public function fatal($message, array $context = []) 98 | { 99 | $this->log(self::FATAL, $message, $context); 100 | } 101 | 102 | /** 103 | * 运行时错误 104 | * 105 | * 例如: 用户非法操作,一般不需要立即采取行动,但需要记录和监控 106 | * 107 | * @param string $message 108 | * @param array $context 109 | * @return null|void 110 | */ 111 | public function error($message, array $context = []) 112 | { 113 | $this->log(self::ERROR, $message, $context); 114 | } 115 | 116 | /** 117 | * 表明会出现潜在错误的情形 118 | * 119 | * 例如: 使用一个已经废弃的API,虽然没有错误,但应该提醒用户修正 120 | * 121 | * @param string $message 122 | * @param array $context 123 | * @return null|void 124 | */ 125 | public function warn($message, array $context = []) 126 | { 127 | $this->log(self::WARN, $message, $context); 128 | } 129 | 130 | /** 131 | * 记录程序运行时的相关信息 132 | * 133 | * 例如: 用户登录,SQL记录 134 | * 135 | * @param string $message 136 | * @param array $context 137 | * @return null|void 138 | */ 139 | public function info($message, array $context = []) 140 | { 141 | $this->log(self::INFO, $message, $context); 142 | } 143 | 144 | /** 145 | * 调试信息 146 | * 147 | * 主要用于开发期间记录调试信息,线上一般不开启 148 | * 149 | * @param string $message 150 | * @param array $context 151 | * @return null|void 152 | */ 153 | public function debug($message, array $context = []) 154 | { 155 | $this->log(self::DEBUG, $message, $context); 156 | } 157 | 158 | /** 159 | * 记录日志 160 | * 161 | * @param int $level 日志级别 162 | * @param string $message 内容 163 | * @param array $context 上下文 164 | * @return null|void 165 | * @throws InvalidArgumentException 166 | */ 167 | private function log($level, $message, array $context = []) 168 | { 169 | if (!isset($this->levels[$level])) { 170 | throw new InvalidArgumentException('日志级别无效:' . $level); 171 | } 172 | 173 | if (empty($this->handlers)) { 174 | $this->addHandler(new NullHandler()); 175 | } 176 | 177 | if (!$this->timeZone) { 178 | $this->timeZone = new \DateTimeZone(date_default_timezone_get() ?: 'UTC'); 179 | } 180 | if (is_array($message)) { 181 | $message = json_encode($message, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE); 182 | } 183 | $file = '???'; 184 | $line = 0; 185 | $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2); 186 | if (isset($backtrace[1]) && isset($backtrace[1]['file'])) { 187 | $file = basename($backtrace[1]['file']); 188 | $line = $backtrace[1]['line']; 189 | } 190 | $record = [ 191 | 'message' => (string)$message, 192 | 'context' => $context, 193 | 'level' => $level, 194 | 'level_name' => $this->levels[$level], 195 | 'channel' => $this->channel, 196 | 'datetime' => \DateTime::createFromFormat('U.u', sprintf('%.6F', microtime(true)), $this->timeZone)->setTimezone($this->timeZone), 197 | 'extra' => [], 198 | 'file' => $file, 199 | 'line' => $line, 200 | ]; 201 | 202 | foreach ($this->handlers as $handler) { 203 | if ($handler->handle($record)) { 204 | break; 205 | } 206 | } 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /src/Core/Lib/Form.php: -------------------------------------------------------------------------------- 1 | ' . "\r\n"; 21 | return $string; 22 | } 23 | 24 | /** 25 | * 多行输入框 26 | * 27 | * 创建一个单行输入框 28 | * @param string $name 控件名称 29 | * @param string $value 默认值 30 | * @param array $attr 其他属性 31 | * @return string 32 | */ 33 | public static function textarea($name, $value = '', $attr = []) 34 | { 35 | $string = '' . "\r\n"; 36 | return $string; 37 | } 38 | 39 | /** 40 | * 编辑器 41 | * 42 | * 创建一个编辑器实例 43 | * @param string $name 控件名称 44 | * @param string $value 默认值 45 | * @param string $mode 模式(full|simple),默认:full 46 | * @param array $attr 47 | * @return string 48 | */ 49 | public static function editor($name, $value = '', $mode = 'full', $attr = []) 50 | { 51 | if (!defined('EDITOR_INIT')) { 52 | $string = ' 53 | 56 | 57 | 58 | '; 59 | define('EDITOR_INIT', TRUE); 60 | } 61 | $options = []; 62 | $options['imageUrl'] = isset($attr['imageUrl']) ? $attr['imageUrl'] : ''; 63 | $options['fileUrl'] = isset($attr['fileUrl']) ? $attr['fileUrl'] : ''; 64 | $options['imagePath'] = $options['filePath'] = ''; 65 | if ($mode == 'simple') { 66 | $options['initialFrameWidth'] = isset($attr['width']) ? $attr['width'] : '500'; 67 | $options['initialFrameHeight'] = isset($attr['height']) ? $attr['height'] : '200'; 68 | $options['toolbars'] = [["bold", "italic", "underline", "strikethrough", "forecolor", "backcolor", "insertorderedlist", "insertunorderedlist", "justifyleft", "justifycenter", "justifyright", "justifyjustify", "fontfamily", "fontsize"]]; 69 | } else { 70 | $options['initialFrameWidth'] = isset($attr['width']) ? $attr['width'] : '90%'; 71 | $options['initialFrameHeight'] = isset($attr['height']) ? $attr['height'] : '400'; 72 | } 73 | $string .= ' 74 | 75 | 79 | '; 80 | return $string; 81 | } 82 | 83 | /** 84 | * 单选框 85 | * 86 | * 创建一组单选框 87 | * 88 | * @param string $name 控件名称 89 | * @param string $value 默认值 90 | * @param array $options 选项列表 array('值'=>'描述') 91 | * @return string 92 | */ 93 | public static function radio($name, $value = '', $options = []) 94 | { 95 | $string = ''; 96 | foreach ($options as $key => $val) { 97 | $id = str_replace(['[', ']'], '_', $name) . $key; 98 | $chk = $value == $key ? ' checked' : ''; 99 | $string .= '  '; 100 | } 101 | return $string; 102 | } 103 | 104 | /** 105 | * 创建一个下拉列表框 106 | * 107 | * @param string $name 控件名称 108 | * @param string $value 默认值 109 | * @param array $options 选项列表 array('值'=>'描述') 110 | * @param array $attr 111 | * @return string 112 | */ 113 | public static function select($name, $value, $options = [], $attr = []) 114 | { 115 | $string = "\n"; 121 | return $string; 122 | } 123 | 124 | /** 125 | * 复选框 126 | * 127 | * 创建一个复选框 128 | * @param string $name 控件名称 129 | * @param string $value 默认值 130 | * @param string $desc 描述 131 | * @param boolean $checked 默认是否选中 132 | * @return string 133 | */ 134 | public static function checkbox($name, $value, $desc, $checked = false) 135 | { 136 | $id = str_replace(['[', ']'], '_', $name) . $value; 137 | $string = ''; 138 | $string .= '' . "\r\n"; 139 | return $string; 140 | } 141 | 142 | private static function makeAttr($attr) 143 | { 144 | if (is_string($attr)) return $attr; 145 | $at = ''; 146 | foreach ($attr as $key => $val) { 147 | $at .= "{$key}=\"{$val}\" "; 148 | } 149 | return $at; 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /src/Core/Lib/Tree.php: -------------------------------------------------------------------------------- 1 | data[$id] = (array)$data; 40 | $this->child[$parentId][] = $id; 41 | $this->parent[$id] = $parentId; 42 | } 43 | 44 | /** 45 | * 根据节点ID返回节点数据 46 | * 47 | * @param int $id 48 | * @return null|array 49 | */ 50 | public function getValue($id) 51 | { 52 | return isset($this->data[$id]) ? $this->data[$id] : null; 53 | } 54 | 55 | /** 56 | * 获取某个节点的子节点ID列表 57 | * 58 | * @param int $id 59 | * @return array 60 | */ 61 | public function getChildren($id) 62 | { 63 | return isset($this->child[$id]) ? $this->child[$id] : array(); 64 | } 65 | 66 | /** 67 | * 获取某个节点的子节点(包括子节点的子节点)ID列表 68 | * 69 | * @param int $id 70 | * @return array 71 | */ 72 | public function getDeepChildren($id) 73 | { 74 | $list = array(); 75 | if (isset($this->child[$id])) { 76 | foreach ($this->child[$id] as $cid) { 77 | $list[] = $cid; 78 | if (isset($this->child[$cid])) { 79 | $list = array_merge($list, $this->getDeepChildren($cid)); 80 | } 81 | } 82 | } 83 | return $list; 84 | } 85 | 86 | /** 87 | * 返回某个节点的父节点ID 88 | * 89 | * @param int $id 90 | * @return int 91 | */ 92 | public function getParentId($id) 93 | { 94 | return $this->parent[$id]; 95 | } 96 | 97 | /** 98 | * 返回各层父ID列表 99 | * 100 | * @param int $id 101 | * @return array 102 | */ 103 | public function getDeepParentIds($id) 104 | { 105 | $list = array(); 106 | if ($this->parent[$id] > 0) { 107 | $list[] = $pid = $this->parent[$id]; 108 | if ($this->parent[$pid] > 0) { 109 | $list = array_merge($list, $this->getDeepParentIds($pid)); 110 | } 111 | } 112 | return $list; 113 | } 114 | 115 | /** 116 | * 获取某个节点下的子节点列表 117 | * 118 | * @param array $list 119 | * @param int $root 120 | * @return array 121 | */ 122 | public function getList(&$list, $root = 0) 123 | { 124 | if (isset($this->child[$root])) { 125 | foreach ($this->child[$root] as $id) { 126 | $list[] = $id; 127 | if (isset($this->child[$id])) { 128 | $this->getList($list, $id); 129 | } 130 | } 131 | } 132 | } 133 | 134 | /** 135 | * 获取某个节点的层级数 136 | * 137 | * @param int $id 138 | * @return int 139 | */ 140 | public function getLayer($id) 141 | { 142 | $n = 0; 143 | $nodeId = $id; 144 | while ($this->parent[$nodeId] > 0) { 145 | $nodeId = $this->parent[$nodeId]; 146 | $n++; 147 | } 148 | return $n; 149 | } 150 | 151 | public function getLayerHtml($id, $space = '', $s = '├─ ') 152 | { 153 | $layer = $this->getLayer($id); 154 | if ($layer > 0) { 155 | return str_repeat($space, $layer) . $s; 156 | } 157 | return ''; 158 | } 159 | 160 | /** 161 | * 返回一个树形结构HTML 162 | * 163 | * $str = '' 164 | * 165 | * @param int $rootId 根分类ID 166 | * @param int $selectedId 选中的ID 167 | * @param string $str 每个节点拼接的HTML格式 168 | * @param string $groupStr 169 | * @param string $selStr 170 | * @return string 171 | */ 172 | public function makeTreeHtml($rootId, $selectedId, $str, $groupStr = '', $selStr = 'selected') 173 | { 174 | $childIds = $this->getChildren($rootId); 175 | $_count = count($childIds); 176 | $result = ''; 177 | if ($_count > 0) { 178 | foreach ($childIds as $_key => $_id) { 179 | $data = $this->getValue($_id); 180 | 181 | if ($_count == ($_key + 1)) { 182 | $spacer = $this->getLayerHtml($_id, '|    ', $this->icon[2]); //└─ 183 | } else { 184 | $spacer = $this->getLayerHtml($_id, '|    ', $this->icon[1]); //├─ 185 | } 186 | 187 | $selected = ($_id == $selectedId) ? $selStr : ''; 188 | @extract($data); 189 | $string = ''; 190 | 191 | if ($groupStr && $this->getParentId($_id) > 0) { 192 | eval("\$string = \"{$groupStr}\";"); 193 | } else { 194 | eval("\$string = \"{$str}\";"); 195 | } 196 | 197 | $result .= $string; 198 | if (isset($this->child[$_id])) { 199 | $result .= $this->makeTreeHtml($_id, $selectedId, $str, $groupStr, $selStr); 200 | } 201 | } 202 | } 203 | return $result; 204 | } 205 | 206 | } 207 | -------------------------------------------------------------------------------- /src/Core/Lib/Files.php: -------------------------------------------------------------------------------- 1 | 1024) { 73 | $bytes /= 1024; 74 | $i++; 75 | } 76 | return round($bytes, 2) . $s[$i]; 77 | } 78 | 79 | /** 80 | * 检查文件或目录是否可写 81 | * 82 | * @param string $filename 文件或目录 83 | * @return bool 84 | */ 85 | public static function writeable($filename) 86 | { 87 | return is_writable($filename); 88 | } 89 | 90 | /** 91 | * 检查文件或目录是否可读 92 | * 93 | * @param string $filename 文件或目录 94 | * @return bool 95 | */ 96 | public static function readable($filename) 97 | { 98 | return is_readable($filename); 99 | } 100 | 101 | /** 102 | * 检查是否有效文件名 103 | * 104 | * @param string $string 105 | * @return bool 106 | */ 107 | public static function checkName($string) 108 | { 109 | return preg_match('/^[a-z0-9\_\]+$/', $string); 110 | } 111 | 112 | /** 113 | * 列出该目录及其子目录下的所有文件 114 | * 115 | * @param string $dir 目录名 116 | * @return array 117 | */ 118 | public static function scanDir($dir) 119 | { 120 | $dir = rtrim($dir, '/\\'); 121 | $result = []; 122 | if (is_dir($dir)) { 123 | if ($d = opendir($dir)) { 124 | while (false !== ($file = readdir($d))) { 125 | if ($file != '.' && $file != '..') { 126 | if (is_dir($dir . DIRECTORY_SEPARATOR . $file)) { 127 | $result = array_merge($result, static::scanDir($dir . DIRECTORY_SEPARATOR . $file)); 128 | } else { 129 | $result[] = $dir . DIRECTORY_SEPARATOR . $file; 130 | } 131 | } 132 | } 133 | closedir($d); 134 | } 135 | } 136 | return $result; 137 | } 138 | 139 | /** 140 | * 返回文件小写扩展名 141 | * 142 | * @param string $filename 143 | * @return string 144 | */ 145 | public static function getFileExt($filename) 146 | { 147 | return strtolower(pathinfo($filename, PATHINFO_EXTENSION)); 148 | } 149 | 150 | /** 151 | * 获取文件的MimeType 152 | * 153 | * @param string $filename 文件名 154 | * @return null|string 155 | */ 156 | public static function getMimeType($filename) 157 | { 158 | if (($ext = static::getFileExt($filename)) != "") { 159 | return static::getMimeTypeByExt($ext); 160 | } 161 | return null; 162 | } 163 | 164 | /** 165 | * 根据扩展名获取对应MimeType 166 | * 167 | * @param string $ext 扩展名 168 | * @return string 169 | */ 170 | public static function getMimeTypeByExt($ext) 171 | { 172 | self::loadMimeTypes(); 173 | return isset(static::$mimeTypes[$ext]) ? static::$mimeTypes[$ext] : ''; 174 | } 175 | 176 | /** 177 | * 加载mimeType配置信息 178 | */ 179 | private static function loadMimeTypes() 180 | { 181 | if (empty(self::$mimeTypes)) { 182 | self::$mimeTypes = require __DIR__ . '/mimeTypes.php'; 183 | } 184 | } 185 | 186 | /** 187 | * 递归拷贝 188 | * 189 | * @param string $src 源路径 190 | * @param string $dst 目标路径 191 | * @return bool 192 | */ 193 | public static function copyRecurse($src, $dst) 194 | { 195 | $dir = opendir($src); 196 | if (!$dir) return false; 197 | is_dir($dst) || mkdir($dst); 198 | while (false !== ($file = readdir($dir))) { 199 | if (($file != '.') && ($file != '..')) { 200 | if (is_dir($src . '/' . $file)) { 201 | self::copyRecurse($src . '/' . $file, $dst . '/' . $file); 202 | } else { 203 | copy($src . '/' . $file, $dst . '/' . $file); 204 | } 205 | } 206 | } 207 | closedir($dir); 208 | return true; 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /src/Core/Exception/ErrorHandler.php: -------------------------------------------------------------------------------- 1 | 15 | * @package Core\Exception 16 | */ 17 | class ErrorHandler 18 | { 19 | /** 20 | * 日志记录器 21 | * 22 | * @var LoggerInterface 23 | */ 24 | protected $logger; 25 | 26 | /** 27 | * 不记录日志的异常列表 28 | * 29 | * @var array 30 | */ 31 | protected $dontReport = [ 32 | 'Core\Exception\HttpException', 33 | ]; 34 | 35 | public function __construct(LoggerInterface $logger) 36 | { 37 | $this->logger = $logger; 38 | } 39 | 40 | /** 41 | * 注册异常处理器 42 | */ 43 | public function register() 44 | { 45 | // 关闭显示错误消息 46 | ini_set('display_errors', App::isDebug()); 47 | // 注册错误处理函数,将所有错误转为异常 48 | set_error_handler(function ($code, $str, $file, $line) { 49 | throw new \ErrorException($str, $code, 0, $file, $line); 50 | }); 51 | // 注册异常处理函数 52 | set_exception_handler([$this, 'handle']); 53 | // 注册shutdown函数, 记录运行时无法捕获的错误 54 | $this->registerShutdown(); 55 | } 56 | 57 | /** 58 | * shutdown函数, 记录运行时无法捕获的错误 59 | */ 60 | protected function registerShutdown() 61 | { 62 | register_shutdown_function(function () { 63 | $error = error_get_last(); 64 | if ($error) { 65 | $errTypes = [E_ERROR => 'E_ERROR', E_PARSE => 'E_PARSE', E_USER_ERROR => 'E_USER_ERROR']; 66 | if (isset($errTypes[$error['type']])) { 67 | $info = $errTypes[$error['type']] . ": {$error['message']} in {$error['file']} on line {$error['line']}"; 68 | $this->logger->fatal($info); 69 | } 70 | } 71 | }); 72 | } 73 | 74 | /** 75 | * 异常信息上报 76 | * 77 | * @param Exception|\ErrorException $e 78 | * @return bool 79 | */ 80 | protected function report($e) 81 | { 82 | if (!is_object($this->logger)) { 83 | return false; 84 | } 85 | foreach ($this->dontReport as $className) { 86 | if ($e instanceof $className) { 87 | return false; 88 | } 89 | } 90 | $this->logger->error((string)$e); 91 | return true; 92 | } 93 | 94 | /** 95 | * 输出异常信息 96 | * 97 | * @param Exception|\ErrorException $e 98 | */ 99 | protected function render($e) 100 | { 101 | if ($this->isHttpException($e)) { 102 | $this->renderHttpException($e); 103 | return; 104 | } 105 | 106 | if (App::isDebug()) { 107 | $this->renderDebugInfo($e); 108 | return; 109 | } 110 | 111 | exit('系统发生异常: ' . get_class($e) . '(' . $e->getCode() . ')'); 112 | } 113 | 114 | /** 115 | * 输出debug信息 116 | * 117 | * @param Exception|\ErrorException $e 118 | */ 119 | protected function renderDebugInfo($e) 120 | { 121 | $errType = get_class($e); 122 | 123 | if (PHP_SAPI == 'cli') { 124 | echo "----------------------------------------------------------------------------------------------------\n"; 125 | echo "exception: {$errType}\n"; 126 | echo "error: {$e->getMessage()} (#{$e->getCode()})\n"; 127 | echo "file: {$e->getFile()} ({$e->getLine()})\n"; 128 | echo "----------------------------------------------------------------------------------------------------\n"; 129 | echo $e->getTraceAsString(); 130 | echo "\n----------------------------------------------------------------------------------------------------\n"; 131 | } else { 132 | $errMsg = 'error: ' . $e->getMessage() . '
    errno: ' . $e->getCode(); 133 | $errMsg .= '
    file: ' . $e->getFile() . '(' . $e->getLine() . ')
    '; 134 | if ($e instanceof DBException) { 135 | $errMsg .= 'sql: ' . $e->getSql(); 136 | $errMsg .= '
    params: ' . VarDumper::export($e->getParams()); 137 | } 138 | echo '
    '; 139 | echo '

    [' . $errType . ']

    '; 140 | echo '

    '; 141 | echo $errMsg; 142 | echo '
    time: ' . date('Y-m-d H:i:s'); 143 | echo '

    '; 144 | 145 | echo '

    [PHP Debug]

    '; 146 | echo '
    ';
    147 |             echo $e->getTraceAsString();
    148 |             echo '
    '; 149 | echo '
    '; 150 | } 151 | } 152 | 153 | /** 154 | * 输出HTTP异常信息 155 | * 156 | * @param HttpException $e 157 | */ 158 | protected function renderHttpException(HttpException $e) 159 | { 160 | App::respond(new Response($e->getCode(), $e->getMessage())); 161 | } 162 | 163 | /** 164 | * 是否http异常 165 | * 166 | * @param Exception|\ErrorException $e 167 | * @return bool 168 | */ 169 | protected function isHttpException($e) 170 | { 171 | return ($e instanceof HttpException); 172 | } 173 | 174 | /** 175 | * 处理异常 176 | * 177 | * @param Exception|\ErrorException $e 178 | */ 179 | public function handle($e) 180 | { 181 | $this->report($e); 182 | $this->render($e); 183 | exit; 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /src/Core/Http/Stream.php: -------------------------------------------------------------------------------- 1 | 10 | * @package Core\Http 11 | */ 12 | class Stream implements StreamInterface 13 | { 14 | private $stream; 15 | private $readable; 16 | private $writable; 17 | private $seekable; 18 | private $size; 19 | private $uri; 20 | 21 | private static $readWriteMode = [ 22 | 'read' => [ 23 | 'r' => true, 'w+' => true, 'r+' => true, 'x+' => true, 'c+' => true, 24 | 'rb' => true, 'w+b' => true, 'r+b' => true, 'x+b' => true, 25 | 'c+b' => true, 'rt' => true, 'w+t' => true, 'r+t' => true, 26 | 'x+t' => true, 'c+t' => true, 'a+' => true 27 | ], 28 | 'write' => [ 29 | 'w' => true, 'w+' => true, 'rw' => true, 'r+' => true, 'x+' => true, 30 | 'c+' => true, 'wb' => true, 'w+b' => true, 'r+b' => true, 31 | 'x+b' => true, 'c+b' => true, 'w+t' => true, 'r+t' => true, 32 | 'x+t' => true, 'c+t' => true, 'a' => true, 'a+' => true 33 | ] 34 | ]; 35 | 36 | public function __construct($stream) 37 | { 38 | if (!is_resource($stream)) { 39 | throw new \InvalidArgumentException('stream must be a resource'); 40 | } 41 | $this->stream = $stream; 42 | $meta = stream_get_meta_data($stream); 43 | $this->seekable = (bool)$meta['seekable']; 44 | $this->readable = isset(self::$readWriteMode['read'][$meta['mode']]); 45 | $this->writable = isset(self::$readWriteMode['write'][$meta['mode']]); 46 | $this->uri = isset($meta['uri']) ? $meta['uri'] : null; 47 | } 48 | 49 | public function __toString() 50 | { 51 | try { 52 | $this->seek(0); 53 | $contents = (string)stream_get_contents($this->stream); 54 | return $contents; 55 | } catch (\Exception $e) { 56 | } 57 | return ''; 58 | } 59 | 60 | public function close() 61 | { 62 | if (isset($this->stream)) { 63 | if (is_resource($this->stream)) { 64 | fclose($this->stream); 65 | } 66 | $this->detach(); 67 | } 68 | } 69 | 70 | public function detach() 71 | { 72 | if (!$this->stream) { 73 | return null; 74 | } 75 | $result = $this->stream; 76 | unset($this->stream); 77 | $this->size = $this->uri = null; 78 | $this->readable = $this->writable = $this->seekable = null; 79 | return $result; 80 | } 81 | 82 | public function getSize() 83 | { 84 | if ($this->size !== null) { 85 | return $this->size; 86 | } 87 | if (!$this->stream) { 88 | return null; 89 | } 90 | if ($this->uri) { 91 | clearstatcache(true, $this->uri); 92 | } 93 | $stats = fstat($this->stream); 94 | if (isset($stats['size'])) { 95 | $this->size = $stats['size']; 96 | return $this->size; 97 | } 98 | return null; 99 | } 100 | 101 | public function tell() 102 | { 103 | $result = ftell($this->stream); 104 | if ($result === false) { 105 | throw new \RuntimeException('Unable to determine stream position'); 106 | } 107 | return $result; 108 | } 109 | 110 | public function eof() 111 | { 112 | return !$this->stream || feof($this->stream); 113 | } 114 | 115 | public function isSeekable() 116 | { 117 | return $this->seekable; 118 | } 119 | 120 | public function seek($offset, $whence = SEEK_SET) 121 | { 122 | if (!$this->seekable) { 123 | throw new \RuntimeException('Stream is not seekable'); 124 | } elseif (fseek($this->stream, $offset, $whence) === -1) { 125 | throw new \RuntimeException('Unable to seek to stream position ' 126 | . $offset . ' with whence ' . var_export($whence, true)); 127 | } 128 | } 129 | 130 | public function rewind() 131 | { 132 | $this->seek(0); 133 | } 134 | 135 | public function isWritable() 136 | { 137 | return $this->writable; 138 | } 139 | 140 | public function write($string) 141 | { 142 | if (!$this->writable) { 143 | throw new \RuntimeException('Cannot write to a non-writable stream'); 144 | } 145 | $this->size = null; 146 | $result = fwrite($this->stream, $string); 147 | if ($result === false) { 148 | throw new \RuntimeException('Unable to write to stream'); 149 | } 150 | return $result; 151 | } 152 | 153 | public function isReadable() 154 | { 155 | return $this->readable; 156 | } 157 | 158 | public function read($length) 159 | { 160 | if (!$this->readable) { 161 | throw new \RuntimeException('Cannot read from non-readable stream'); 162 | } 163 | if ($length < 0) { 164 | throw new \RuntimeException('Length parameter cannot be negative'); 165 | } 166 | if (0 === $length) { 167 | return ''; 168 | } 169 | $string = fread($this->stream, $length); 170 | if (false === $string) { 171 | throw new \RuntimeException('Unable to read from stream'); 172 | } 173 | 174 | return $string; 175 | } 176 | 177 | public function getContents() 178 | { 179 | $contents = stream_get_contents($this->stream); 180 | if ($contents === false) { 181 | throw new \RuntimeException('Unable to read stream contents'); 182 | } 183 | return $contents; 184 | } 185 | 186 | public function getMetadata($key = null) 187 | { 188 | if (!$this->stream) { 189 | return $key ? '' : []; 190 | } elseif (!$key) { 191 | return stream_get_meta_data($this->stream); 192 | } 193 | $meta = stream_get_meta_data($this->stream); 194 | return isset($meta[$key]) ? $meta[$key] : null; 195 | } 196 | 197 | public function __destruct() 198 | { 199 | $this->close(); 200 | } 201 | } -------------------------------------------------------------------------------- /src/Core/Lib/Strings.php: -------------------------------------------------------------------------------- 1 | 8 | * @package Core\Lib 9 | */ 10 | class Strings 11 | { 12 | 13 | /** 14 | * 生成一个token 15 | * 16 | * @return string 17 | */ 18 | public static function makeToken() 19 | { 20 | return md5(uniqid(microtime(true), true)); 21 | } 22 | 23 | /** 24 | * 产生随机字符串 25 | * 26 | * @param int $length 长度 27 | * @param string $string 包含的字符列表,必须是ASCII字符 28 | * @return string 29 | */ 30 | public static function random($length, $string = '23456789ABCDEFGHIJKLMNPQRSTUVWXYZabcdefghijkmnpqrstuvwxyz') 31 | { 32 | return substr(str_shuffle(str_repeat($string, $length < strlen($string) ? 1 : ceil($length / strlen($string)))), 0, $length); 33 | } 34 | 35 | /** 36 | * 字符串截取 37 | * 38 | * @param string $str 39 | * @param int $len 40 | * @param string $dot 41 | * @param string $encoding 42 | * @return string 43 | */ 44 | public static function truncate($str, $len = 0, $dot = '...', $encoding = CHARSET) 45 | { 46 | if (!$len || strlen($str) <= $len) return $str; 47 | $tempStr = ''; 48 | $pre = $end = chr(1); 49 | $str = str_replace(['&', '"', '<', '>'], [$pre . '&' . $end, $pre . '"' . $end, $pre . '<' . $end, $pre . '>' . $end], $str); 50 | $encoding = strtolower($encoding); 51 | if ($encoding == 'utf-8') { 52 | $n = $tn = $noc = 0; 53 | while ($n < strlen($str)) { 54 | $t = ord($str[$n]); 55 | if ($t == 9 || $t == 10 || (32 <= $t && $t <= 126)) { 56 | $tn = 1; 57 | $n++; 58 | $noc++; 59 | } elseif (194 <= $t && $t <= 223) { 60 | $tn = 2; 61 | $n += 2; 62 | $noc += 2; 63 | } elseif (224 <= $t && $t < 239) { 64 | $tn = 3; 65 | $n += 3; 66 | $noc += 2; 67 | } elseif (240 <= $t && $t <= 247) { 68 | $tn = 4; 69 | $n += 4; 70 | $noc += 2; 71 | } elseif (248 <= $t && $t <= 251) { 72 | $tn = 5; 73 | $n += 5; 74 | $noc += 2; 75 | } elseif ($t == 252 || $t == 253) { 76 | $tn = 6; 77 | $n += 6; 78 | $noc += 2; 79 | } else { 80 | $n++; 81 | } 82 | if ($noc >= $len) { 83 | break; 84 | } 85 | } 86 | if ($noc > $len) { 87 | $n -= $tn; 88 | } 89 | $tempStr = substr($str, 0, $n); 90 | } elseif ($encoding == 'gbk') { 91 | for ($i = 0; $i < $len; $i++) { 92 | $tempStr .= ord($str{$i}) > 127 ? $str{$i} . $str{++$i} : $str{$i}; 93 | } 94 | } 95 | $tempStr = str_replace([$pre . '&' . $end, $pre . '"' . $end, $pre . '<' . $end, $pre . '>' . $end], ['&', '"', '<', '>'], $tempStr); 96 | return $tempStr . $dot; 97 | } 98 | 99 | /** 100 | * 字符串过滤 101 | * 102 | * @param string $string 103 | * @param boolean $isurl 104 | * @return string 105 | */ 106 | public static function safeStr($string, $isurl = false) 107 | { 108 | $string = preg_replace('/[\\x00-\\x08\\x0B\\x0C\\x0E-\\x1F]/', '', $string); 109 | $string = str_replace(["\0", "%00", "\r"], '', $string); 110 | empty($isurl) && $string = preg_replace("/&(?!(#[0-9]+|[a-z]+);)/si", '&', $string); 111 | $string = str_replace(["%3C", '<'], '<', $string); 112 | $string = str_replace(["%3E", '>'], '>', $string); 113 | $string = str_replace(['"', "'", "\t", ' '], ['"', ''', ' ', '  '], $string); 114 | return trim($string); 115 | } 116 | 117 | /** 118 | * 数组元素串接 119 | * 120 | * 将数组的每个元素用逗号连接起来,并给每个元素添加单引号,用于SQL语句中串接ID 121 | * @param array $array 122 | * @return string 123 | */ 124 | public static function simplode($array) 125 | { 126 | return "'" . implode("','", $array) . "'"; 127 | } 128 | 129 | /** 130 | * 为特殊字符加上反斜杠 131 | * 132 | * 与addslashes不同之处在于本函数支持数组 133 | * 134 | * @param string|array $input 135 | * @return string|array 返回处理后的变量 136 | */ 137 | public static function addSlashes($input) 138 | { 139 | if (!is_array($input)) return addslashes($input); 140 | foreach ($input as $key => $val) { 141 | $input[$key] = static::addSlashes($val); 142 | } 143 | return $input; 144 | } 145 | 146 | /** 147 | * 去除特殊字符的反斜杠 148 | * 149 | * 与stripslashes不同之处在于本函数支持数组 150 | * 151 | * @param string|array $input 152 | * @return string|array 返回处理后的变量 153 | */ 154 | public static function delSlashes($input) 155 | { 156 | if (!is_array($input)) return stripslashes($input); 157 | foreach ($input as $key => $val) { 158 | $input[$key] = static::delSlashes($val); 159 | } 160 | return $input; 161 | } 162 | 163 | /** 164 | * 计算字符串长度,一个汉字为1 165 | * 166 | * @param string $string 167 | * @return int 168 | */ 169 | public static function len($string) 170 | { 171 | return mb_strlen($string, CHARSET); 172 | } 173 | 174 | /** 175 | * base64编码为可用于URL参数形式 176 | * 177 | * @param string $string 178 | * @return string 179 | */ 180 | public static function base64EncodeURL($string) 181 | { 182 | return str_replace(['+', '/'], ['-', '_'], rtrim(base64_encode($string), '=')); 183 | } 184 | 185 | /** 186 | * 解码URL形式base64 187 | * 188 | * @param string $string 189 | * @return string 190 | */ 191 | public static function base64DecodeURL($string) 192 | { 193 | return base64_decode(str_replace(['-','_'], ['+', '/'], $string)); 194 | } 195 | 196 | /** 197 | * 检查字符串编码是否是UTF8 198 | * 199 | * @param string $value 200 | * @return bool 201 | */ 202 | public static function isUTF8($value) 203 | { 204 | return $value === '' || preg_match('/^./su', $value) === 1; 205 | } 206 | } 207 | --------------------------------------------------------------------------------