diff --git a/.travis.yml b/.travis.yml index 94de5bee..96a9dffc 100644 --- a/.travis.yml +++ b/.travis.yml @@ -37,13 +37,13 @@ script: ## LINT - find . -path ./vendor -prune -o -type f -name \*.php -exec php -l {} \; ## PHP_CodeSniffer - - THINK_AUTOLOAD=0 vendor/bin/phpcs --runtime-set ignore_errors_on_exit 1 --runtime-set ignore_warnings_on_exit 1 --standard=PSR2 --ignore="vendor/*" ./ + - vendor/bin/phpcs --runtime-set ignore_errors_on_exit 1 --runtime-set ignore_warnings_on_exit 1 --standard=PSR2 --ignore="vendor/*" ./ ## PHP Copy/Paste Detector - - THINK_AUTOLOAD=0 vendor/bin/phpcpd --verbose --exclude vendor ./ || true + - vendor/bin/phpcpd --verbose --exclude vendor ./ || true ## PHPLOC - - THINK_AUTOLOAD=0 vendor/bin/phploc --exclude vendor ./ + - vendor/bin/phploc --exclude vendor ./ ## PHPUNIT - - THINK_AUTOLOAD=0 vendor/bin/phpunit --coverage-clover=coverage.xml --configuration=phpunit.xml + - vendor/bin/phpunit --coverage-clover=coverage.xml --configuration=phpunit.xml after_success: - bash <(curl -s https://codecov.io/bash) diff --git a/README.md b/README.md index f4a0a628..ecbb33c9 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -ThinkPHP 5.0.0 RC3 +ThinkPHP 5.0 RC4 =============== [![Build Status](https://img.shields.io/travis/top-think/framework.svg)](https://travis-ci.org/top-think/framework) @@ -39,12 +39,8 @@ ThinkPHP5在保持快速开发和大道至简的核心理念不变的同时,PH ~~~ www WEB部署目录(或者子目录) -├─composer.json composer定义文件 -├─README.md README文件 -├─LICENSE.txt 授权说明文件 ├─application 应用目录 │ ├─common 公共模块目录(可以更改) -│ ├─runtime 应用的运行时目录(可写,可定制) │ ├─module_name 模块目录 │ │ ├─config.php 模块配置文件 │ │ ├─common.php 模块函数文件 @@ -53,6 +49,7 @@ www WEB部署目录(或者子目录) │ │ ├─view 视图目录 │ │ └─ ... 更多类库目录 │ │ +│ ├─command.php 命令行工具配置文件 │ ├─common.php 公共函数文件 │ ├─config.php 公共配置文件 │ ├─route.php 路由配置文件 @@ -60,6 +57,7 @@ www WEB部署目录(或者子目录) │ ├─public WEB目录(对外访问目录) │ ├─index.php 入口文件 +│ ├─router.php 快速测试文件 │ └─.htaccess 用于apache的重写 │ ├─thinkphp 框架系统目录 @@ -68,17 +66,22 @@ www WEB部署目录(或者子目录) │ │ ├─think Think类库包目录 │ │ └─traits 系统Trait目录 │ │ -│ ├─mode 应用模式目录 │ ├─tpl 系统模板目录 -│ ├─tests 单元测试文件目录 │ ├─base.php 基础定义文件 +│ ├─console.php 控制台入口文件 │ ├─convention.php 框架惯例配置文件 │ ├─helper.php 助手函数文件 │ ├─phpunit.xml phpunit配置文件 │ └─start.php 框架入口文件 │ ├─extend 扩展类库目录 +├─runtime 应用的运行时目录(可写,可定制) ├─vendor 第三方类库目录(Composer依赖库) +├─build.php 自动生成定义文件(参考) +├─composer.json composer 定义文件 +├─LICENSE.txt 授权说明文件 +├─README.md README 文件 +├─think 命令行入口文件 ~~~ > router.php用于php自带webserver支持,可用于快速测试 @@ -87,28 +90,7 @@ www WEB部署目录(或者子目录) ## 命名规范 -ThinkPHP5的命名规范如下: - -### 目录和文件 - -* 目录不强制规范,驼峰和小写+下划线模式均支持; -* 类库、函数文件统一以`.php`为后缀; -* 类的文件名均以命名空间定义,并且命名空间的路径和类库文件所在路径一致; -* 类名和类文件名保持一致,统一采用驼峰法命名(首字母大写); - -### 函数和类、属性命名 -* 类的命名采用驼峰法,并且首字母大写,例如 `User`、`UserType`,不需要添加后缀,例如UserController应该直接命名为User; -* 函数的命名使用小写字母和下划线(小写字母开头)的方式,例如 `get_client_ip`; -* 方法的命名使用驼峰法,并且首字母小写或者使用下划线“_”,例如 `getUserName`,`_parseType`,通常下划线开头的方法属于私有方法; -* 属性的命名使用驼峰法,并且首字母小写或者使用下划线“_”,例如 `tableName`、`_instance`,通常下划线开头的属性属于私有属性; -* 以双下划线“__”打头的函数或方法作为魔法方法,例如 `__call` 和 `__autoload`; - -### 常量和配置 -* 常量以大写字母和下划线命名,例如 `APP_DEBUG`和 `APP_MODE`; -* 配置参数以小写字母和下划线命名,例如 `url_route_on`; - -### 数据表和字段 -* 数据表和字段采用小写加下划线方式命名,并注意字段名不要以下划线开头,例如 think_user 表和 user_name字段,类似 _username 这样的数据表字段可能会被过滤。 +ThinkPHP5的命名规范遵循PSR-2规范以及PSR-4自动加载规范。 ## 参与开发 注册并登录 Github 帐号, fork 本项目并进行改动。 diff --git a/base.php b/base.php index 41bdd7dd..66d53e9f 100644 --- a/base.php +++ b/base.php @@ -9,53 +9,53 @@ // | Author: liu21st // +---------------------------------------------------------------------- -// 开始运行时间和内存使用 -define('START_TIME', microtime(true)); -define('START_MEM', memory_get_usage()); -// 版本信息 -define('THINK_VERSION', '5.0.0 RC3'); -// 系统常量 -defined('DS') or define('DS', DIRECTORY_SEPARATOR); -defined('THINK_PATH') or define('THINK_PATH', dirname(__FILE__) . DS); +define('THINK_VERSION', '5.0.0 RC4'); +define('THINK_START_TIME', number_format(microtime(true), 8, '.', '')); +define('THINK_START_MEM', memory_get_usage()); +define('EXT', '.php'); +define('DS', DIRECTORY_SEPARATOR); +defined('THINK_PATH') or define('THINK_PATH', __DIR__ . DS); +define('LIB_PATH', THINK_PATH . 'library' . DS); +define('CORE_PATH', LIB_PATH . 'think' . DS); +define('TRAIT_PATH', LIB_PATH . 'traits' . DS); defined('APP_PATH') or define('APP_PATH', dirname($_SERVER['SCRIPT_FILENAME']) . DS); defined('ROOT_PATH') or define('ROOT_PATH', dirname(APP_PATH) . DS); -defined('LIB_PATH') or define('LIB_PATH', THINK_PATH . 'library' . DS); defined('EXTEND_PATH') or define('EXTEND_PATH', ROOT_PATH . 'extend' . DS); -defined('MODE_PATH') or define('MODE_PATH', THINK_PATH . 'mode' . DS); // 系统应用模式目录 -defined('CORE_PATH') or define('CORE_PATH', LIB_PATH . 'think' . DS); -defined('TRAIT_PATH') or define('TRAIT_PATH', LIB_PATH . 'traits' . DS); -defined('APP_NAMESPACE') or define('APP_NAMESPACE', 'app'); -defined('COMMON_MODULE') or define('COMMON_MODULE', 'common'); +defined('VENDOR_PATH') or define('VENDOR_PATH', ROOT_PATH . 'vendor' . DS); defined('RUNTIME_PATH') or define('RUNTIME_PATH', ROOT_PATH . 'runtime' . DS); defined('LOG_PATH') or define('LOG_PATH', RUNTIME_PATH . 'log' . DS); defined('CACHE_PATH') or define('CACHE_PATH', RUNTIME_PATH . 'cache' . DS); defined('TEMP_PATH') or define('TEMP_PATH', RUNTIME_PATH . 'temp' . DS); -defined('VENDOR_PATH') or define('VENDOR_PATH', ROOT_PATH . 'vendor' . DS); -defined('EXT') or define('EXT', '.php'); +defined('CONF_PATH') or define('CONF_PATH', APP_PATH); // 配置文件目录 defined('CONF_EXT') or define('CONF_EXT', EXT); // 配置文件后缀 -defined('MODEL_LAYER') or define('MODEL_LAYER', 'model'); -defined('VIEW_LAYER') or define('VIEW_LAYER', 'view'); -defined('CONTROLLER_LAYER') or define('CONTROLLER_LAYER', 'controller'); -defined('VALIDATE_LAYER') or define('VALIDATE_LAYER', 'validate'); -defined('APP_MULTI_MODULE') or define('APP_MULTI_MODULE', true); // 是否多模块 defined('ENV_PREFIX') or define('ENV_PREFIX', 'PHP_'); // 环境变量的配置前缀 -defined('IS_API') or define('IS_API', false); // 是否API接口 -defined('APP_AUTO_RUN') or define('APP_AUTO_RUN', true); // 是否自动运行 -defined('APP_ROUTE_ON') or define('APP_ROUTE_ON', true); // 是否允许路由 -defined('APP_ROUTE_MUST') or define('APP_ROUTE_MUST', true); // 是否严格检查路由 -defined('CLASS_APPEND_SUFFIX') or define('CLASS_APPEND_SUFFIX', false); // 是否追加类名后缀 -// 应用模式 默认为普通模式 -defined('APP_MODE') or define('APP_MODE', function_exists('saeAutoLoader') ? 'sae' : 'common'); // 环境常量 -define('IS_CGI', strpos(PHP_SAPI, 'cgi') === 0 ? 1 : 0); -define('IS_WIN', strstr(PHP_OS, 'WIN') ? 1 : 0); -define('IS_MAC', strstr(PHP_OS, 'Darwin') ? 1 : 0); -define('IS_CLI', PHP_SAPI == 'cli' ? 1 : 0); -define('IS_AJAX', (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) == 'xmlhttprequest') ? true : false); -define('NOW_TIME', $_SERVER['REQUEST_TIME']); -define('REQUEST_METHOD', IS_CLI ? 'GET' : $_SERVER['REQUEST_METHOD']); -define('IS_GET', REQUEST_METHOD == 'GET' ? true : false); -define('IS_POST', REQUEST_METHOD == 'POST' ? true : false); -define('IS_PUT', REQUEST_METHOD == 'PUT' ? true : false); -define('IS_DELETE', REQUEST_METHOD == 'DELETE' ? true : false); +define('IS_CLI', PHP_SAPI == 'cli' ? true : false); +define('IS_WIN', strstr(PHP_OS, 'WIN') ? true : false); + +// 载入Loader类 +require CORE_PATH . 'Loader.php'; + +// 加载环境变量配置文件 +if (is_file(ROOT_PATH . 'env' . EXT)) { + $env = include ROOT_PATH . 'env' . EXT; + foreach ($env as $key => $val) { + $name = ENV_PREFIX . strtoupper($key); + if (is_bool($val)) { + $val = $val ? 1 : 0; + } elseif (!is_scalar($val)) { + continue; + } + putenv("$name=$val"); + } +} + +// 注册自动加载 +\think\Loader::register(); + +// 注册错误和异常处理机制 +\think\Error::register(); + +// 加载模式配置文件 +\think\Config::set(include THINK_PATH . 'convention' . EXT); diff --git a/composer.json b/composer.json index 6a23e52a..5d3e5b43 100644 --- a/composer.json +++ b/composer.json @@ -20,12 +20,12 @@ "topthink/think-installer": "*" }, "require-dev": { + "phpunit/phpunit": "4.8.*", "johnkary/phpunit-speedtrap": "^1.0", "mikey179/vfsStream": "~1.6", - "phploc/phploc": "*", - "phpunit/phpunit": "4.8.*", - "sebastian/phpcpd": "*", - "squizlabs/php_codesniffer": "2.*" - }, - "minimum-stability": "dev" + "phploc/phploc": "2.*", + "sebastian/phpcpd": "2.*", + "squizlabs/php_codesniffer": "2.*", + "phpdocumentor/reflection-docblock": "^2.0" + } } diff --git a/mode/console/Error.php b/console.php similarity index 71% rename from mode/console/Error.php rename to console.php index a0537e10..5947d3fe 100644 --- a/mode/console/Error.php +++ b/console.php @@ -1,6 +1,6 @@ 'app', + // 应用调试模式 + 'app_debug' => true, + // 应用Trace + 'app_trace' => false, // 应用模式状态 'app_status' => '', + // 是否支持多模块 + 'app_multi_module' => true, // 注册的根命名空间 'root_namespace' => [], // 扩展配置文件 @@ -15,12 +23,8 @@ return [ 'extra_file_list' => [THINK_PATH . 'helper' . EXT], // 默认输出类型 'default_return_type' => 'html', - // 默认语言 - 'default_lang' => 'zh-cn', - // response是否返回方式 - 'response_return' => false, - // 默认AJAX 数据返回格式,可选JSON XML ... - 'default_ajax_return' => 'JSON', + // 默认AJAX 数据返回格式,可选json xml ... + 'default_ajax_return' => 'json', // 默认JSONP格式返回的处理方法 'default_jsonp_handler' => 'jsonpReturn', // 默认JSONP处理方法 @@ -31,8 +35,12 @@ return [ 'lang_switch_on' => false, // 默认全局过滤方法 用逗号分隔多个 'default_filter' => '', - // 是否启用控制器类后缀 - 'use_controller_suffix' => false, + // 默认语言 + 'default_lang' => 'zh-cn', + // 应用类库后缀 + 'class_suffix' => false, + // 控制器类后缀 + 'controller_suffix' => false, // +---------------------------------------------------------------------- // | 模块设置 @@ -41,7 +49,7 @@ return [ // 默认模块名 'default_module' => 'index', // 禁止访问模块 - 'deny_module_list' => [COMMON_MODULE], + 'deny_module_list' => ['common'], // 默认控制器名 'default_controller' => 'Index', // 默认操作名 @@ -52,6 +60,8 @@ return [ 'empty_controller' => 'Error', // 操作方法后缀 'action_suffix' => '', + // 自动搜索控制器 + 'controller_auto_search' => false, // +---------------------------------------------------------------------- // | URL设置 @@ -63,33 +73,31 @@ return [ 'pathinfo_fetch' => ['ORIG_PATH_INFO', 'REDIRECT_PATH_INFO', 'REDIRECT_URL'], // pathinfo分隔符 'pathinfo_depr' => '/', - // 获取当前页面地址的系统变量 默认为REQUEST_URI - 'url_request_uri' => 'REQUEST_URI', - // 基础URL路径 - 'base_url' => $_SERVER["SCRIPT_NAME"], // URL伪静态后缀 - 'url_html_suffix' => '.html', + 'url_html_suffix' => 'html', // URL普通方式参数 用于自动生成 'url_common_param' => false, //url禁止访问的后缀 'url_deny_suffix' => 'ico|png|gif|jpg', + // URL参数方式 0 按名称成对解析 1 按顺序解析 + 'url_param_type' => 0, // 是否开启路由 'url_route_on' => true, // 是否强制使用路由 'url_route_must' => false, - // URL模块映射 - 'url_module_map' => [], // 域名部署 'url_domain_deploy' => false, // 域名根,如.thinkphp.cn 'url_domain_root' => '', - // 是否自动转换URL中的控制器名 - 'url_controller_convert' => true, - // 是否自动转换URL中的操作名 - 'url_action_convert' => true, + // 是否自动转换URL中的控制器和操作名 + 'url_convert' => true, + // 默认的访问控制器层 + 'url_controller_layer' => 'controller', + // 表单请求类型伪装变量 + 'var_method' => '_method', // +---------------------------------------------------------------------- - // | 模板引擎设置 + // | 模板设置 // +---------------------------------------------------------------------- 'template' => [ @@ -98,7 +106,7 @@ return [ // 模板路径 'view_path' => '', // 模板后缀 - 'view_suffix' => '.html', + 'view_suffix' => 'html', // 模板文件名分隔符 'view_depr' => DS, // 模板引擎普通标签开始标记 @@ -123,25 +131,33 @@ return [ // 异常页面的模板文件 'exception_tmpl' => THINK_PATH . 'tpl' . DS . 'think_exception.tpl', - // 异常处理忽略的错误类型,支持PHP所有的错误级别常量,多个级别可以用|运算法 - // 参考:http://php.net/manual/en/errorfunc.constants.php - 'exception_ignore_type' => 0, + // 错误显示信息,非调试模式有效 'error_message' => '页面错误!请稍后再试~', - // 错误定向页面 - 'error_page' => '', // 显示错误信息 'show_error_msg' => false, + // 异常处理handle类 留空使用 \think\exception\Handle + 'exception_handle' => '', // +---------------------------------------------------------------------- // | 日志设置 // +---------------------------------------------------------------------- 'log' => [ - // 日志记录方式,支持 file socket trace sae - 'type' => 'File', + // 日志记录方式,内置 file socket 支持扩展 + 'type' => 'File', // 日志保存目录 - 'path' => LOG_PATH, + 'path' => LOG_PATH, + // 日志记录级别 + 'level' => [], + ], + + // +---------------------------------------------------------------------- + // | Trace设置 开启 app_trace 后 有效 + // +---------------------------------------------------------------------- + 'trace' => [ + // 内置Html Console 支持扩展 + 'type' => 'Html', ], // +---------------------------------------------------------------------- @@ -175,46 +191,72 @@ return [ 'auto_start' => true, ], + // +---------------------------------------------------------------------- + // | Cookie设置 + // +---------------------------------------------------------------------- + 'cookie' => [ + // cookie 名称前缀 + 'prefix' => '', + // cookie 保存时间 + 'expire' => 0, + // cookie 保存路径 + 'path' => '/', + // cookie 有效域名 + 'domain' => '', + // cookie 启用安全传输 + 'secure' => false, + // httponly设置 + 'httponly' => '', + // 是否使用 setcookie + 'setcookie' => true, + ], + // +---------------------------------------------------------------------- // | 数据库设置 // +---------------------------------------------------------------------- 'database' => [ // 数据库类型 - 'type' => 'mysql', + 'type' => 'mysql', // 数据库连接DSN配置 - 'dsn' => '', + 'dsn' => '', // 服务器地址 - 'hostname' => 'localhost', + 'hostname' => 'localhost', // 数据库名 - 'database' => '', + 'database' => '', // 数据库用户名 - 'username' => 'root', + 'username' => 'root', // 数据库密码 - 'password' => '', + 'password' => '', // 数据库连接端口 - 'hostport' => '', + 'hostport' => '', // 数据库连接参数 - 'params' => [], + 'params' => [], // 数据库编码默认采用utf8 - 'charset' => 'utf8', + 'charset' => 'utf8', // 数据库表前缀 - 'prefix' => '', + 'prefix' => '', // 数据库调试模式 - 'debug' => false, + 'debug' => false, // 数据库部署方式:0 集中式(单一服务器),1 分布式(主从服务器) - 'deploy' => 0, + 'deploy' => 0, // 数据库读写是否分离 主从式有效 - 'rw_separate' => false, + 'rw_separate' => false, // 读写分离后 主服务器数量 - 'master_num' => 1, + 'master_num' => 1, // 指定从服务器序号 - 'slave_no' => '', + 'slave_no' => '', + // 是否严格检查字段是否存在 + 'fields_strict' => true, + // 自动写入时间戳字段 + 'auto_timestamp' => false, ], + //分页配置 'paginate' => [ - 'type' => 'bootstrap', - 'var_page' => 'page', + 'type' => 'bootstrap', + 'var_page' => 'page', + 'list_rows' => 15, ], ]; diff --git a/helper.php b/helper.php index 7db8de1d..16f3f0ed 100644 --- a/helper.php +++ b/helper.php @@ -18,367 +18,490 @@ use think\Config; use think\Cookie; use think\Db; use think\Debug; -use think\Input; use think\Lang; use think\Loader; use think\Log; use think\Request; use think\Response; -use think\Route; use think\Session; use think\Url; use think\View; -/** - * 快速导入Traits PHP5.5以上无需调用 - * @param string $class trait库 - * @param string $ext 类库后缀 - * @return boolean - */ -function load_trait($class, $ext = EXT) -{ - return Loader::import($class, TRAIT_PATH, $ext); -} - -/** - * 抛出异常处理 - * - * @param string $msg 异常消息 - * @param integer $code 异常代码 默认为0 - * @param string $exception 异常类 - * - * @throws Exception - */ -function exception($msg, $code = 0, $exception = '') -{ - $e = $exception ?: '\think\Exception'; - throw new $e($msg, $code); -} - -/** - * 记录时间(微秒)和内存使用情况 - * @param string $start 开始标签 - * @param string $end 结束标签 - * @param integer|string $dec 小数位 如果是m 表示统计内存占用 - * @return mixed - */ -function debug($start, $end = '', $dec = 6) -{ - if ('' == $end) { - Debug::remark($start); - } else { - return 'm' == $dec ? Debug::getRangeMem($start, $end) : Debug::getRangeTime($start, $end, $dec); +if (!function_exists('load_trait')) { + /** + * 快速导入Traits PHP5.5以上无需调用 + * @param string $class trait库 + * @param string $ext 类库后缀 + * @return boolean + */ + function load_trait($class, $ext = EXT) + { + return Loader::import($class, TRAIT_PATH, $ext); } } -/** - * 获取语言变量值 - * @param string $name 语言变量名 - * @param array $vars 动态变量值 - * @param string $lang 语言 - * @return mixed - */ -function lang($name, $vars = [], $lang = '') -{ - return Lang::get($name, $vars, $lang); -} - -/** - * 获取和设置配置参数 - * @param string $name 参数名 - * @param mixed $value 参数值 - * @param string $range 作用域 - * @return mixed - */ -function config($name = '', $value = null, $range = '') -{ - if (is_null($value) && is_string($name)) { - return Config::get($name, $range); - } else { - return Config::set($name, $value, $range); +if (!function_exists('exception')) { + /** + * 抛出异常处理 + * + * @param string $msg 异常消息 + * @param integer $code 异常代码 默认为0 + * @param string $exception 异常类 + * + * @throws Exception + */ + function exception($msg, $code = 0, $exception = '') + { + $e = $exception ?: '\think\Exception'; + throw new $e($msg, $code); } } -/** - * 获取输入数据 支持默认值和过滤 - * @param string $key 获取的变量名 - * @param mixed $default 默认值 - * @param string $filter 过滤方法 - * @param bool $merge 是否合并系统默认过滤方法 - * @return mixed - */ -function input($key, $default = null, $filter = null, $merge = false) -{ - if (0 === strpos($key, '?')) { - $key = substr($key, 1); - $has = '?'; - } else { - $has = ''; - } - if ($pos = strpos($key, '.')) { - // 指定参数来源 - $method = substr($key, 0, $pos); - if (in_array($method, ['get', 'post', 'put', 'delete', 'param', 'request', 'session', 'cookie', 'server', 'globals', 'env', 'path', 'file'])) { - $key = substr($key, $pos + 1); +if (!function_exists('debug')) { + /** + * 记录时间(微秒)和内存使用情况 + * @param string $start 开始标签 + * @param string $end 结束标签 + * @param integer|string $dec 小数位 如果是m 表示统计内存占用 + * @return mixed + */ + function debug($start, $end = '', $dec = 6) + { + if ('' == $end) { + Debug::remark($start); } else { + return 'm' == $dec ? Debug::getRangeMem($start, $end) : Debug::getRangeTime($start, $end, $dec); + } + } +} + +if (!function_exists('lang')) { + /** + * 获取语言变量值 + * @param string $name 语言变量名 + * @param array $vars 动态变量值 + * @param string $lang 语言 + * @return mixed + */ + function lang($name, $vars = [], $lang = '') + { + return Lang::get($name, $vars, $lang); + } +} + +if (!function_exists('config')) { + /** + * 获取和设置配置参数 + * @param string|array $name 参数名 + * @param mixed $value 参数值 + * @param string $range 作用域 + * @return mixed + */ + function config($name = '', $value = null, $range = '') + { + if (is_null($value) && is_string($name)) { + return Config::get($name, $range); + } else { + return Config::set($name, $value, $range); + } + } +} + +if (!function_exists('input')) { + /** + * 获取输入数据 支持默认值和过滤 + * @param string $key 获取的变量名 + * @param mixed $default 默认值 + * @param string $filter 过滤方法 + * @return mixed + */ + function input($key, $default = null, $filter = null) + { + if (0 === strpos($key, '?')) { + $key = substr($key, 1); + $has = true; + } + if ($pos = strpos($key, '.')) { + // 指定参数来源 + $method = substr($key, 0, $pos); + if (in_array($method, ['get', 'post', 'put', 'delete', 'param', 'request', 'session', 'cookie', 'server', 'env', 'path', 'file'])) { + $key = substr($key, $pos + 1); + } else { + $method = 'param'; + } + } else { + // 默认为自动判断 $method = 'param'; } - } else { - // 默认为自动判断 - $method = 'param'; - } - return Input::$method($has . $key, $default, $filter, $merge); -} - -/** - * 渲染输出Widget - * @param string $name Widget名称 - * @param array $data 传人的参数 - * @return mixed - */ -function widget($name, $data = []) -{ - return Loader::action($name, $data, 'widget'); -} - -/** - * 实例化Model - * @param string $name Model名称 - * @param string $layer 业务层名称 - * @return \think\Model - */ -function model($name = '', $layer = MODEL_LAYER) -{ - return Loader::model($name, $layer); -} - -/** - * 实例化数据库类 - * @param string $name 操作的数据表名称(不含前缀) - * @param array|string $config 数据库配置参数 - * @return \think\db\Connection - */ -function db($name = '', $config = []) -{ - return Db::connect($config)->name($name); -} - -/** - * 实例化控制器 格式:[模块/]控制器 - * @param string $name 资源地址 - * @param string $layer 控制层名称 - * @return \think\Controller - */ -function controller($name, $layer = CONTROLLER_LAYER) -{ - return Loader::controller($name, $layer); -} - -/** - * 调用模块的操作方法 参数格式 [模块/控制器/]操作 - * @param string $url 调用地址 - * @param string|array $vars 调用参数 支持字符串和数组 - * @param string $layer 要调用的控制层名称 - * @return mixed - */ -function action($url, $vars = [], $layer = CONTROLLER_LAYER) -{ - return Loader::action($url, $vars, $layer); -} - -/** - * 导入所需的类库 同java的Import 本函数有缓存功能 - * @param string $class 类库命名空间字符串 - * @param string $baseUrl 起始路径 - * @param string $ext 导入的文件扩展名 - * @return boolean - */ -function import($class, $baseUrl = '', $ext = EXT) -{ - return Loader::import($class, $baseUrl, $ext); -} - -/** - * 快速导入第三方框架类库 所有第三方框架的类库文件统一放到 系统的Vendor目录下面 - * @param string $class 类库 - * @param string $ext 类库后缀 - * @return boolean - */ -function vendor($class, $ext = EXT) -{ - return Loader::import($class, VENDOR_PATH, $ext); -} - -/** - * 浏览器友好的变量输出 - * @param mixed $var 变量 - * @param boolean $echo 是否输出 默认为true 如果为false 则返回输出字符串 - * @param string $label 标签 默认为空 - * @return void|string - */ -function dump($var, $echo = true, $label = null) -{ - return Debug::dump($var, $echo, $label); -} - -/** - * Url生成 - * @param string $url 路由地址 - * @param string|array $value 变量 - * @param bool|string $suffix 前缀 - * @param bool|string $domain 域名 - * @return string - */ -function url($url = '', $vars = '', $suffix = true, $domain = false) -{ - return Url::build($url, $vars, $suffix, $domain); -} - -/** - * Session管理 - * @param string|array $name session名称,如果为数组表示进行session设置 - * @param mixed $value session值 - * @param string $prefix 前缀 - * @return mixed - */ -function session($name, $value = '', $prefix = null) -{ - if (is_array($name)) { - // 初始化 - Session::init($name); - } elseif (is_null($name)) { - // 清除 - Session::clear($value); - } elseif ('' === $value) { - // 判断或获取 - return 0 === strpos($name, '?') ? Session::has(substr($name, 1), $prefix) : Session::get($name, $prefix); - } elseif (is_null($value)) { - // 删除 - return Session::delete($name, $prefix); - } else { - // 设置 - return Session::set($name, $value, $prefix); - } -} - -/** - * Cookie管理 - * @param string|array $name cookie名称,如果为数组表示进行cookie设置 - * @param mixed $value cookie值 - * @param mixed $option 参数 - * @return mixed - */ -function cookie($name, $value = '', $option = null) -{ - if (is_array($name)) { - // 初始化 - Cookie::init($name); - } elseif (is_null($name)) { - // 清除 - Cookie::clear($value); - } elseif ('' === $value) { - // 获取 - return Cookie::get($name); - } elseif (is_null($value)) { - // 删除 - return Cookie::delete($name); - } else { - // 设置 - return Cookie::set($name, $value, $option); - } -} - -/** - * 缓存管理 - * @param mixed $name 缓存名称,如果为数组表示进行缓存设置 - * @param mixed $value 缓存值 - * @param mixed $options 缓存参数 - * @return mixed - */ -function cache($name, $value = '', $options = null) -{ - if (is_array($options)) { - // 缓存操作的同时初始化 - Cache::connect($options); - } elseif (is_array($name)) { - // 缓存初始化 - return Cache::connect($name); - } - if ('' === $value) { - // 获取缓存 - return Cache::get($name); - } elseif (is_null($value)) { - // 删除缓存 - return Cache::rm($name); - } else { - // 缓存数据 - if (is_array($options)) { - $expire = isset($options['expire']) ? $options['expire'] : null; //修复查询缓存无法设置过期时间 + if (isset($has)) { + return request()->has($key, $method, $default); } else { - $expire = is_numeric($options) ? $options : null; //默认快捷缓存设置过期时间 + return request()->$method($key, $default, $filter); } - return Cache::set($name, $value, $expire); } } -/** - * 记录日志信息 - * @param mixed $log log信息 支持字符串和数组 - * @param string $level 日志级别 - * @return void|array - */ -function trace($log = '[think]', $level = 'log') -{ - if ('[think]' === $log) { - return Log::getLog(); - } else { - Log::record($log, $level); +if (!function_exists('widget')) { + /** + * 渲染输出Widget + * @param string $name Widget名称 + * @param array $data 传人的参数 + * @return mixed + */ + function widget($name, $data = []) + { + return Loader::action($name, $data, 'widget'); } } -/** - * 渲染模板输出 - * @param string $template 模板文件 - * @param array $vars 模板变量 - * @return string - */ -function view($template = '', $vars = []) -{ - return View::instance(Config::get('template'), Config::get('view_replace_str'))->fetch($template, $vars); +if (!function_exists('model')) { + /** + * 实例化Model + * @param string $name Model名称 + * @param string $layer 业务层名称 + * @param bool $appendSuffix 是否添加类名后缀 + * @return \think\Model + */ + function model($name = '', $layer = 'model', $appendSuffix = false) + { + return Loader::model($name, $layer, $appendSuffix); + } } -/** - * 路由注册 - * @param string $rule 路由规则 - * @param mixed $route 路由地址 - * @param sting $type 请求类型 - * @param array $option 路由参数 - * @param array $pattern 变量规则 - * @return void - */ -function route($rule = '', $route = [], $type = '*', $option = [], $pattern = []) -{ - Route::register($rule, $route, $type, $option, $pattern); +if (!function_exists('validate')) { + /** + * 实例化验证器 + * @param string $name 验证器名称 + * @param string $layer 业务层名称 + * @param bool $appendSuffix 是否添加类名后缀 + * @return \think\Validate + */ + function validate($name = '', $layer = 'validate', $appendSuffix = false) + { + return Loader::validate($name, $layer, $appendSuffix); + } } -/** - * 获取Request参数 - * @param string $name 方法 - * @param mixed $param 参数 - * @return mixed - */ -function request($name, $param = '') -{ - return Request::instance()->$name($param); +if (!function_exists('db')) { + /** + * 实例化数据库类 + * @param string $name 操作的数据表名称(不含前缀) + * @param array|string $config 数据库配置参数 + * @return \think\db\Query + */ + function db($name = '', $config = []) + { + return Db::connect($config)->name($name); + } } -/** - * 设置Response输出 - * @param string $name 方法 - * @param mixed $param 参数 - * @return mixed - */ -function response($name, $param = '') -{ - return Response::$name($param); +if (!function_exists('controller')) { + /** + * 实例化控制器 格式:[模块/]控制器 + * @param string $name 资源地址 + * @param string $layer 控制层名称 + * @param bool $appendSuffix 是否添加类名后缀 + * @return \think\Controller + */ + function controller($name, $layer = 'controller', $appendSuffix = false) + { + return Loader::controller($name, $layer, $appendSuffix); + } +} + +if (!function_exists('action')) { + /** + * 调用模块的操作方法 参数格式 [模块/控制器/]操作 + * @param string $url 调用地址 + * @param string|array $vars 调用参数 支持字符串和数组 + * @param string $layer 要调用的控制层名称 + * @param bool $appendSuffix 是否添加类名后缀 + * @return mixed + */ + function action($url, $vars = [], $layer = 'controller', $appendSuffix = false) + { + return Loader::action($url, $vars, $layer, $appendSuffix); + } +} + +if (!function_exists('import')) { + /** + * 导入所需的类库 同java的Import 本函数有缓存功能 + * @param string $class 类库命名空间字符串 + * @param string $baseUrl 起始路径 + * @param string $ext 导入的文件扩展名 + * @return boolean + */ + function import($class, $baseUrl = '', $ext = EXT) + { + return Loader::import($class, $baseUrl, $ext); + } +} + +if (!function_exists('vendor')) { + /** + * 快速导入第三方框架类库 所有第三方框架的类库文件统一放到 系统的Vendor目录下面 + * @param string $class 类库 + * @param string $ext 类库后缀 + * @return boolean + */ + function vendor($class, $ext = EXT) + { + return Loader::import($class, VENDOR_PATH, $ext); + } +} + +if (!function_exists('dump')) { + /** + * 浏览器友好的变量输出 + * @param mixed $var 变量 + * @param boolean $echo 是否输出 默认为true 如果为false 则返回输出字符串 + * @param string $label 标签 默认为空 + * @return void|string + */ + function dump($var, $echo = true, $label = null) + { + return Debug::dump($var, $echo, $label); + } +} + +if (!function_exists('url')) { + /** + * Url生成 + * @param string $url 路由地址 + * @param string|array $value 变量 + * @param bool|string $suffix 前缀 + * @param bool|string $domain 域名 + * @return string + */ + function url($url = '', $vars = '', $suffix = true, $domain = false) + { + return Url::build($url, $vars, $suffix, $domain); + } +} + +if (!function_exists('session')) { + /** + * Session管理 + * @param string|array $name session名称,如果为数组表示进行session设置 + * @param mixed $value session值 + * @param string $prefix 前缀 + * @return mixed + */ + function session($name, $value = '', $prefix = null) + { + if (is_array($name)) { + // 初始化 + Session::init($name); + } elseif (is_null($name)) { + // 清除 + Session::clear($value); + } elseif ('' === $value) { + // 判断或获取 + return 0 === strpos($name, '?') ? Session::has(substr($name, 1), $prefix) : Session::get($name, $prefix); + } elseif (is_null($value)) { + // 删除 + return Session::delete($name, $prefix); + } else { + // 设置 + return Session::set($name, $value, $prefix); + } + } +} + +if (!function_exists('cookie')) { + /** + * Cookie管理 + * @param string|array $name cookie名称,如果为数组表示进行cookie设置 + * @param mixed $value cookie值 + * @param mixed $option 参数 + * @return mixed + */ + function cookie($name, $value = '', $option = null) + { + if (is_array($name)) { + // 初始化 + Cookie::init($name); + } elseif (is_null($name)) { + // 清除 + Cookie::clear($value); + } elseif ('' === $value) { + // 获取 + return Cookie::get($name); + } elseif (is_null($value)) { + // 删除 + return Cookie::delete($name); + } else { + // 设置 + return Cookie::set($name, $value, $option); + } + } +} + +if (!function_exists('cache')) { + /** + * 缓存管理 + * @param mixed $name 缓存名称,如果为数组表示进行缓存设置 + * @param mixed $value 缓存值 + * @param mixed $options 缓存参数 + * @return mixed + */ + function cache($name, $value = '', $options = null) + { + if (is_array($options)) { + // 缓存操作的同时初始化 + Cache::connect($options); + } elseif (is_array($name)) { + // 缓存初始化 + return Cache::connect($name); + } + if ('' === $value) { + // 获取缓存 + return Cache::get($name); + } elseif (is_null($value)) { + // 删除缓存 + return Cache::rm($name); + } else { + // 缓存数据 + if (is_array($options)) { + $expire = isset($options['expire']) ? $options['expire'] : null; //修复查询缓存无法设置过期时间 + } else { + $expire = is_numeric($options) ? $options : null; //默认快捷缓存设置过期时间 + } + return Cache::set($name, $value, $expire); + } + } +} + +if (!function_exists('trace')) { + /** + * 记录日志信息 + * @param mixed $log log信息 支持字符串和数组 + * @param string $level 日志级别 + * @return void|array + */ + function trace($log = '[think]', $level = 'log') + { + if ('[think]' === $log) { + return Log::getLog(); + } else { + Log::record($log, $level); + } + } +} + +if (!function_exists('request')) { + /** + * 获取当前Request对象实例 + * @return Request + */ + function request() + { + return Request::instance(); + } +} + +if (!function_exists('response')) { + /** + * 创建普通 Response 对象实例 + * @param mixed $data 输出数据 + * @param int|string $code 状态码 + * @param array $header 头信息 + * @param string $type + * @return Response + */ + function response($data = [], $code = 200, $header = [], $type = 'html') + { + return Response::create($data, $type, $code, $header); + } +} + +if (!function_exists('view')) { + /** + * 渲染模板输出 + * @param string $template 模板文件 + * @param array $vars 模板变量 + * @param integer $code 状态码 + * @return \think\response\View + */ + function view($template = '', $vars = [], $code = 200) + { + return Response::create($template, 'view', $code)->vars($vars); + } +} + +if (!function_exists('json')) { + /** + * 获取\think\response\Json对象实例 + * @param mixed $data 返回的数据 + * @param integer $code 状态码 + * @param array $header 头部 + * @param array $options 参数 + * @return \think\response\Json + */ + function json($data = [], $code = 200, $header = [], $options = []) + { + return Response::create($data, 'json', $code, $header, $options); + } +} + +if (!function_exists('jsonp')) { + /** + * 获取\think\response\Jsonp对象实例 + * @param mixed $data 返回的数据 + * @param integer $code 状态码 + * @param array $header 头部 + * @param array $options 参数 + * @return \think\response\Jsonp + */ + function jsonp($data = [], $code = 200, $header = [], $options = []) + { + return Response::create($data, 'jsonp', $code, $header, $options); + } +} + +if (!function_exists('xml')) { + /** + * 获取\think\response\Xml对象实例 + * @param mixed $data 返回的数据 + * @param integer $code 状态码 + * @param array $header 头部 + * @param array $options 参数 + * @return \think\response\Xml + */ + function xml($data = [], $code = 200, $header = [], $options = []) + { + return Response::create($data, 'xml', $code, $header, $options); + } +} + +if (!function_exists('redirect')) { + /** + * 获取\think\response\Redirect对象实例 + * @param mixed $url 重定向地址 支持Url::build方法的地址 + * @param array|integer $params 额外参数 + * @param integer $code 状态码 + * @return \think\response\Redirect + */ + function redirect($url = [], $params = [], $code = 302) + { + if (is_integer($params)) { + $code = $params; + $params = []; + } + return Response::create($url, 'redirect', $code)->params($params); + } +} + +if (!function_exists('abort')) { + /** + * 抛出HTTP异常 + * @param integer $code 状态码 + * @param string $message 错误信息 + * @param array $header 参数 + */ + function abort($code, $message = null, $header = []) + { + throw new \think\exception\HttpException($code, $message, null, $header); + } } diff --git a/lang/.gitignore b/lang/.gitignore deleted file mode 100644 index c96a04f0..00000000 --- a/lang/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -* -!.gitignore \ No newline at end of file diff --git a/lang/zh-cn.php b/lang/zh-cn.php new file mode 100644 index 00000000..44731c34 --- /dev/null +++ b/lang/zh-cn.php @@ -0,0 +1,61 @@ + +// +---------------------------------------------------------------------- + +// 核心中文语言包 +return [ + // 系统错误提示 + 'Undefined variable' => '未定义变量', + 'Undefined index' => '未定义索引', + 'Parse error' => '语法解析错误', + 'Type error' => '类型错误', + 'Fatal error' => '致命错误', + + // 框架核心错误提示 + 'dispatch type not support' => '不支持的调度类型', + 'method param miss' => '方法参数错误', + 'method not exists' => '方法不存在', + 'module not exists' => '模块不存在', + 'controller not exists' => '控制器不存在', + 'class not exists' => '类不存在', + 'property not exists' => '类的属性不存在', + 'template not exists' => '模板文件不存在', + 'illegal controller name' => '非法的控制器名称', + 'illegal action name' => '非法的操作名称', + 'url suffix deny' => '禁止的URL后缀访问', + 'Route Not Found' => '当前访问路由未定义', + 'Underfined db type' => '未定义数据库类型', + 'variable type error' => '变量类型错误', + 'PSR-4 error' => 'PSR-4 规范错误', + 'not support total' => '简洁模式下不能获取数据总数', + 'not support last' => '简洁模式下不能获取最后一页', + 'error session handler' => '错误的SESSION处理器类', + 'not allow php tag' => '模板不允许使用PHP语法', + 'not support' => '不支持', + 'redisd master' => 'Redisd 主服务器错误', + 'redisd slave' => 'Redisd 从服务器错误', + 'must run at sae' => '必须在SAE运行', + 'memcache init error' => '未开通Memcache服务,请在SAE管理平台初始化Memcache服务', + 'KVDB init error' => '没有初始化KVDB,请在SAE管理平台初始化KVDB服务', + 'fields not exists' => '数据表字段不存在', + 'where express error' => '查询表达式错误', + 'no data to update' => '没有任何数据需要更新', + 'miss data to insert' => '缺少需要写入的数据', + 'miss complex primary data' => '缺少复合主键数据', + 'miss update condition' => '缺少更新条件', + 'model data Not Found' => '模型数据不存在', + 'table data not Found' => '表数据不存在', + 'delete without condition' => '没有条件不会执行删除操作', + 'miss relation data' => '缺少关联表数据', + 'tag attr must' => '模板标签属性必须', + 'tag error' => '模板标签错误', + 'cache write error' => '缓存写入失败', + 'sae mc write error' => 'SAE mc 写入错误', +]; diff --git a/library/think/App.php b/library/think/App.php index 2d0e0623..4b26992f 100644 --- a/library/think/App.php +++ b/library/think/App.php @@ -11,7 +11,17 @@ namespace think; +use think\Config; +use think\Exception; +use think\exception\HttpException; +use think\exception\HttpResponseException; +use think\Hook; +use think\Lang; +use think\Loader; +use think\Log; +use think\Request; use think\Response; +use think\Route; /** * App 应用管理 @@ -19,127 +29,180 @@ use think\Response; */ class App { + /** + * @var bool 是否初始化过 + */ + protected static $init = false; + + /** + * @var string 当前模块路径 + */ + public static $modulePath; + + /** + * @var bool 应用调试模式 + */ + public static $debug = true; + + /** + * @var string 应用类库命名空间 + */ + public static $namespace = 'app'; + + /** + * @var bool 应用类库后缀 + */ + public static $suffix = false; + + /** + * @var bool 应用路由检测 + */ + protected static $routeCheck; + + /** + * @var bool 严格路由检测 + */ + protected static $routeMust; + + protected static $dispatch; /** * 执行应用程序 * @access public - * @return void + * @param Request $request Request对象 + * @return Response + * @throws Exception */ - public static function run() + public static function run(Request $request = null) { - // 初始化应用(公共模块) - self::initModule(COMMON_MODULE, Config::get()); + is_null($request) && $request = Request::instance(); - // 获取配置参数 - $config = Config::get(); + $config = self::initCommon(); - // 注册根命名空间 - if (!empty($config['root_namespace'])) { - Loader::addNamespace($config['root_namespace']); - } + try { - // 加载额外文件 - if (!empty($config['extra_file_list'])) { - foreach ($config['extra_file_list'] as $file) { - $file = strpos($file, '.') ? $file : APP_PATH . $file . EXT; - if (is_file($file)) { - include_once $file; + // 开启多语言机制 + if ($config['lang_switch_on']) { + // 获取当前语言 + $request->langset(Lang::detect()); + // 加载系统语言包 + Lang::load(THINK_PATH . 'lang' . DS . $request->langset() . EXT); + if (!$config['app_multi_module']) { + Lang::load(APP_PATH . 'lang' . DS . $request->langset() . EXT); } } - } - // 设置系统时区 - date_default_timezone_set($config['default_timezone']); - - // 监听app_init - APP_HOOK && Hook::listen('app_init'); - - // 开启多语言机制 - if ($config['lang_switch_on']) { - // 获取当前语言 - defined('LANG_SET') or define('LANG_SET', Lang::range()); - // 加载系统语言包 - Lang::load(THINK_PATH . 'lang' . DS . LANG_SET . EXT); - if (!APP_MULTI_MODULE) { - Lang::load(APP_PATH . 'lang' . DS . LANG_SET . EXT); + // 获取应用调度信息 + $dispatch = self::$dispatch; + if (empty($dispatch)) { + // 进行URL路由检测 + $dispatch = self::routeCheck($request, $config); } + // 记录当前调度信息 + $request->dispatch($dispatch); + // 记录路由信息 + self::$debug && Log::record('[ ROUTE ] ' . var_export($dispatch, true), 'info'); + // 监听app_begin + Hook::listen('app_begin', $dispatch); + + switch ($dispatch['type']) { + case 'redirect': + // 执行重定向跳转 + $data = Response::create($dispatch['url'], 'redirect')->code($dispatch['status']); + break; + case 'module': + // 模块/控制器/操作 + $data = self::module($dispatch['module'], $config, isset($dispatch['convert']) ? $dispatch['convert'] : null); + break; + case 'controller': + // 执行控制器操作 + $data = Loader::action($dispatch['controller'], $dispatch['params']); + break; + case 'method': + // 执行回调方法 + $data = self::invokeMethod($dispatch['method'], $dispatch['params']); + break; + case 'function': + // 执行闭包 + $data = self::invokeFunction($dispatch['function'], $dispatch['params']); + break; + case 'response': + $data = $dispatch['response']; + break; + default: + throw new \InvalidArgumentException('dispatch type not support'); + } + } catch (HttpResponseException $exception) { + $data = $exception->getResponse(); } - // 获取当前请求的调度信息 - $dispatch = Request::instance()->dispatch(); - if (empty($dispatch)) { - // 未指定调度类型 则进行URL路由检测 - $dispatch = self::route($config); - } - // 记录路由信息 - APP_DEBUG && Log::record('[ ROUTE ] ' . var_export($dispatch, true), 'info'); - // 监听app_begin - APP_HOOK && Hook::listen('app_begin', $dispatch); - switch ($dispatch['type']) { - case 'redirect': - // 执行重定向跳转 - header('Location: ' . $dispatch['url'], true, $dispatch['status']); - break; - case 'module': - // 模块/控制器/操作 - $data = self::module($dispatch['module'], $config); - break; - case 'controller': - // 执行控制器操作 - $data = Loader::action($dispatch['controller'], $dispatch['params']); - break; - case 'method': - // 执行回调方法 - $data = self::invokeMethod($dispatch['method'], $dispatch['params']); - break; - case 'function': - // 规则闭包 - $data = self::invokeFunction($dispatch['function'], $dispatch['params']); - break; - case 'finish': - // 已经完成 不再继续执行 - break; - default: - throw new Exception('dispatch type not support', 10008); - } + // 清空类的实例化 + Loader::clearInstance(); + // 输出数据到客户端 - if (isset($data)) { - // 监听app_end - APP_HOOK && Hook::listen('app_end', $data); - // 自动响应输出 - return Response::send($data, '', Config::get('response_return')); + if ($data instanceof Response) { + $response = $data; + } elseif (!is_null($data)) { + // 默认自动识别响应输出类型 + $isAjax = $request->isAjax(); + $type = $isAjax ? Config::get('default_ajax_return') : Config::get('default_return_type'); + $response = Response::create($data, $type); + } else { + $response = Response::create(); } + + // 监听app_end + Hook::listen('app_end', $response); + + // Trace调试注入 + if (Config::get('app_trace')) { + Debug::inject($response); + } + + return $response; } - // 执行函数或者闭包方法 支持参数调用 + /** + * 设置当前请求的调度信息 + * @access public + * @param array|string $dispatch 调度信息 + * @param string $type 调度类型 + * @param array $params 参数 + * @return void + */ + public static function dispatch($dispatch, $type = 'module', $params = []) + { + self::$dispatch = ['type' => $type, $type => $dispatch, 'params' => $params]; + } + + /** + * 执行函数或者闭包方法 支持参数调用 + * @access public + * @param string|array|\Closure $function 函数或者闭包 + * @param array $vars 变量 + * @return mixed + */ public static function invokeFunction($function, $vars = []) { $reflect = new \ReflectionFunction($function); $args = self::bindParams($reflect, $vars); // 记录执行信息 - APP_DEBUG && Log::record('[ RUN ] ' . $reflect->getFileName() . '[ ' . var_export($vars, true) . ' ]', 'info'); + self::$debug && Log::record('[ RUN ] ' . $reflect->__toString(), 'info'); return $reflect->invokeArgs($args); } - // 调用反射执行类的方法 支持参数绑定 + /** + * 调用反射执行类的方法 支持参数绑定 + * @access public + * @param string|array $method 方法 + * @param array $vars 变量 + * @return mixed + */ public static function invokeMethod($method, $vars = []) { if (empty($vars)) { // 自动获取请求变量 - switch (REQUEST_METHOD) { - case 'POST': - $vars = array_merge($_GET, $_POST); - break; - case 'PUT': - static $_PUT = null; - if (is_null($_PUT)) { - parse_str(file_get_contents('php://input'), $_PUT); - } - $vars = array_merge($_GET, $_PUT); - break; - default: - $vars = $_GET; - } + $vars = Request::instance()->param(); } if (is_array($method)) { $class = is_object($method[0]) ? $method[0] : new $method[0]; @@ -150,11 +213,17 @@ class App } $args = self::bindParams($reflect, $vars); // 记录执行信息 - APP_DEBUG && Log::record('[ RUN ] ' . $reflect->getFileName() . '[ ' . var_export($args, true) . ' ]', 'info'); + self::$debug && Log::record('[ RUN ] ' . $reflect->__toString(), 'info'); return $reflect->invokeArgs(isset($class) ? $class : null, $args); } - // 绑定参数 + /** + * 绑定参数 + * @access public + * @param \ReflectionMethod|\ReflectionFunction $reflect 反射类 + * @param array $vars 变量 + * @return array + */ private static function bindParams($reflect, $vars) { $args = []; @@ -163,104 +232,180 @@ class App if ($reflect->getNumberOfParameters() > 0) { $params = $reflect->getParameters(); foreach ($params as $param) { - $name = $param->getName(); - if (1 == $type && !empty($vars)) { + $name = $param->getName(); + $class = $param->getClass(); + if ($class && 'think\Request' == $class->getName()) { + $args[] = Request::instance(); + } elseif (1 == $type && !empty($vars)) { $args[] = array_shift($vars); } elseif (0 == $type && isset($vars[$name])) { $args[] = $vars[$name]; } elseif ($param->isDefaultValueAvailable()) { $args[] = $param->getDefaultValue(); } else { - throw new Exception('method param miss:' . $name, 10004); + throw new \InvalidArgumentException('method param miss:' . $name); } } // 全局过滤 - array_walk_recursive($args, 'think\\Input::filterExp'); + array_walk_recursive($args, [Request::instance(), 'filterExp']); } return $args; } - // 执行 模块/控制器/操作 - private static function module($result, $config) + /** + * 执行模块 + * @access public + * @param array $result 模块/控制器/操作 + * @param array $config 配置参数 + * @param bool $convert 是否自动转换控制器和操作名 + * @return mixed + */ + public static function module($result, $config, $convert = null) { - if (APP_MULTI_MODULE) { + if (is_string($result)) { + $result = explode('/', $result); + } + $request = Request::instance(); + if ($config['app_multi_module']) { // 多模块部署 - $module = strtolower($result[0] ?: $config['default_module']); - if ($maps = $config['url_module_map']) { - if (isset($maps[$module])) { - // 记录当前别名 - define('MODULE_ALIAS', $module); - // 获取实际的项目名 - $module = $maps[MODULE_ALIAS]; - } elseif (array_search($module, $maps)) { - // 禁止访问原始项目 - $module = ''; + $module = strip_tags(strtolower($result[0] ?: $config['default_module'])); + $bind = Route::getBind('module'); + $available = false; + if ($bind) { + // 绑定模块 + list($bindModule) = explode('/', $bind); + if ($module == $bindModule) { + $available = true; } + } elseif (!in_array($module, $config['deny_module_list']) && is_dir(APP_PATH . $module)) { + $available = true; } - // 获取模块名称 - define('MODULE_NAME', strip_tags($module)); // 模块初始化 - if (MODULE_NAME && !in_array(MODULE_NAME, $config['deny_module_list']) && is_dir(APP_PATH . MODULE_NAME)) { - define('MODULE_PATH', APP_PATH . MODULE_NAME . DS); - define('VIEW_PATH', MODULE_PATH . VIEW_LAYER . DS); + if ($module && $available) { // 初始化模块 - self::initModule(MODULE_NAME, $config); + $request->module($module); + $config = self::init($module); } else { - throw new Exception('module [ ' . MODULE_NAME . ' ] not exists ', 10005); + throw new HttpException(404, 'module not exists:' . $module); } } else { // 单一模块部署 - define('MODULE_NAME', ''); - define('MODULE_PATH', APP_PATH); - define('VIEW_PATH', MODULE_PATH . VIEW_LAYER . DS); + $module = ''; + $request->module($module); } + // 当前模块路径 + App::$modulePath = APP_PATH . ($module ? $module . DS : ''); + // 是否自动转换控制器和操作名 + $convert = is_bool($convert) ? $convert : $config['url_convert']; // 获取控制器名 - $controllerName = strip_tags($result[1] ?: Config::get('default_controller')); - defined('CONTROLLER_NAME') or define('CONTROLLER_NAME', Config::get('url_controller_convert') ? strtolower($controllerName) : $controllerName); + $controller = strip_tags($result[1] ?: $config['default_controller']); + $controller = $convert ? strtolower($controller) : $controller; // 获取操作名 - $actionName = strip_tags($result[2] ?: Config::get('default_action')); - defined('ACTION_NAME') or define('ACTION_NAME', Config::get('url_action_convert') ? strtolower($actionName) : $actionName); + $actionName = strip_tags($result[2] ?: $config['default_action']); + $actionName = $convert ? strtolower($actionName) : $actionName; - // 执行操作 - if (!preg_match('/^[A-Za-z](\/|\.|\w)*$/', CONTROLLER_NAME)) { - // 安全检测 - throw new Exception('illegal controller name:' . CONTROLLER_NAME, 10000); - } - $instance = Loader::controller(CONTROLLER_NAME, '', Config::get('use_controller_suffix'), Config::get('empty_controller')); - // 获取当前操作名 - $action = ACTION_NAME . Config::get('action_suffix'); + // 设置当前请求的控制器、操作 + $request->controller($controller)->action($actionName); + + // 监听module_init + Hook::listen('module_init', $request); try { - // 操作方法开始监听 - $call = [$instance, $action]; - APP_HOOK && Hook::listen('action_begin', $call); + $instance = Loader::controller($controller, $config['url_controller_layer'], $config['controller_suffix'], $config['empty_controller']); + if (is_null($instance)) { + throw new HttpException(404, 'controller not exists:' . $controller); + } + // 获取当前操作名 + $action = $actionName . $config['action_suffix']; if (!preg_match('/^[A-Za-z](\w)*$/', $action)) { // 非法操作 - throw new \ReflectionException('illegal action name :' . ACTION_NAME); + throw new \ReflectionException('illegal action name:' . $actionName); } + // 执行操作方法 + $call = [$instance, $action]; + Hook::listen('action_begin', $call); + $data = self::invokeMethod($call); } catch (\ReflectionException $e) { // 操作不存在 if (method_exists($instance, '_empty')) { $method = new \ReflectionMethod($instance, '_empty'); $data = $method->invokeArgs($instance, [$action, '']); - APP_DEBUG && Log::record('[ RUN ] ' . $method->getFileName(), 'info'); + self::$debug && Log::record('[ RUN ] ' . $method->__toString(), 'info'); } else { - throw new Exception('method [ ' . (new \ReflectionClass($instance))->getName() . '->' . $action . ' ] not exists ', 10002); + throw new HttpException(404, 'method not exists:' . (new \ReflectionClass($instance))->getName() . '->' . $action); } } return $data; } - // 初始化模块 - private static function initModule($module, $config) + /** + * 初始化应用 + */ + public static function initCommon() + { + if (empty(self::$init)) { + // 初始化应用 + $config = self::init(); + self::$suffix = $config['class_suffix']; + + // 应用调试模式 + self::$debug = Config::get('app_debug'); + if (!self::$debug) { + ini_set('display_errors', 'Off'); + } else { + //重新申请一块比较大的buffer + if (ob_get_level() > 0) { + $output = ob_get_clean(); + } + ob_start(); + if (!empty($output)) { + echo $output; + } + } + + // 应用命名空间 + self::$namespace = $config['app_namespace']; + Loader::addNamespace($config['app_namespace'], APP_PATH); + if (!empty($config['root_namespace'])) { + Loader::addNamespace($config['root_namespace']); + } + + // 加载额外文件 + if (!empty($config['extra_file_list'])) { + foreach ($config['extra_file_list'] as $file) { + $file = strpos($file, '.') ? $file : APP_PATH . $file . EXT; + if (is_file($file)) { + include_once $file; + } + } + } + + // 设置系统时区 + date_default_timezone_set($config['default_timezone']); + + // 监听app_init + Hook::listen('app_init'); + + self::$init = $config; + } + return self::$init; + } + + /** + * 初始化应用或模块 + * @access public + * @param string $module 模块名 + * @return array + */ + private static function init($module = '') { // 定位模块目录 - $module = (COMMON_MODULE == $module || !APP_MULTI_MODULE) ? '' : $module . DS; + $module = $module ? $module . DS : ''; // 加载初始化文件 if (is_file(APP_PATH . $module . 'init' . EXT)) { @@ -268,29 +413,29 @@ class App } else { $path = APP_PATH . $module; // 加载模块配置 - $config = Config::load(APP_PATH . $module . 'config' . CONF_EXT); + $config = Config::load(CONF_PATH . $module . 'config' . CONF_EXT); // 加载应用状态配置 if ($config['app_status']) { - $config = Config::load(APP_PATH . $module . $config['app_status'] . CONF_EXT); + $config = Config::load(CONF_PATH . $module . $config['app_status'] . CONF_EXT); } // 读取扩展配置文件 if ($config['extra_config_list']) { foreach ($config['extra_config_list'] as $name => $file) { - $filename = $path . $file . CONF_EXT; + $filename = CONF_PATH . $module . $file . CONF_EXT; Config::load($filename, is_string($name) ? $name : pathinfo($filename, PATHINFO_FILENAME)); } } // 加载别名文件 - if (is_file($path . 'alias' . EXT)) { - Loader::addMap(include $path . 'alias' . EXT); + if (is_file(CONF_PATH . $module . 'alias' . EXT)) { + Loader::addClassMap(include CONF_PATH . $module . 'alias' . EXT); } // 加载行为扩展文件 - if (APP_HOOK && is_file($path . 'tags' . EXT)) { - Hook::import(include $path . 'tags' . EXT); + if (is_file(CONF_PATH . $module . 'tags' . EXT)) { + Hook::import(include CONF_PATH . $module . 'tags' . EXT); } // 加载公共文件 @@ -300,53 +445,63 @@ class App // 加载当前模块语言包 if ($config['lang_switch_on'] && $module) { - Lang::load($path . 'lang' . DS . LANG_SET . EXT); + Lang::load($path . 'lang' . DS . Request::instance()->langset() . EXT); } } + return Config::get(); } /** * URL路由检测(根据PATH_INFO) * @access public - * @param array $config - * @throws Exception + * @param \think\Request $request + * @param array $config + * @return array + * @throws \think\Exception */ - public static function route(array $config) + public static function routeCheck($request, array $config) { - - define('__INFO__', Request::instance()->pathinfo()); - define('__EXT__', Request::instance()->ext()); - // 检测URL禁用后缀 - if ($config['url_deny_suffix'] && preg_match('/\.(' . $config['url_deny_suffix'] . ')$/i', __INFO__)) { - throw new Exception('url suffix deny'); + if ($config['url_deny_suffix'] && preg_match('/\.(' . $config['url_deny_suffix'] . ')$/i', $request->pathinfo())) { + throw new Exception('url suffix deny:' . $request->ext()); } - $_SERVER['PATH_INFO'] = Request::instance()->path(); - $depr = $config['pathinfo_depr']; - $result = false; + $path = $request->path(); + $depr = $config['pathinfo_depr']; + $result = false; // 路由检测 - if (APP_ROUTE_ON && !empty($config['url_route_on'])) { + $check = !is_null(self::$routeCheck) ? self::$routeCheck : $config['url_route_on']; + if ($check) { // 开启路由 if (!empty($config['route'])) { - // 注册路由定义文件 - Route::register($config['route']); + // 导入路由配置 + Route::import($config['route']); } // 路由检测(根据路由定义返回不同的URL调度) - $result = Route::check($_SERVER['PATH_INFO'], $depr, !IS_CLI ? $config['url_domain_deploy'] : false); - if (APP_ROUTE_MUST && false === $result && $config['url_route_must']) { + $result = Route::check($request, $path, $depr, $config['url_domain_deploy']); + $must = !is_null(self::$routeMust) ? self::$routeMust : $config['url_route_must']; + if ($must && false === $result) { // 路由无效 - throw new Exception('route not define '); + throw new HttpException(404, 'Route Not Found'); } } if (false === $result) { - // 路由无效默认分析为模块/控制器/操作/参数...方式URL - $result = Route::parseUrl($_SERVER['PATH_INFO'], $depr); + // 路由无效 解析模块/控制器/操作/参数... 支持控制器自动搜索 + $result = Route::parseUrl($path, $depr, $config['controller_auto_search']); } - //保证$_REQUEST正常取值 - $_REQUEST = array_merge($_POST, $_GET, $_COOKIE); - // 注册调度机制 - return Request::instance()->dispatch($result); + return $result; } + /** + * 设置应用的路由检测机制 + * @access public + * @param bool $route 是否需要检测路由 + * @param bool $must 是否强制检测路由 + * @return void + */ + public static function route($route, $must = false) + { + self::$routeCheck = $route; + self::$routeMust = $must; + } } diff --git a/library/think/Build.php b/library/think/Build.php index 7b26a103..76d82609 100644 --- a/library/think/Build.php +++ b/library/think/Build.php @@ -13,8 +13,15 @@ namespace think; class Build { - // 根据传入的build资料创建目录和文件 - public static function run(array $build = []) + /** + * 根据传入的build资料创建目录和文件 + * @access protected + * @param array $build build列表 + * @param string $namespace 应用类库命名空间 + * @param bool $suffix 类库后缀 + * @return void + */ + public static function run(array $build = [], $namespace = 'app', $suffix = false) { // 锁定 $lockfile = APP_PATH . 'build.lock'; @@ -32,31 +39,41 @@ class Build self::buildFile($list); } else { // 创建模块 - self::module($module, $list); + self::module($module, $list, $namespace, $suffix); } } // 解除锁定 unlink($lockfile); } - // 创建目录 + /** + * 创建目录 + * @access protected + * @param array $list 目录列表 + * @return void + */ protected static function buildDir($list) { foreach ($list as $dir) { if (!is_dir(APP_PATH . $dir)) { // 创建目录 - mkdir(APP_PATH . $dir, 0777, true); + mkdir(APP_PATH . $dir, 0755, true); } } } - // 创建文件 + /** + * 创建文件 + * @access protected + * @param array $list 文件列表 + * @return void + */ protected static function buildFile($list) { foreach ($list as $file) { if (!is_dir(APP_PATH . dirname($file))) { // 创建目录 - mkdir(APP_PATH . dirname($file), 0777, true); + mkdir(APP_PATH . dirname($file), 0755, true); } if (!is_file(APP_PATH . $file)) { file_put_contents(APP_PATH . $file, 'php' == pathinfo($file, PATHINFO_EXTENSION) ? "items, $this->convertToArray($items))); } - /** * 交换数组中的键和值 * @@ -116,7 +115,6 @@ class Collection implements ArrayAccess, Countable, IteratorAggregate, JsonSeria return array_pop($this->items); } - /** * 通过使用用户自定义函数,以字符串返回数组 * @@ -199,7 +197,6 @@ class Collection implements ArrayAccess, Countable, IteratorAggregate, JsonSeria return $this; } - /** * 用回调函数过滤数组中的元素 * @param callable|null $callback @@ -230,11 +227,11 @@ class Collection implements ArrayAccess, Countable, IteratorAggregate, JsonSeria foreach ($this->items as $row) { $key = $value = null; $keySet = $valueSet = false; - if ($index_key !== null && array_key_exists($index_key, $row)) { + if (null !== $index_key && array_key_exists($index_key, $row)) { $keySet = true; $key = (string)$row[$index_key]; } - if ($column_key === null) { + if (null === $column_key) { $valueSet = true; $value = $row; } elseif (is_array($row) && array_key_exists($column_key, $row)) { @@ -252,7 +249,6 @@ class Collection implements ArrayAccess, Countable, IteratorAggregate, JsonSeria return $result; } - /** * 对数组排序 * @@ -275,7 +271,6 @@ class Collection implements ArrayAccess, Countable, IteratorAggregate, JsonSeria return new static($items); } - /** * 将数组打乱 * @@ -375,4 +370,4 @@ class Collection implements ArrayAccess, Countable, IteratorAggregate, JsonSeria } return (array)$items; } -} \ No newline at end of file +} diff --git a/library/think/Config.php b/library/think/Config.php index 8e181340..105920b1 100644 --- a/library/think/Config.php +++ b/library/think/Config.php @@ -29,10 +29,10 @@ class Config /** * 解析配置文件或内容 - * @param string $config 配置文件路径或内容 - * @param string $type 配置解析类型 - * @param string $name 配置名(如设置即表示二级配置) - * @param string $range 作用域 + * @param string $config 配置文件路径或内容 + * @param string $type 配置解析类型 + * @param string $name 配置名(如设置即表示二级配置) + * @param string $range 作用域 */ public static function parse($config, $type = '', $name = '', $range = '') { @@ -40,15 +40,15 @@ class Config if (empty($type)) { $type = pathinfo($config, PATHINFO_EXTENSION); } - $class = (false === strpos($type, '\\')) ? '\\think\\config\\driver\\' . ucwords($type) : $type; + $class = false !== strpos($type, '\\') ? $type : '\\think\\config\\driver\\' . ucwords($type); self::set((new $class())->parse($config), $name, $range); } /** * 加载配置文件(PHP格式) - * @param string $file 配置文件名 - * @param string $name 配置名(如设置即表示二级配置) - * @param string $range 作用域 + * @param string $file 配置文件名 + * @param string $name 配置名(如设置即表示二级配置) + * @param string $range 作用域 * @return mixed */ public static function load($file, $name = '', $range = '') @@ -58,11 +58,9 @@ class Config self::$config[$range] = []; } if (is_file($file)) { - // 记录加载信息 - APP_DEBUG && Log::record('[ CONFIG ] ' . $file, 'info'); $type = pathinfo($file, PATHINFO_EXTENSION); if ('php' != $type) { - return self::parse($config, $type, $name, $range); + return self::parse($file, $type, $name, $range); } else { return self::set(include $file, $name, $range); } @@ -73,8 +71,8 @@ class Config /** * 检测配置是否存在 - * @param string $name 配置参数名(支持二级配置 .号分割) - * @param string $range 作用域 + * @param string $name 配置参数名(支持二级配置 .号分割) + * @param string $range 作用域 * @return bool */ public static function has($name, $range = '') @@ -83,7 +81,7 @@ class Config if (!strpos($name, '.')) { // 判断环境变量 - $result = getenv(ENV_PREFIX . $name); + $result = getenv(ENV_PREFIX . strtoupper($name)); if (false !== $result) { return $result; } @@ -91,7 +89,7 @@ class Config } else { // 二维数组设置和获取支持 $name = explode('.', $name); - $result = getenv(ENV_PREFIX . $name[0] . '_' . $name[1]); + $result = getenv(ENV_PREFIX . strtoupper($name[0] . '_' . $name[1])); // 判断环境变量 if (false !== $result) { return $result; @@ -102,8 +100,8 @@ class Config /** * 获取配置参数 为空则获取所有配置 - * @param string $name 配置参数名(支持二级配置 .号分割) - * @param string $range 作用域 + * @param string $name 配置参数名(支持二级配置 .号分割) + * @param string $range 作用域 * @return mixed */ public static function get($name = null, $range = '') @@ -115,7 +113,7 @@ class Config } if (!strpos($name, '.')) { - $result = getenv(ENV_PREFIX . $name); + $result = getenv(ENV_PREFIX . strtoupper($name)); if (false !== $result) { return $result; } @@ -124,7 +122,7 @@ class Config } else { // 二维数组设置和获取支持 $name = explode('.', $name); - $result = getenv(ENV_PREFIX . $name[0] . '_' . $name[1]); + $result = getenv(ENV_PREFIX . strtoupper($name[0] . '_' . $name[1])); // 判断环境变量 if (false !== $result) { return $result; @@ -136,9 +134,9 @@ class Config /** * 设置配置参数 name为数组则为批量设置 - * @param string $name 配置参数名(支持二级配置 .号分割) - * @param mixed $value 配置值 - * @param string $range 作用域 + * @param string|array $name 配置参数名(支持二级配置 .号分割) + * @param mixed $value 配置值 + * @param string $range 作用域 * @return mixed */ public static function set($name, $value = null, $range = '') @@ -177,7 +175,11 @@ class Config */ public static function reset($range = '') { - $range = $range ?: self::$range; - true === $range ? self::$config = [] : self::$config[$range] = []; + $range = $range ?: self::$range; + if (true === $range) { + self::$config = []; + } else { + self::$config[$range] = []; + } } } diff --git a/library/think/Console.php b/library/think/Console.php index 24b85134..991e3a7d 100644 --- a/library/think/Console.php +++ b/library/think/Console.php @@ -21,6 +21,7 @@ use think\console\input\Argument as InputArgument; use think\console\input\Definition as InputDefinition; use think\console\input\Option as InputOption; use think\console\Output; +use think\console\output\Nothing; use think\console\output\Stream; class Console @@ -49,7 +50,8 @@ class Console "think\\console\\command\\Lists", "think\\console\\command\\Build", "think\\console\\command\\make\\Controller", - "think\\console\\command\\make\\Model" + "think\\console\\command\\make\\Model", + "think\\console\\command\\optimize\\Autoload", ]; public function __construct($name = 'UNKNOWN', $version = 'UNKNOWN') @@ -66,6 +68,44 @@ class Console } } + public static function init($run = true) + { + static $console; + if (!$console) { + // 实例化console + $console = new self('Think Console', '0.1'); + // 读取指令集 + if (is_file(CONF_PATH . 'command' . EXT)) { + $commands = include CONF_PATH . 'command' . EXT; + if (is_array($commands)) { + foreach ($commands as $command) { + if (class_exists($command) && is_subclass_of($command, "\\think\\console\\command\\Command")) { + // 注册指令 + $console->add(new $command()); + } + } + } + } + } + if ($run) { + // 运行 + $console->run(); + } else { + return $console; + } + } + + public static function call($command, array $parameters = []) + { + $console = self::init(false); + + array_unshift($parameters, $command); + + $input = new Input($parameters); + + $console->find($command)->run($input, new Nothing()); + } + /** * 执行当前的指令 * @return int @@ -112,8 +152,8 @@ class Console /** * 执行指令 - * @param Input $input - * @param Output $output + * @param Input $input + * @param Output $output * @return int */ public function doRun(Input $input, Output $output) @@ -640,8 +680,8 @@ class Console /** * 配置基于用户的参数和选项的输入和输出实例。 - * @param Input $input 输入实例 - * @param Output $output 输出实例 + * @param Input $input 输入实例 + * @param Output $output 输出实例 */ protected function configureIO(Input $input, Output $output) { @@ -663,18 +703,11 @@ class Console if (true === $input->hasParameterOption(['--quiet', '-q'])) { $output->setVerbosity(Output::VERBOSITY_QUIET); } else { - if ($input->hasParameterOption('-vvv') || $input->hasParameterOption('--verbose=3') - || $input->getParameterOption('--verbose') === 3 - ) { + if ($input->hasParameterOption('-vvv') || $input->hasParameterOption('--verbose=3') || $input->getParameterOption('--verbose') === 3) { $output->setVerbosity(Output::VERBOSITY_DEBUG); - } elseif ($input->hasParameterOption('-vv') || $input->hasParameterOption('--verbose=2') - || $input->getParameterOption('--verbose') === 2 - ) { + } elseif ($input->hasParameterOption('-vv') || $input->hasParameterOption('--verbose=2') || $input->getParameterOption('--verbose') === 2) { $output->setVerbosity(Output::VERBOSITY_VERY_VERBOSE); - } elseif ($input->hasParameterOption('-v') || $input->hasParameterOption('--verbose=1') - || $input->hasParameterOption('--verbose') - || $input->getParameterOption('--verbose') - ) { + } elseif ($input->hasParameterOption('-v') || $input->hasParameterOption('--verbose=1') || $input->hasParameterOption('--verbose') || $input->getParameterOption('--verbose')) { $output->setVerbosity(Output::VERBOSITY_VERBOSE); } } @@ -682,9 +715,9 @@ class Console /** * 执行指令 - * @param Command $command 指令实例 - * @param Input $input 输入实例 - * @param Output $output 输出实例 + * @param Command $command 指令实例 + * @param Input $input 输入实例 + * @param Output $output 输出实例 * @return int * @throws \Exception */ diff --git a/library/think/Controller.php b/library/think/Controller.php index c58360ea..a34d90fd 100644 --- a/library/think/Controller.php +++ b/library/think/Controller.php @@ -13,14 +13,21 @@ namespace think; \think\Loader::import('controller/Jump', TRAIT_PATH, EXT); -use think\View; +use think\Exception; +use think\exception\ValidateException; class Controller { use \traits\controller\Jump; // 视图类实例 - protected $view = null; + protected $view; + // Request实例 + protected $request; + // 验证失败是否抛出异常 + protected $failException = false; + // 是否批量验证 + protected $batchValidate = false; /** * 前置操作方法列表 @@ -31,11 +38,16 @@ class Controller /** * 架构函数 + * @param Request $request Request对象 * @access public */ - public function __construct() + public function __construct(Request $request = null) { - $this->view = View::instance(Config::get('template'), Config::get('view_replace_str')); + if (is_null($request)) { + $request = Request::instance(); + } + $this->view = View::instance(Config::get('template'), Config::get('view_replace_str')); + $this->request = $request; // 控制器初始化 if (method_exists($this, '_initialize')) { @@ -64,14 +76,14 @@ class Controller if (is_string($options['only'])) { $options['only'] = explode(',', $options['only']); } - if (!in_array(ACTION_NAME, $options['only'])) { + if (!in_array($this->request->action(), $options['only'])) { return; } } elseif (isset($options['except'])) { if (is_string($options['except'])) { $options['except'] = explode(',', $options['except']); } - if (in_array(ACTION_NAME, $options['except'])) { + if (in_array($this->request->action(), $options['except'])) { return; } } @@ -83,38 +95,40 @@ class Controller /** * 加载模板输出 - * @access public - * @param string $template 模板文件名 - * @param array $vars 模板输出变量 - * @param array $config 模板参数 + * @access protected + * @param string $template 模板文件名 + * @param array $vars 模板输出变量 + * @param array $replace 模板替换 + * @param array $config 模板参数 * @return mixed */ - public function fetch($template = '', $vars = [], $config = []) + protected function fetch($template = '', $vars = [], $replace = [], $config = []) { - return $this->view->fetch($template, $vars, $config); + return $this->view->fetch($template, $vars, $replace, $config); } /** * 渲染内容输出 - * @access public - * @param string $content 模板内容 - * @param array $vars 模板输出变量 - * @param array $config 模板参数 + * @access protected + * @param string $content 模板内容 + * @param array $vars 模板输出变量 + * @param array $replace 替换内容 + * @param array $config 模板参数 * @return mixed */ - public function display($content = '', $vars = [], $config = []) + protected function display($content = '', $vars = [], $replace = [], $config = []) { - return $this->view->display($content, $vars, $config); + return $this->view->display($content, $vars, $replace, $config); } /** * 模板变量赋值 * @access protected - * @param mixed $name 要显示的模板变量 - * @param mixed $value 变量的值 + * @param mixed $name 要显示的模板变量 + * @param mixed $value 变量的值 * @return void */ - public function assign($name, $value = '') + protected function assign($name, $value = '') { $this->view->assign($name, $value); } @@ -125,24 +139,38 @@ class Controller * @param array|string $engine 引擎参数 * @return void */ - public function engine($engine) + protected function engine($engine) { $this->view->engine($engine); } + /** + * 设置验证失败后是否抛出异常 + * @access protected + * @param bool $fail 是否抛出异常 + * @return $this + */ + protected function validateFailException($fail = true) + { + $this->failException = $fail; + return $this; + } + /** * 验证数据 * @access protected - * @param array $data 数据 + * @param array $data 数据 * @param string|array $validate 验证器名或者验证规则数组 - * @param array $message 提示信息 - * @param mixed $callback 回调方法(闭包) - * @return true|string|array + * @param array $message 提示信息 + * @param bool $batch 是否批量验证 + * @param mixed $callback 回调方法(闭包) + * @return array|string|true + * @throws ValidateException */ - public function validate($data, $validate, $message = [], $callback = null) + protected function validate($data, $validate, $message = [], $batch = false, $callback = null) { if (is_array($validate)) { - $v = Loader::validate(Config::get('default_validate')); + $v = Loader::validate(); $v->rule($validate); } else { if (strpos($validate, '.')) { @@ -154,17 +182,25 @@ class Controller $v->scene($scene); } } + // 是否批量验证 + if($batch || $this->batchValidate){ + $v->batch(true); + } if (is_array($message)) { $v->message($message); } - if (is_callable($callback)) { + if ($callback && is_callable($callback)) { call_user_func_array($callback, [$v, &$data]); } if (!$v->check($data)) { - return $v->getError(); + if ($this->failException) { + throw new ValidateException($v->getError()); + } else { + return $v->getError(); + } } else { return true; } diff --git a/library/think/Cookie.php b/library/think/Cookie.php index 3ababc3b..eb31c769 100644 --- a/library/think/Cookie.php +++ b/library/think/Cookie.php @@ -13,7 +13,6 @@ namespace think; class Cookie { - protected static $config = [ // cookie 名称前缀 'prefix' => '', @@ -38,6 +37,9 @@ class Cookie */ public static function init(array $config = []) { + if (empty($config)) { + $config = Config::get('cookie'); + } self::$config = array_merge(self::$config, array_change_key_case($config)); if (!empty(self::$config['httponly'])) { ini_set('session.cookie_httponly', 1); @@ -86,17 +88,30 @@ class Cookie array_walk_recursive($value, 'self::jsonFormatProtect', 'encode'); $value = 'think:' . json_encode($value); } - $expire = !empty($config['expire']) ? time() + intval($config['expire']) : 0; + $expire = !empty($config['expire']) ? $_SERVER['REQUEST_TIME'] + intval($config['expire']) : 0; if ($config['setcookie']) { setcookie($name, $value, $expire, $config['path'], $config['domain'], $config['secure'], $config['httponly']); } $_COOKIE[$name] = $value; } + /** + * 判断Cookie数据 + * @param string $name cookie名称 + * @param string|null $prefix cookie前缀 + * @return bool + */ + public static function has($name, $prefix = null) + { + $prefix = !is_null($prefix) ? $prefix : self::$config['prefix']; + $name = $prefix . $name; + return isset($_COOKIE[$name]); + } + /** * Cookie获取 - * @param string $name cookie名称 - * @param string|null $prefix cookie前缀 + * @param string $name cookie名称 + * @param string|null $prefix cookie前缀 * @return mixed */ public static function get($name, $prefix = null) @@ -118,8 +133,8 @@ class Cookie /** * Cookie删除 - * @param string $name cookie名称 - * @param string|null $prefix cookie前缀 + * @param string $name cookie名称 + * @param string|null $prefix cookie前缀 * @return mixed */ public static function delete($name, $prefix = null) @@ -128,7 +143,7 @@ class Cookie $prefix = !is_null($prefix) ? $prefix : $config['prefix']; $name = $prefix . $name; if ($config['setcookie']) { - setcookie($name, '', time() - 3600, $config['path'], $config['domain'], $config['secure'], $config['httponly']); + setcookie($name, '', $_SERVER['REQUEST_TIME'] - 3600, $config['path'], $config['domain'], $config['secure'], $config['httponly']); } // 删除指定cookie unset($_COOKIE[$name]); @@ -154,7 +169,7 @@ class Cookie foreach ($_COOKIE as $key => $val) { if (0 === strpos($key, $prefix)) { if ($config['setcookie']) { - setcookie($key, '', time() - 3600, $config['path'], $config['domain'], $config['secure'], $config['httponly']); + setcookie($key, '', $_SERVER['REQUEST_TIME'] - 3600, $config['path'], $config['domain'], $config['secure'], $config['httponly']); } unset($_COOKIE[$key]); } diff --git a/library/think/Db.php b/library/think/Db.php index d329b4e8..e46bc80a 100644 --- a/library/think/Db.php +++ b/library/think/Db.php @@ -11,19 +11,37 @@ namespace think; +use think\App; +use think\Collection; +use think\db\Query; + /** - * ThinkPHP 数据库中间层实现类 + * Class Db + * @package think + * @method Query table(string $table) static 指定数据表(含前缀) + * @method Query name(string $name) static 指定数据表(不含前缀) + * @method Query where(mixed $field, string $op = null, mixed $condition = null) static 查询条件 + * @method Query join(mixed $join, mixed $condition = null, string $type = 'INNER') static JOIN查询 + * @method Query union(mixed $union, boolean $all = false) static UNION查询 + * @method Query limit(mixed $offset, integer $length = null) static 查询LIMIT + * @method Query order(mixed $field, string $order = null) static 查询ORDER + * @method mixed value(string $field) static 获取某个字段的值 + * @method array column(string $field, string $key = '') static 获取某个列的值 + * @method mixed find(mixed $data = []) static 查询单个记录 + * @method mixed select(mixed $data = []) static 查询多个记录 + * @method integer insert(array $data, boolean $replace = false, boolean $getLastInsID = false, string $sequence = null) static 插入一条记录 + * @method integer insertAll(array $dataSet) static 插入多条记录 + * @method integer update(array $data) static 更新记录 + * @method integer delete(mixed $data = []) static 删除记录 + * @method boolean chunk(integer $count, callable $callback, string $column = null) static 分块获取数据 + * @method mixed query(string $sql, array $bind = [], boolean $fetch = false, boolean $master = false, mixed $class = false) static SQL查询 + * @method integer execute(string $sql, array $bind = [], boolean $fetch = false, boolean $getLastInsID = false, string $sequence = null) static SQL执行 + * @method PaginatorCollection paginate(integer $listRows = 15, boolean $simple = false, array $config = []) static 分页查询 */ class Db { - // 数组数据集 - const RESULTSET_ARRAY = 1; - // 对象数据集 - const RESULTSET_COLLECTION = 2; - // 自定义对象数据集 - const RESULTSET_CLASS = 3; // 数据库连接实例 - private static $instances = []; + private static $instance = []; // 查询次数 public static $queryTimes = 0; // 执行次数 @@ -33,9 +51,9 @@ class Db * 数据库初始化 并取得数据库类实例 * @static * @access public - * @param mixed $config 连接配置 - * @param bool|string $name 连接标识 true 强制重新连接 - * @return db\Connection + * @param mixed $config 连接配置 + * @param bool|string $name 连接标识 true 强制重新连接 + * @return \think\db\Connection * @throws Exception */ public static function connect($config = [], $name = false) @@ -43,22 +61,22 @@ class Db if (false === $name) { $name = md5(serialize($config)); } - if (true === $name || !isset(self::$instances[$name])) { + if (true === $name || !isset(self::$instance[$name])) { // 解析连接参数 支持数组和字符串 $options = self::parseConfig($config); if (empty($options['type'])) { - throw new Exception('db type error'); + throw new \InvalidArgumentException('Underfined db type'); } - $class = (!empty($options['namespace']) ? $options['namespace'] : '\\think\\db\\connector\\') . ucwords($options['type']); + $class = false !== strpos($options['type'], '\\') ? $options['type'] : '\\think\\db\\connector\\' . ucwords($options['type']); // 记录初始化信息 - APP_DEBUG && Log::record('[ DB ] INIT ' . $options['type'] . ':' . var_export($options, true), 'info'); + App::$debug && Log::record('[ DB ] INIT ' . $options['type'] . ':' . var_export($options, true), 'info'); if (true === $name) { return new $class($options); } else { - self::$instances[$name] = new $class($options); + self::$instance[$name] = new $class($options); } } - return self::$instances[$name]; + return self::$instance[$name]; } /** diff --git a/library/think/Debug.php b/library/think/Debug.php index 89feea77..28b6ab1e 100644 --- a/library/think/Debug.php +++ b/library/think/Debug.php @@ -11,6 +11,13 @@ namespace think; +use think\Config; +use think\exception\ClassNotFoundException; +use think\Log; +use think\Request; +use think\Response; +use think\response\Redirect; + class Debug { // 区间时间信息 @@ -20,8 +27,8 @@ class Debug /** * 记录时间(微秒)和内存使用情况 - * @param string $name 标记位置 - * @param mixed $value 标记值 留空则取当前 time 表示仅记录时间 否则同时记录时间和内存 + * @param string $name 标记位置 + * @param mixed $value 标记值 留空则取当前 time 表示仅记录时间 否则同时记录时间和内存 * @return mixed */ public static function remark($name, $value = '') @@ -36,9 +43,9 @@ class Debug /** * 统计某个区间的时间(微秒)使用情况 - * @param string $start 开始标签 - * @param string $end 结束标签 - * @param integer|string $dec 小数位 + * @param string $start 开始标签 + * @param string $end 结束标签 + * @param integer|string $dec 小数位 * @return integer */ public static function getRangeTime($start, $end, $dec = 6) @@ -56,7 +63,7 @@ class Debug */ public static function getUseTime($dec = 6) { - return number_format((microtime(true) - START_TIME), $dec); + return number_format((microtime(true) - THINK_START_TIME), $dec); } /** @@ -70,9 +77,9 @@ class Debug /** * 记录区间的内存使用情况 - * @param string $start 开始标签 - * @param string $end 结束标签 - * @param integer|string $dec 小数位 + * @param string $start 开始标签 + * @param string $end 结束标签 + * @param integer|string $dec 小数位 * @return string */ public static function getRangeMem($start, $end, $dec = 2) @@ -97,7 +104,7 @@ class Debug */ public static function getUseMem($dec = 2) { - $size = memory_get_usage() - START_MEM; + $size = memory_get_usage() - THINK_START_MEM; $a = ['B', 'KB', 'MB', 'GB', 'TB']; $pos = 0; while ($size >= 1024) { @@ -109,9 +116,9 @@ class Debug /** * 统计区间的内存峰值情况 - * @param string $start 开始标签 - * @param string $end 结束标签 - * @param integer|string $dec 小数位 + * @param string $start 开始标签 + * @param string $end 结束标签 + * @param integer|string $dec 小数位 * @return mixed */ public static function getMemPeak($start, $end, $dec = 2) @@ -131,7 +138,7 @@ class Debug /** * 获取文件加载信息 - * @param bool $detail 是否显示详细 + * @param bool $detail 是否显示详细 * @return integer|array */ public static function getFile($detail = false) @@ -149,9 +156,9 @@ class Debug /** * 浏览器友好的变量输出 - * @param mixed $var 变量 - * @param boolean $echo 是否输出 默认为true 如果为false 则返回输出字符串 - * @param string $label 标签 默认为空 + * @param mixed $var 变量 + * @param boolean $echo 是否输出 默认为true 如果为false 则返回输出字符串 + * @param string $label 标签 默认为空 * @return void|string */ public static function dump($var, $echo = true, $label = null) @@ -177,4 +184,36 @@ class Debug } } + public static function inject(Response $response) + { + $config = Config::get('trace'); + $type = isset($config['type']) ? $config['type'] : 'Html'; + $request = Request::instance(); + $accept = $request->header('accept'); + $contentType = $response->getHeader('Content-Type'); + $class = false !== strpos($type, '\\') ? $type : '\\think\\debug\\' . ucwords($type); + unset($config['type']); + if (class_exists($class)) { + $trace = new $class($config); + } else { + throw new ClassNotFoundException('class not exists:' . $class, $class); + } + + if ($response instanceof Redirect) { + //TODO 记录 + } else { + $output = $trace->output($response, Log::getLog()); + if (is_string($output)) { + // trace调试信息注入 + $content = $response->getContent(); + $pos = strripos($content, ''); + if (false !== $pos) { + $content = substr($content, 0, $pos) . $output . substr($content, $pos); + } else { + $content = $content . $output; + } + $response->content($content); + } + } + } } diff --git a/library/think/Error.php b/library/think/Error.php index eea83677..28f75882 100644 --- a/library/think/Error.php +++ b/library/think/Error.php @@ -11,9 +11,10 @@ namespace think; -use think\Config; +use think\console\Output as ConsoleOutput; use think\exception\ErrorException; -use think\Log; +use think\exception\Handle; +use think\exception\ThrowableError; class Error { @@ -23,6 +24,7 @@ class Error */ public static function register() { + error_reporting(E_ALL); set_error_handler([__CLASS__, 'appError']); set_exception_handler([__CLASS__, 'appException']); register_shutdown_function([__CLASS__, 'appShutdown']); @@ -30,59 +32,20 @@ class Error /** * Exception Handler - * @param \Exception $exception - * @return bool true-禁止往下传播已处理过的异常 + * @param \Exception|\Throwable $e */ - public static function appException($exception) + public static function appException($e) { - // 收集异常数据 - if (APP_DEBUG) { - // 调试模式,获取详细的错误信息 - $data = [ - 'name' => get_class($exception), - 'file' => $exception->getFile(), - 'line' => $exception->getLine(), - 'message' => $exception->getMessage(), - 'trace' => $exception->getTrace(), - 'code' => self::getCode($exception), - 'source' => self::getSourceCode($exception), - 'datas' => self::getExtendData($exception), - - 'tables' => [ - 'GET Data' => $_GET, - 'POST Data' => $_POST, - 'Files' => $_FILES, - 'Cookies' => $_COOKIE, - 'Session' => isset($_SESSION) ? $_SESSION : [], - 'Server/Request Data' => $_SERVER, - 'Environment Variables' => $_ENV, - 'ThinkPHP Constants' => self::getTPConst(), - ], - ]; - $log = "[{$data['code']}]{$data['message']}[{$data['file']}:{$data['line']}]"; - } else { - // 部署模式仅显示 Code 和 Message - $data = [ - 'code' => $exception->getCode(), - 'message' => $exception->getMessage(), - ]; - $log = "[{$data['code']}]{$data['message']}"; + if (!$e instanceof \Exception) { + $e = new ThrowableError($e); } - // 记录异常日志 - Log::record($log, 'error'); - - /* 非API模式下的部署模式,跳转到指定的 Error Page */ - $error_page = Config::get('error_page'); - if (!(APP_DEBUG || IS_API) && !empty($error_page)) { - header("Location: {$error_page}"); + self::getExceptionHandler()->report($e); + if (IS_CLI) { + self::getExceptionHandler()->renderForConsole(new ConsoleOutput, $e); } else { - // 输出错误信息 - self::output($exception, $data); + self::getExceptionHandler()->render($e)->send(); } - - // 禁止往下传播已处理过的异常 - return true; } /** @@ -91,141 +54,64 @@ class Error * @param integer $errstr 详细错误信息 * @param string $errfile 出错的文件 * @param integer $errline 出错行号 - * @return bool true-禁止往下传播已处理过的异常 + * @param array $errcontext + * @throws ErrorException */ - public static function appError($errno, $errstr, $errfile = null, $errline = 0, array $errcontext = []) + public static function appError($errno, $errstr, $errfile = '', $errline = 0, $errcontext = []) { - if ($errno & Config::get('exception_ignore_type')) { - // 忽略的异常记录到日志 - Log::record("[{$errno}]{$errstr}[{$errfile}:{$errline}]", 'notice'); - } else { + $exception = new ErrorException($errno, $errstr, $errfile, $errline, $errcontext); + if (error_reporting() & $errno) { // 将错误信息托管至 think\exception\ErrorException - throw new ErrorException($errno, $errstr, $errfile, $errline, $errcontext); - // 禁止往下传播已处理过的异常 - return true; + throw $exception; + } else { + self::getExceptionHandler()->report($exception); } } /** * Shutdown Handler - * @return bool true-禁止往下传播已处理过的异常; false-未处理的异常继续传播 */ public static function appShutdown() { + if (!is_null($error = error_get_last()) && self::isFatal($error['type'])) { + // 将错误信息托管至think\ErrorException + $exception = new ErrorException($error['type'], $error['message'], $error['file'], $error['line']); + + self::appException($exception); + } + // 写入日志 Log::save(); - - if ($error = error_get_last()) { - // 将错误信息托管至think\ErrorException - $exception = new ErrorException( - $error['type'], - $error['message'], - $error['file'], - $error['line'] - ); - - /** - * Shutdown handler 中的异常将不被往下传播 - * 所以,这里我们必须手动传播而不能像 Error handler 中那样 throw - */ - self::appException($exception); - // 禁止往下传播已处理过的异常 - return true; - } - return false; } /** - * 输出异常信息 - * @param \Exception $exception - * @param array $data 异常信息 - * @return void + * 确定错误类型是否致命 + * + * @param int $type + * @return bool */ - public static function output($exception, array $data) + protected static function isFatal($type) { - http_response_code($exception instanceof Exception ? $exception->getHttpStatus() : 500); - - $type = Config::get('default_return_type'); - if (!APP_DEBUG && !Config::get('show_error_msg')) { - // 不显示详细错误信息 - $data['message'] = Config::get('error_message'); - } - if (IS_API && 'html' != $type) { - // 异常信息输出监听 - APP_HOOK && Hook::listen('error_output', $data); - // 输出异常内容 - Response::send($data, $type, Config::get('response_return')); - } else { - //ob_end_clean(); - extract($data); - include Config::get('exception_tmpl'); - } + return in_array($type, [E_ERROR, E_CORE_ERROR, E_COMPILE_ERROR, E_PARSE]); } /** - * 获取错误编码 - * ErrorException则使用错误级别作为错误编码 - * @param \Exception $exception - * @return integer 错误编码 + * Get an instance of the exception handler. + * + * @return Handle */ - private static function getCode($exception) + public static function getExceptionHandler() { - $code = $exception->getCode(); - if (!$code && $exception instanceof ErrorException) { - $code = $exception->getSeverity(); + static $handle; + if (!$handle) { + // 异常处理handle + $class = Config::get('exception_handle'); + if ($class && class_exists($class) && is_subclass_of($class, "\\think\\exception\\Handle")) { + $handle = new $class; + } else { + $handle = new Handle; + } } - return $code; - } - - /** - * 获取出错文件内容 - * 获取错误的前9行和后9行 - * @param \Exception $exception - * @return array 错误文件内容 - */ - private static function getSourceCode($exception) - { - // 读取前9行和后9行 - $line = $exception->getLine(); - $first = ($line - 9 > 0) ? $line - 9 : 1; - - try { - $contents = file($exception->getFile()); - $source = [ - 'first' => $first, - 'source' => array_slice($contents, $first - 1, 19), - ]; - } catch (Exception $e) { - $source = []; - } - return $source; - } - - /** - * 获取异常扩展信息 - * 用于非调试模式html返回类型显示 - * @param \Exception $exception - * @return array 异常类定义的扩展数据 - */ - private static function getExtendData($exception) - { - $data = []; - if ($exception instanceof Exception) { - $data = $exception->getData(); - } - return $data; - } - - /** - * 获取ThinkPHP常量列表 - * @return array 常量列表 - */ - private static function getTPConst() - { - $consts = ['THINK_VERSION', 'THINK_PATH', 'LIB_PATH', 'EXTEND_PATH', 'MODE_PATH', 'CORE_PATH', 'TRAIT_PATH', 'APP_PATH', 'RUNTIME_PATH', 'LOG_PATH', 'CACHE_PATH', 'TEMP_PATH', 'MODULE_PATH', 'VIEW_PATH', 'APP_NAMESPACE', 'COMMON_MODULE', 'APP_MULTI_MODULE', 'MODULE_ALIAS', 'MODULE_NAME', 'CONTROLLER_NAME', 'ACTION_NAME', 'MODEL_LAYER', 'VIEW_LAYER', 'CONTROLLER_LAYER', 'APP_DEBUG', 'APP_HOOK', 'ENV_PREFIX', 'IS_API', 'VENDOR_PATH', 'APP_AUTO_RUN', 'APP_MODE', 'REQUEST_METHOD', 'IS_CGI', 'IS_WIN', 'IS_API', 'IS_CLI', 'IS_GET', 'IS_POST', 'IS_PUT', 'IS_AJAX', 'IS_DELETE', 'NOW_TIME', 'LANG_SET', 'EXT', 'DS', '__INFO__', '__EXT__']; - foreach ($consts as $const) { - $data[$const] = defined($const) ? constant($const) : 'undefined'; - } - return $data; + return $handle; } } diff --git a/library/think/Exception.php b/library/think/Exception.php index abf32018..ac648764 100644 --- a/library/think/Exception.php +++ b/library/think/Exception.php @@ -11,17 +11,8 @@ namespace think; -/** - * ThinkPHP核心异常类 - * 所有系统异常必须继承该类 - */ class Exception extends \Exception { - /** - * 系统异常后发送给客户端的HTTP Status - * @var integer - */ - protected $httpStatus = 500; /** * 保存异常页面显示的额外Debug数据 @@ -43,7 +34,7 @@ class Exception extends \Exception * key2 value2 * * @param string $label 数据分类,用于异常页面显示 - * @param Array $data 需要显示的数据,必须为关联数组 + * @param array $data 需要显示的数据,必须为关联数组 */ final protected function setData($label, array $data) { @@ -59,13 +50,5 @@ class Exception extends \Exception { return $this->data; } - - /** - * 获取要发送给客户端的HTTP Status - * @return integer HTTP Status - */ - final public function getHttpStatus() - { - return $this->httpStatus; - } + } diff --git a/library/think/File.php b/library/think/File.php index 6994d534..d9d03325 100644 --- a/library/think/File.php +++ b/library/think/File.php @@ -11,16 +11,62 @@ namespace think; +use SplFileInfo; use SplFileObject; class File extends SplFileObject { - /** * 错误信息 * @var string */ private $error = ''; + // 当前完整文件名 + protected $filename; + // 文件上传命名规则 + protected $rule = 'date'; + // 单元测试 + protected $isTest; + // 上传文件信息 + protected $info; + + public function __construct($filename, $mode = 'r') + { + parent::__construct($filename, $mode); + $this->filename = $this->getRealPath(); + } + + /** + * 是否测试 + * @param bool $test 是否测试 + * @return $this + */ + public function isTest($test = false) + { + $this->isTest = $test; + return $this; + } + + /** + * 设置上传信息 + * @param array $info 上传文件信息 + * @return $this + */ + public function setUploadInfo($info) + { + $this->info = $info; + return $this; + } + + /** + * 获取上传文件的信息 + * @param string $name + * @return array|string + */ + public function getInfo($name = '') + { + return isset($this->info[$name]) ? $this->info[$name] : $this->info; + } /** * 检查目录是否可写 @@ -33,7 +79,7 @@ class File extends SplFileObject return true; } - if (mkdir($path, 0777, true)) { + if (mkdir($path, 0755, true)) { return true; } else { $this->error = "目录 {$path} 创建失败!"; @@ -48,27 +94,56 @@ class File extends SplFileObject public function getMime() { $finfo = finfo_open(FILEINFO_MIME_TYPE); - return finfo_file($finfo, $this->getRealPath()); + return finfo_file($finfo, $this->filename); + } + + /** + * 设置文件的命名规则 + * @param string $rule 文件命名规则 + * @return $this + */ + public function rule($rule) + { + $this->rule = $rule; + return $this; + } + + /** + * 检测是否合法的上传文件 + * @return bool + */ + public function isValid() + { + if ($this->isTest) { + return is_file($this->filename); + } + return is_uploaded_file($this->filename); } /** * 移动文件 - * @param string $path 保存路径 - * @param string $savename 保存的文件名 - * @param boolean $replace 同名文件是否覆盖 + * @param string $path 保存路径 + * @param string|bool $savename 保存的文件名 默认自动生成 + * @param boolean $replace 同名文件是否覆盖 * @return false|SplFileInfo false-失败 否则返回SplFileInfo实例 */ - public function move($path, $savename = '', $replace = true) + public function move($path, $savename = true, $replace = true) { - if (!is_uploaded_file($this->getRealPath())) { + // 检测合法性 + if (!$this->isValid()) { + $this->error = '非法上传文件'; return false; } - if (false === $this->checkPath($path)) { + $path = rtrim($path, DS) . DS; + // 文件保存命名规则 + $savename = $this->getSaveName($savename); + + // 检测目录 + if (false === $this->checkPath(dirname($path . $savename))) { return false; } - $savename = $savename ?: $this->getFilename(); /* 不覆盖同名文件 */ if (!$replace && is_file($path . $savename)) { $this->error = '存在同名文件' . $path . $savename; @@ -76,7 +151,9 @@ class File extends SplFileObject } /* 移动文件 */ - if (!move_uploaded_file($this->getRealPath(), $path . $savename)) { + if ($this->isTest) { + rename($this->filename, $path . $savename); + } elseif (!move_uploaded_file($this->filename, $path . $savename)) { $this->error = '文件上传保存错误!'; return false; } @@ -84,6 +161,43 @@ class File extends SplFileObject return new SplFileInfo($path . $savename); } + /** + * 获取保存文件名 + * @param string|bool $savename 保存的文件名 默认自动生成 + * @return string + */ + protected function getSaveName($savename) + { + if (true === $savename) { + // 自动生成文件名 + if ($this->rule instanceof \Closure) { + $savename = call_user_func_array($this->rule, [$this]); + } else { + switch ($this->rule) { + case 'md5': + $md5 = md5_file($this->filename); + $savename = substr($md5, 0, 2) . DS . substr($md5, 2); + break; + case 'sha1': + $sha1 = sha1_file($this->filename); + $savename = substr($sha1, 0, 2) . DS . substr($sha1, 2); + break; + case 'date': + $savename = date('Ymd') . DS . md5(microtime(true)); + break; + default: + $savename = call_user_func($this->rule); + } + } + if (!strpos($savename, '.')) { + $savename .= '.' . pathinfo($this->getInfo('name'), PATHINFO_EXTENSION); + } + } elseif ('' === $savename) { + $savename = $this->getInfo('name'); + } + return $savename; + } + /** * 获取错误信息 * @return mixed diff --git a/library/think/Hook.php b/library/think/Hook.php index f5e55af4..4f863d63 100644 --- a/library/think/Hook.php +++ b/library/think/Hook.php @@ -11,6 +11,10 @@ namespace think; +use think\App; +use think\Debug; +use think\Log; + class Hook { @@ -18,9 +22,9 @@ class Hook /** * 动态添加行为扩展到某个标签 - * @param string $tag 标签名称 - * @param mixed $behavior 行为名称 - * @param bool $first 是否放到开头执行 + * @param string $tag 标签名称 + * @param mixed $behavior 行为名称 + * @param bool $first 是否放到开头执行 * @return void */ public static function add($tag, $behavior, $first = false) @@ -39,12 +43,12 @@ class Hook /** * 批量导入插件 - * @param array $data 插件信息 + * @param array $tags 插件信息 * @param boolean $recursive 是否递归合并 - * @return void */ - public static function import(array $tags, $recursive = true) + public static function import($tags, $recursive = true) { + empty($tags) && $tags = []; if (!$recursive) { // 覆盖导入 self::$tags = array_merge(self::$tags, $tags); @@ -84,47 +88,65 @@ class Hook /** * 监听标签的行为 - * @param string $tag 标签名称 - * @param mixed $params 传入参数 - * @return void + * @param string $tag 标签名称 + * @param mixed $params 传入参数 + * @param mixed $extra 额外参数 + * @param bool $once 只获取一个有效返回值 + * @return mixed */ - public static function listen($tag, &$params = null) + public static function listen($tag, &$params = null, $extra = null, $once = false) { + $results = []; if (isset(self::$tags[$tag])) { foreach (self::$tags[$tag] as $name) { - if (APP_DEBUG) { + if (App::$debug) { Debug::remark('behavior_start', 'time'); } - $result = self::exec($name, $tag, $params); + $result = self::exec($name, $tag, $params, $extra); - if (APP_DEBUG) { + if (!is_null($result) && $once) { + return $result; + } + + if (App::$debug) { Debug::remark('behavior_end', 'time'); - Log::record('[ BEHAVIOR ] Run ' . ($name instanceof \Closure ? 'Closure' : $name) . ' @' . $tag . ' [ RunTime:' . Debug::getRangeTime('behavior_start', 'behavior_end') . 's ]', 'info'); + if ($name instanceof \Closure) { + $name = 'Closure'; + } elseif (is_object($name)) { + $name = get_class($name); + } + Log::record('[ BEHAVIOR ] Run ' . $name . ' @' . $tag . ' [ RunTime:' . Debug::getRangeTime('behavior_start', 'behavior_end') . 's ]', 'info'); } if (false === $result) { // 如果返回false 则中断行为执行 - return; + break; } + $results[] = $result; } } - return; + return $once ? null : $results; } /** * 执行某个行为 - * @param string $class 行为类名称 - * @param string $tag 方法名(标签名) - * @param Mixed $params 传人的参数 + * @param mixed $class 要执行的行为 + * @param string $tag 方法名(标签名) + * @param Mixed $params 传人的参数 + * @param mixed $extra 额外参数 * @return mixed */ - public static function exec($class, $tag = '', &$params = null) + public static function exec($class, $tag = '', &$params = null,$extra=null) { if ($class instanceof \Closure) { - return $class($params); + $result = call_user_func_array($class, [ & $params,$extra]); + } elseif (is_object($class)) { + $result = call_user_func_array([$class, $tag], [ & $params,$extra]); + } else { + $obj = new $class(); + $result = ($tag && is_callable([$obj, $tag])) ? $obj->$tag($params,$extra) : $obj->run($params,$extra); } - $obj = new $class(); - return ($tag && is_callable([$obj, $tag])) ? $obj->$tag($params) : $obj->run($params); + return $result; } } diff --git a/library/think/Input.php b/library/think/Input.php deleted file mode 100644 index 6ecd82de..00000000 --- a/library/think/Input.php +++ /dev/null @@ -1,467 +0,0 @@ - -// +---------------------------------------------------------------------- - -namespace think; - -use think\Config; -use think\File; - -class Input -{ - // 全局过滤规则 - public static $filters; - - /** - * 获取get变量 - * @param string $name 数据名称 - * @param string $default 默认值 - * @param string $filter 过滤方法 - * @param boolean $merge 是否与默认的过虑方法合并 - * @return mixed - */ - public static function get($name = '', $default = null, $filter = null, $merge = false) - { - return self::data($_GET, $name, $default, $filter, $merge); - } - - /** - * 获取post变量 - * @param string $name 数据名称 - * @param string $default 默认值 - * @param string $filter 过滤方法 - * @param boolean $merge 是否与默认的过虑方法合并 - * @return mixed - */ - public static function post($name = '', $default = null, $filter = null, $merge = false) - { - return self::data($_POST, $name, $default, $filter, $merge); - } - - /** - * 获取put变量 - * @param string $name 数据名称 - * @param string $default 默认值 - * @param string $filter 过滤方法 - * @param boolean $merge 是否与默认的过虑方法合并 - * @return mixed - */ - public static function put($name = '', $default = null, $filter = null, $merge = false) - { - static $_PUT = null; - if (is_null($_PUT)) { - parse_str(file_get_contents('php://input'), $_PUT); - } - return self::data($_PUT, $name, $default, $filter, $merge); - } - - /** - * 获取delete变量 - * @param string $name 数据名称 - * @param string $default 默认值 - * @param string $filter 过滤方法 - * @param boolean $merge 是否与默认的过虑方法合并 - * @return mixed - */ - public static function delete($name = '', $default = null, $filter = null, $merge = false) - { - static $_DELETE = null; - if (is_null($_DELETE)) { - parse_str(file_get_contents('php://input'), $_DELETE); - $_DELETE = array_merge($_DELETE, $_GET); - } - return self::data($_DELETE, $name, $default, $filter, $merge); - } - - /** - * 根据请求方法获取变量 - * @param string $name 数据名称 - * @param string $default 默认值 - * @param string $filter 过滤方法 - * @param boolean $merge 是否与默认的过虑方法合并 - * @return mixed - */ - public static function param($name = '', $default = null, $filter = null, $merge = false) - { - switch ($_SERVER['REQUEST_METHOD']) { - case 'POST': - $method = 'post'; - break; - case 'PUT': - $method = 'put'; - break; - case 'DELETE': - $method = 'delete'; - break; - default: - $method = 'get'; - } - return self::$method($name, $default, $filter, $merge); - } - - /** - * 获取request变量 - * @param string $name 数据名称 - * @param string $default 默认值 - * @param string $filter 过滤方法 - * @param boolean $merge 是否与默认的过虑方法合并 - * @return mixed - */ - public static function request($name = '', $default = null, $filter = null, $merge = false) - { - return self::data($_REQUEST, $name, $default, $filter, $merge); - } - - /** - * 获取session变量 - * @param string $name 数据名称 - * @param string $default 默认值 - * @param string $filter 过滤方法 - * @param boolean $merge 是否与默认的过虑方法合并 - * @return mixed - */ - public static function session($name = '', $default = null, $filter = null, $merge = false) - { - if (PHP_SESSION_DISABLED == session_status()) { - session_start(); - } - return self::data($_SESSION, $name, $default, $filter, $merge); - } - - /** - * 获取cookie变量 - * @param string $name 数据名称 - * @param string $default 默认值 - * @param string $filter 过滤方法 - * @param boolean $merge 是否与默认的过虑方法合并 - * @return mixed - */ - public static function cookie($name = '', $default = null, $filter = null, $merge = false) - { - return self::data($_COOKIE, $name, $default, $filter, $merge); - } - - /** - * 获取post变量 - * @param string $name 数据名称 - * @param string $default 默认值 - * @param string $filter 过滤方法 - * @param boolean $merge 是否与默认的过虑方法合并 - * @return mixed - */ - public static function server($name = '', $default = null, $filter = null, $merge = false) - { - return self::data($_SERVER, strtoupper($name), $default, $filter, $merge); - } - - /** - * 获取GLOBALS变量 - * @param string $name 数据名称 - * @param string $default 默认值 - * @param string $filter 过滤方法 - * @param boolean $merge 是否与默认的过虑方法合并 - * @return mixed - */ - public static function globals($name = '', $default = null, $filter = null, $merge = false) - { - return self::data($GLOBALS, $name, $default, $filter, $merge); - } - - /** - * 获取环境变量 - * @param string $name 数据名称 - * @param string $default 默认值 - * @param string $filter 过滤方法 - * @param boolean $merge 是否与默认的过虑方法合并 - * @return mixed - */ - public static function env($name = '', $default = null, $filter = null, $merge = false) - { - return self::data($_ENV, strtoupper($name), $default, $filter, $merge); - } - - /** - * 获取PATH_INFO - * @param string $name 数据名称 - * @param string $default 默认值 - * @param string $filter 过滤方法 - * @param boolean $merge 是否与默认的过虑方法合并 - * @return mixed - */ - public static function path($name = '', $default = null, $filter = null, $merge = false) - { - if (!empty($_SERVER['PATH_INFO'])) { - $depr = Config::get('pathinfo_depr'); - $input = explode($depr, trim($_SERVER['PATH_INFO'], $depr)); - return self::data($input, $name, $default, $filter, $merge); - } else { - return $default; - } - } - - /** - * 获取$_FILES - * @param string $name 数据名称 - * @param array $files 上传文件 - * @return \think\File|array - */ - public static function file($name = '', $files = []) - { - $files = $files ?: (isset($_FILES) ? $_FILES : []); - if (!empty($files)) { - if ('' === $name) { - // 获取全部文件 - $file = []; - foreach ($files as $name => $val) { - if (empty($val['tmp_name'])) { - continue; - } - if (is_array($val['tmp_name'])) { - foreach ($val['tmp_name'] as $item) { - $file[] = new File($item); - } - } else { - $file[] = new File($val['tmp_name']); - } - } - return $file; - } elseif (!empty($files[$name]['tmp_name'])) { - if (is_array($files[$name]['tmp_name'])) { - $file = []; - foreach ($files[$name]['tmp_name'] as $item) { - $file[] = new File($item); - } - return $file; - } else { - return new File($files[$name]['tmp_name']); - } - } - } - return null; - } - - /** - * 获取变量 支持过滤和默认值 - * @param array $input 数据源 - * @param string $name 字段名 - * @param mixed $default 默认值 - * @param mixed $filter 过滤函数 - * @param boolean $merge 是否与默认的过虑方法合并 - * @return mixed - */ - public static function data($input, $name = '', $default = null, $filter = null, $merge = false) - { - if (0 === strpos($name, '?')) { - return self::has(substr($name, 1), $input); - } - if (!empty($input)) { - $data = $input; - $name = (string) $name; - if ('' != $name) { - // 解析name - list($name, $type) = static::parseName($name); - // 按.拆分成多维数组进行判断 - foreach (explode('.', $name) as $val) { - if (isset($data[$val])) { - $data = $data[$val]; - } else { - // 无输入数据,返回默认值 - return $default; - } - } - } - - // 解析过滤器 - $filters = static::parseFilter($filter, $merge); - // 为方便传参把默认值附加在过滤器后面 - $filters[] = $default; - if (is_array($data)) { - array_walk_recursive($data, 'self::filter', $filters); - } else { - self::filter($data, $name ?: 0, $filters); - } - if (isset($type) && $data !== $default) { - // 强制类型转换 - static::typeCast($data, $type); - } - } else { - $data = $default; - } - return $data; - } - - /** - * 判断一个变量是否设置 - * @param string $name - * @param array $data - * @return bool - */ - public static function has($name, $data) - { - foreach (explode('.', $name) as $val) { - if (!isset($data[$val])) { - return false; - } else { - $data = $data[$val]; - } - } - return true; - } - - /** - * 设置默认的过滤函数 - * @param string|array $name - * @return array - */ - public static function setFilter($name) - { - if (is_string($name)) { - $name = explode(',', $name); - } - static::$filters = $name; - } - - /** - * 过滤表单中的表达式 - * @param string $value - * @return void - */ - public static function filterExp(&$value) - { - // TODO 其他安全过滤 - - // 过滤查询特殊字符 - if (preg_match('/^(EXP|NEQ|GT|EGT|LT|ELT|OR|XOR|LIKE|NOTLIKE|NOT BETWEEN|NOTBETWEEN|BETWEEN|NOTIN|NOT IN|IN)$/i', $value)) { - $value .= ' '; - } - } - - /** - * 递归过滤给定的值 - * @param mixed $value 键值 - * @param mixed $key 键名 - * @param array $filters 过滤方法+默认值 - * @return mixed - */ - private static function filter(&$value, $key, $filters) - { - // 分离出默认值 - $default = array_pop($filters); - foreach ($filters as $filter) { - if (is_callable($filter)) { - // 调用函数过滤 - $value = call_user_func($filter, $value); - } else { - $begin = substr($filter, 0, 1); - if (in_array($begin, ['/', '#', '~']) && $begin == $end = substr($filter, -1)) { - // 正则过滤 - if (!preg_match($filter, $value)) { - // 匹配不成功返回默认值 - $value = $default; - break; - } - } else { - // filter函数不存在时, 则使用filter_var进行过滤 - // filter为非整形值时, 调用filter_id取得过滤id - $value = filter_var($value, is_int($filter) ? $filter : filter_id($filter)); - if (false === $value) { - // 不通过过滤器则返回默认值 - $value = $default; - break; - } - } - } - } - self::filterExp($value); - } - - /** - * 解析name - * @param string $name - * @return array 返回name和类型 - */ - private static function parseName($name) - { - return strpos($name, '/') ? explode('/', $name, 2) : [$name, 's']; - } - - /** - * 解析过滤器 - * @param mixed $filter - * @return array - */ - private static function parseFilter($filter, $merge = false) - { - if (is_null($filter)) { - $result = self::getFilter(); - } elseif (empty($filter)) { - $result = []; - } else { - if (is_array($filter)) { - $result = $filter; - } elseif (is_string($filter) && strpos($filter, ',')) { - $result = explode(',', $filter); - } else { - $result = [$filter]; - } - if ($merge) { - // 与默认的过滤函数合并 - $result = array_merge(self::getFilter(), array_diff($result, self::getFilter())); - } - } - return $result; - } - - /** - * 获取过滤方法 - * @return array - */ - private static function getFilter() - { - if (is_null(static::$filters)) { - // 从配置项中读取 - $filters = Config::get('default_filter'); - static::$filters = empty($filters) ? [] : (is_array($filters) ? $filters : explode(',', $filters)); - } - return static::$filters; - } - - /** - * 强类型转换 - * @param string $data - * @param string $type - * @return mixed - */ - private static function typeCast(&$data, $type) - { - switch (strtolower($type)) { - // 数组 - case 'a': - $data = (array) $data; - break; - // 数字 - case 'd': - $data = (int) $data; - break; - // 浮点 - case 'f': - $data = (float) $data; - break; - // 布尔 - case 'b': - $data = (boolean) $data; - break; - // 字符串 - case 's': - default: - $data = (string) $data; - } - } -} diff --git a/library/think/Lang.php b/library/think/Lang.php index 2a60fcbf..35c98f05 100644 --- a/library/think/Lang.php +++ b/library/think/Lang.php @@ -11,6 +11,7 @@ namespace think; +use think\App; use think\Cookie; use think\Log; @@ -39,9 +40,9 @@ class Lang /** * 设置语言定义(不区分大小写) - * @param string|array $name 语言变量 - * @param string $value 语言值 - * @param string $range 语言作用域 + * @param string|array $name 语言变量 + * @param string $value 语言值 + * @param string $range 语言作用域 * @return mixed */ public static function set($name, $value = null, $range = '') @@ -78,7 +79,7 @@ class Lang foreach ($file as $_file) { if (is_file($_file)) { // 记录加载信息 - APP_DEBUG && Log::record('[ LANG ] ' . $_file, 'info'); + App::$debug && Log::record('[ LANG ] ' . $_file, 'info'); $_lang = include $_file; } else { $_lang = []; @@ -93,9 +94,22 @@ class Lang /** * 获取语言定义(不区分大小写) - * @param string|null $name 语言变量 - * @param array $vars 变量替换 - * @param string $range 语言作用域 + * @param string|null $name 语言变量 + * @param array $vars 变量替换 + * @param string $range 语言作用域 + * @return mixed + */ + public static function has($name, $range = '') + { + $range = $range ?: self::$range; + return isset(self::$lang[$range][strtolower($name)]); + } + + /** + * 获取语言定义(不区分大小写) + * @param string|null $name 语言变量 + * @param array $vars 变量替换 + * @param string $range 语言作用域 * @return mixed */ public static function get($name = null, $vars = [], $range = '') @@ -134,7 +148,7 @@ class Lang /** * 自动侦测设置获取语言选择 - * @return void + * @return string */ public static function detect() { @@ -155,8 +169,9 @@ class Lang } if (empty(self::$allowLangList) || in_array($langSet, self::$allowLangList)) { // 合法的语言 - self::$range = $langSet; + self::$range = $langSet ?: self::$range; } + return self::$range; } /** diff --git a/library/think/Loader.php b/library/think/Loader.php index 89548650..ed206107 100644 --- a/library/think/Loader.php +++ b/library/think/Loader.php @@ -11,21 +11,28 @@ namespace think; +use think\exception\ClassNotFoundException; + class Loader { + protected static $instance = []; // 类名映射 protected static $map = []; - // 加载列表 - protected static $load = []; - // 命名空间 - protected static $namespace = []; + // 命名空间别名 protected static $namespaceAlias = []; + // PSR-4 private static $prefixLengthsPsr4 = []; private static $prefixDirsPsr4 = []; + private static $fallbackDirsPsr4 = []; + // PSR-0 - private static $prefixesPsr0 = []; + private static $prefixesPsr0 = []; + private static $fallbackDirsPsr0 = []; + + // 自动加载的文件 + private static $autoloadFiles = []; // 自动加载 public static function autoload($class) @@ -40,55 +47,88 @@ class Loader } } } - // 检查是否定义类库映射 - if (isset(self::$map[$class])) { - if (is_file(self::$map[$class])) { - // 记录加载信息 - APP_DEBUG && self::$load[] = self::$map[$class]; - include self::$map[$class]; - } else { + + if ($file = self::findFile($class)) { + + // Win环境严格区分大小写 + if (IS_WIN && pathinfo($file, PATHINFO_FILENAME) != pathinfo(realpath($file), PATHINFO_FILENAME)) { return false; } - } elseif ($file = self::findFileInComposer($class)) { - // Composer自动加载 - // 记录加载信息 - APP_DEBUG && self::$load[] = $file; - include $file; - } else { - // 命名空间自动加载 - if (!strpos($class, '\\')) { - return false; - } - // 解析命名空间 - list($name, $class) = explode('\\', $class, 2); - if (isset(self::$namespace[$name])) { - // 注册的命名空间 - $path = self::$namespace[$name]; - } elseif (is_dir(EXTEND_PATH . $name)) { - // 扩展类库命名空间 - $path = EXTEND_PATH . $name . DS; - } else { - return false; - } - $filename = $path . str_replace('\\', DS, $class) . EXT; - if (is_file($filename)) { - // 开启调试模式Win环境严格区分大小写 - if (APP_DEBUG && IS_WIN && false === strpos(realpath($filename), $class . EXT)) { - return false; + + __include_file($file); + return true; + } + } + + /** + * 查找文件 + * @param $class + * @return bool + */ + private static function findFile($class) + { + if (!empty(self::$map[$class])) { + // 类库映射 + return self::$map[$class]; + } + + // 查找 PSR-4 + $logicalPathPsr4 = strtr($class, '\\', DS) . EXT; + + $first = $class[0]; + if (isset(self::$prefixLengthsPsr4[$first])) { + foreach (self::$prefixLengthsPsr4[$first] as $prefix => $length) { + if (0 === strpos($class, $prefix)) { + foreach (self::$prefixDirsPsr4[$prefix] as $dir) { + if (is_file($file = $dir . DS . substr($logicalPathPsr4, $length))) { + return $file; + } + } } - // 记录加载信息 - APP_DEBUG && self::$load[] = $filename; - include $filename; - } else { - Log::record('autoloader error : ' . $filename, 'notice'); - return false; } } - return true; + + // 查找 PSR-4 fallback dirs + foreach (self::$fallbackDirsPsr4 as $dir) { + if (is_file($file = $dir . DS . $logicalPathPsr4)) { + return $file; + } + } + + // 查找 PSR-0 + if (false !== $pos = strrpos($class, '\\')) { + // namespaced class name + $logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1) + . strtr(substr($logicalPathPsr4, $pos + 1), '_', DS); + } else { + // PEAR-like class name + $logicalPathPsr0 = strtr($class, '_', DS) . EXT; + } + + if (isset(self::$prefixesPsr0[$first])) { + foreach (self::$prefixesPsr0[$first] as $prefix => $dirs) { + if (0 === strpos($class, $prefix)) { + foreach ($dirs as $dir) { + if (is_file($file = $dir . DS . $logicalPathPsr0)) { + return $file; + } + } + } + } + } + + // 查找 PSR-0 fallback dirs + foreach (self::$fallbackDirsPsr0 as $dir) { + if (is_file($file = $dir . DS . $logicalPathPsr0)) { + return $file; + } + } + + return self::$map[$class] = false; } // 注册classmap - public static function addMap($class, $map = '') + public static function addClassMap($class, $map = '') { if (is_array($class)) { self::$map = array_merge(self::$map, $class); @@ -101,17 +141,98 @@ class Loader public static function addNamespace($namespace, $path = '') { if (is_array($namespace)) { - self::$namespace = array_merge(self::$namespace, $namespace); + foreach ($namespace as $prefix => $paths) { + self::addPsr4($prefix . '\\', rtrim($paths, DS), true); + } } else { - self::$namespace[$namespace] = $path; + self::addPsr4($namespace . '\\', rtrim($path, DS), true); } } + // 添加Ps0空间 + private static function addPsr0($prefix, $paths, $prepend = false) + { + if (!$prefix) { + if ($prepend) { + self::$fallbackDirsPsr0 = array_merge( + (array)$paths, + self::$fallbackDirsPsr0 + ); + } else { + self::$fallbackDirsPsr0 = array_merge( + self::$fallbackDirsPsr0, + (array)$paths + ); + } + + return; + } + + $first = $prefix[0]; + if (!isset(self::$prefixesPsr0[$first][$prefix])) { + self::$prefixesPsr0[$first][$prefix] = (array)$paths; + + return; + } + if ($prepend) { + self::$prefixesPsr0[$first][$prefix] = array_merge( + (array)$paths, + self::$prefixesPsr0[$first][$prefix] + ); + } else { + self::$prefixesPsr0[$first][$prefix] = array_merge( + self::$prefixesPsr0[$first][$prefix], + (array)$paths + ); + } + } + + + // 添加Psr4空间 + private static function addPsr4($prefix, $paths, $prepend = false) + { + if (!$prefix) { + // Register directories for the root namespace. + if ($prepend) { + self::$fallbackDirsPsr4 = array_merge( + (array)$paths, + self::$fallbackDirsPsr4 + ); + } else { + self::$fallbackDirsPsr4 = array_merge( + self::$fallbackDirsPsr4, + (array)$paths + ); + } + } elseif (!isset(self::$prefixDirsPsr4[$prefix])) { + // Register directories for a new namespace. + $length = strlen($prefix); + if ('\\' !== $prefix[$length - 1]) { + throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); + } + self::$prefixLengthsPsr4[$prefix[0]][$prefix] = $length; + self::$prefixDirsPsr4[$prefix] = (array)$paths; + } elseif ($prepend) { + // Prepend directories for an already registered namespace. + self::$prefixDirsPsr4[$prefix] = array_merge( + (array)$paths, + self::$prefixDirsPsr4[$prefix] + ); + } else { + // Append directories for an already registered namespace. + self::$prefixDirsPsr4[$prefix] = array_merge( + self::$prefixDirsPsr4[$prefix], + (array)$paths + ); + } + } + + // 注册命名空间别名 public static function addNamespaceAlias($namespace, $original = '') { if (is_array($namespace)) { - self::$namespaceAlias = array_merge(self::$namespace, $namespace); + self::$namespaceAlias = array_merge(self::$namespaceAlias, $namespace); } else { self::$namespaceAlias[$namespace] = $original; } @@ -121,9 +242,25 @@ class Loader public static function register($autoload = '') { // 注册系统自动加载 - spl_autoload_register($autoload ? $autoload : 'think\\Loader::autoload'); - // 注册composer自动加载 - self::registerComposerLoader(); + spl_autoload_register($autoload ?: 'think\\Loader::autoload', true, true); + // 注册命名空间定义 + self::addNamespace([ + 'think' => LIB_PATH . 'think' . DS, + 'behavior' => LIB_PATH . 'behavior' . DS, + 'traits' => LIB_PATH . 'traits' . DS, + ]); + // 加载类库映射文件 + if (is_file(RUNTIME_PATH . 'classmap' . EXT)) { + self::addClassMap(__include_file(RUNTIME_PATH . 'classmap' . EXT)); + } + + // Composer自动加载支持 + if (is_dir(VENDOR_PATH . 'composer')) { + self::registerComposerLoader(); + } + + // 自动加载extend目录 + self::$fallbackDirsPsr4[] = rtrim(EXTEND_PATH, DS); } // 注册composer自动加载 @@ -132,110 +269,60 @@ class Loader if (is_file(VENDOR_PATH . 'composer/autoload_namespaces.php')) { $map = require VENDOR_PATH . 'composer/autoload_namespaces.php'; foreach ($map as $namespace => $path) { - self::$prefixesPsr0[$namespace[0]][$namespace] = (array) $path; + self::addPsr0($namespace, $path); } } if (is_file(VENDOR_PATH . 'composer/autoload_psr4.php')) { $map = require VENDOR_PATH . 'composer/autoload_psr4.php'; foreach ($map as $namespace => $path) { - $length = strlen($namespace); - if ('\\' !== $namespace[$length - 1]) { - throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); - } - self::$prefixLengthsPsr4[$namespace[0]][$namespace] = $length; - self::$prefixDirsPsr4[$namespace] = (array) $path; + self::addPsr4($namespace, $path); } } if (is_file(VENDOR_PATH . 'composer/autoload_classmap.php')) { $classMap = require VENDOR_PATH . 'composer/autoload_classmap.php'; if ($classMap) { - self::addMap($classMap); + self::addClassMap($classMap); } } if (is_file(VENDOR_PATH . 'composer/autoload_files.php')) { $includeFiles = require VENDOR_PATH . 'composer/autoload_files.php'; foreach ($includeFiles as $fileIdentifier => $file) { - self::composerRequire($fileIdentifier, $file); - } - } - } - - private static function composerRequire($fileIdentifier, $file) - { - if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) { - require $file; - $GLOBALS['__composer_autoload_files'][$fileIdentifier] = true; - } - } - - private static function findFileInComposer($class, $ext = '.php') - { - // PSR-4 lookup - $logicalPathPsr4 = strtr($class, '\\', DS) . $ext; - - $first = $class[0]; - if (isset(self::$prefixLengthsPsr4[$first])) { - foreach (self::$prefixLengthsPsr4[$first] as $prefix => $length) { - if (0 === strpos($class, $prefix)) { - foreach (self::$prefixDirsPsr4[$prefix] as $dir) { - if (file_exists($file = $dir . DS . substr($logicalPathPsr4, $length))) { - return $file; - } - } + if (empty(self::$autoloadFiles[$fileIdentifier])) { + __require_file($file); + self::$autoloadFiles[$fileIdentifier] = true; } } } - // PSR-0 lookup - if (false !== $pos = strrpos($class, '\\')) { - // namespaced class name - $logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1) - . strtr(substr($logicalPathPsr4, $pos + 1), '_', DS); - } else { - // PEAR-like class name - $logicalPathPsr0 = strtr($class, '_', DS) . $ext; - } - - if (isset(self::$prefixesPsr0[$first])) { - foreach (self::$prefixesPsr0[$first] as $prefix => $dirs) { - if (0 === strpos($class, $prefix)) { - foreach ($dirs as $dir) { - if (file_exists($file = $dir . DS . $logicalPathPsr0)) { - return $file; - } - } - } - } - } - // Remember that this class does not exist. - return self::$map[$class] = false; } /** * 导入所需的类库 同java的Import 本函数有缓存功能 - * @param string $class 类库命名空间字符串 + * @param string $class 类库命名空间字符串 * @param string $baseUrl 起始路径 - * @param string $ext 导入的文件扩展名 + * @param string $ext 导入的文件扩展名 * @return boolean */ public static function import($class, $baseUrl = '', $ext = EXT) { static $_file = []; - $class = str_replace(['.', '#'], [DS, '.'], $class); - if (isset($_file[$class . $baseUrl])) { + $key = $class . $baseUrl; + $class = str_replace(['.', '#'], [DS, '.'], $class); + if (isset($_file[$key])) { return true; } if (empty($baseUrl)) { list($name, $class) = explode(DS, $class, 2); - if (isset(self::$namespace[$name])) { + + if (isset(self::$prefixDirsPsr4[$name . '\\'])) { // 注册的命名空间 - $baseUrl = self::$namespace[$name]; + $baseUrl = self::$prefixDirsPsr4[$name . '\\']; } elseif ('@' == $name) { //加载当前模块应用类库 - $baseUrl = MODULE_PATH; + $baseUrl = App::$modulePath; } elseif (is_dir(EXTEND_PATH . $name)) { $baseUrl = EXTEND_PATH; } else { @@ -246,14 +333,24 @@ class Loader $baseUrl .= DS; } // 如果类存在 则导入类库文件 - $filename = $baseUrl . $class . $ext; - if (is_file($filename)) { + if (is_array($baseUrl)) { + foreach ($baseUrl as $path) { + $filename = $path . DS . $class . $ext; + if (is_file($filename)) { + break; + } + } + } else { + $filename = $baseUrl . $class . $ext; + } + + if (!empty($filename) && is_file($filename)) { // 开启调试模式Win环境严格区分大小写 - if (APP_DEBUG && IS_WIN && false === strpos(realpath($filename), $class . $ext)) { + if (IS_WIN && pathinfo($filename, PATHINFO_FILENAME) != pathinfo(realpath($filename), PATHINFO_FILENAME)) { return false; } - include $filename; - $_file[$class . $baseUrl] = true; + __include_file($filename); + $_file[$key] = true; return true; } return false; @@ -261,103 +358,98 @@ class Loader /** * 实例化(分层)模型 - * @param string $name Model名称 - * @param string $layer 业务层名称 - * @param bool $appendSuffix 是否添加类名后缀 + * @param string $name Model名称 + * @param string $layer 业务层名称 + * @param bool $appendSuffix 是否添加类名后缀 + * @param string $common 公共模块名 * @return Object + * @throws ClassNotFoundException */ - public static function model($name = '', $layer = MODEL_LAYER, $appendSuffix = false) + public static function model($name = '', $layer = 'model', $appendSuffix = false, $common = 'common') { - static $_model = []; - if (isset($_model[$name . $layer])) { - return $_model[$name . $layer]; + if (isset(self::$instance[$name . $layer])) { + return self::$instance[$name . $layer]; } if (strpos($name, '/')) { list($module, $name) = explode('/', $name, 2); } else { - $module = APP_MULTI_MODULE ? MODULE_NAME : ''; + $module = Request::instance()->module(); } $class = self::parseClass($module, $layer, $name, $appendSuffix); if (class_exists($class)) { $model = new $class(); } else { - $class = str_replace('\\' . $module . '\\', '\\' . COMMON_MODULE . '\\', $class); + $class = str_replace('\\' . $module . '\\', '\\' . $common . '\\', $class); if (class_exists($class)) { $model = new $class(); } else { - throw new Exception('class [ ' . $class . ' ] not exists', 10001); + throw new ClassNotFoundException('class not exists:' . $class, $class); } } - $_model[$name . $layer] = $model; + self::$instance[$name . $layer] = $model; return $model; } /** * 实例化(分层)控制器 格式:[模块名/]控制器名 - * @param string $name 资源地址 - * @param string $layer 控制层名称 - * @param bool $appendSuffix 是否添加类名后缀 - * @param string $empty 空控制器名称 + * @param string $name 资源地址 + * @param string $layer 控制层名称 + * @param bool $appendSuffix 是否添加类名后缀 + * @param string $empty 空控制器名称 * @return Object|false + * @throws ClassNotFoundException */ - public static function controller($name, $layer = '', $appendSuffix = false, $empty = '') + public static function controller($name, $layer = 'controller', $appendSuffix = false, $empty = '') { - static $_instance = []; - $layer = $layer ?: CONTROLLER_LAYER; - if (isset($_instance[$name . $layer])) { - return $_instance[$name . $layer]; - } if (strpos($name, '/')) { list($module, $name) = explode('/', $name); } else { - $module = APP_MULTI_MODULE ? MODULE_NAME : ''; + $module = Request::instance()->module(); } $class = self::parseClass($module, $layer, $name, $appendSuffix); if (class_exists($class)) { - $action = new $class; - $_instance[$name . $layer] = $action; - return $action; + return new $class(Request::instance()); } elseif ($empty && class_exists($emptyClass = self::parseClass($module, $layer, $empty, $appendSuffix))) { - return new $emptyClass; - } else { - throw new Exception('class [ ' . $class . ' ] not exists', 10001); + return new $emptyClass(Request::instance()); } } /** * 实例化验证类 格式:[模块名/]验证器名 - * @param string $name 资源地址 - * @param string $layer 验证层名称 - * @param bool $appendSuffix 是否添加类名后缀 + * @param string $name 资源地址 + * @param string $layer 验证层名称 + * @param bool $appendSuffix 是否添加类名后缀 + * @param string $common 公共模块名 * @return Object|false + * @throws ClassNotFoundException */ - public static function validate($name = '', $layer = '', $appendSuffix = false) + public static function validate($name = '', $layer = 'validate', $appendSuffix = false, $common = 'common') { + $name = $name ?: Config::get('default_validate'); if (empty($name)) { return new Validate; } - static $_instance = []; - $layer = $layer ?: VALIDATE_LAYER; - if (isset($_instance[$name . $layer])) { - return $_instance[$name . $layer]; + + if (isset(self::$instance[$name . $layer])) { + return self::$instance[$name . $layer]; } if (strpos($name, '/')) { list($module, $name) = explode('/', $name); } else { - $module = APP_MULTI_MODULE ? MODULE_NAME : ''; + $module = Request::instance()->module(); } $class = self::parseClass($module, $layer, $name, $appendSuffix); if (class_exists($class)) { $validate = new $class; } else { - $class = str_replace('\\' . $module . '\\', '\\' . COMMON_MODULE . '\\', $class); + $class = str_replace('\\' . $module . '\\', '\\' . $common . '\\', $class); if (class_exists($class)) { $validate = new $class; } else { - throw new Exception('class [ ' . $class . ' ] not exists', 10001); + throw new ClassNotFoundException('class not exists:' . $class, $class); } } - $_instance[$name . $layer] = $validate; + self::$instance[$name . $layer] = $validate; return $validate; } @@ -373,17 +465,17 @@ class Loader /** * 远程调用模块的操作方法 参数格式 [模块/控制器/]操作 - * @param string $url 调用地址 - * @param string|array $vars 调用参数 支持字符串和数组 - * @param string $layer 要调用的控制层名称 - * @param bool $appendSuffix 是否添加类名后缀 + * @param string $url 调用地址 + * @param string|array $vars 调用参数 支持字符串和数组 + * @param string $layer 要调用的控制层名称 + * @param bool $appendSuffix 是否添加类名后缀 * @return mixed */ - public static function action($url, $vars = [], $layer = CONTROLLER_LAYER, $appendSuffix = false) + public static function action($url, $vars = [], $layer = 'controller', $appendSuffix = false) { $info = pathinfo($url); $action = $info['basename']; - $module = '.' != $info['dirname'] ? $info['dirname'] : CONTROLLER_NAME; + $module = '.' != $info['dirname'] ? $info['dirname'] : Request::instance()->controller(); $class = self::controller($module, $layer, $appendSuffix); if ($class) { if (is_scalar($vars)) { @@ -396,45 +488,20 @@ class Loader return App::invokeMethod([$class, $action . Config::get('action_suffix')], $vars); } } - /** - * 取得对象实例 支持调用类的静态方法 - * - * @param string $class 对象类名 - * @param string $method 类的静态方法名 - * - * @return mixed - * @throws Exception - */ - public static function instance($class, $method = '') - { - static $_instance = []; - $identify = $class . $method; - if (!isset($_instance[$identify])) { - if (class_exists($class)) { - $o = new $class(); - if (!empty($method) && method_exists($o, $method)) { - $_instance[$identify] = call_user_func_array([ & $o, $method], []); - } else { - $_instance[$identify] = $o; - } - } else { - throw new Exception('class not exist :' . $class, 10007); - } - } - return $_instance[$identify]; - } /** * 字符串命名风格转换 * type 0 将Java风格转换为C的风格 1 将C风格转换为Java的风格 - * @param string $name 字符串 + * @param string $name 字符串 * @param integer $type 转换类型 * @return string */ public static function parseName($name, $type = 0) { if ($type) { - return ucfirst(preg_replace_callback('/_([a-zA-Z])/', function ($match) {return strtoupper($match[1]);}, $name)); + return ucfirst(preg_replace_callback('/_([a-zA-Z])/', function ($match) { + return strtoupper($match[1]); + }, $name)); } else { return strtolower(trim(preg_replace("/[A-Z]/", "_\\0", $name), "_")); } @@ -443,16 +510,42 @@ class Loader /** * 解析应用类的类名 * @param string $module 模块名 - * @param string $layer 层名 controller model ... - * @param string $name 类名 + * @param string $layer 层名 controller model ... + * @param string $name 类名 + * @param bool $appendSuffix * @return string */ public static function parseClass($module, $layer, $name, $appendSuffix = false) { $name = str_replace(['/', '.'], '\\', $name); $array = explode('\\', $name); - $class = self::parseName(array_pop($array), 1) . (CLASS_APPEND_SUFFIX || $appendSuffix ? ucfirst($layer) : ''); + $class = self::parseName(array_pop($array), 1) . (App::$suffix || $appendSuffix ? ucfirst($layer) : ''); $path = $array ? implode('\\', $array) . '\\' : ''; - return APP_NAMESPACE . '\\' . (APP_MULTI_MODULE ? $module . '\\' : '') . $layer . '\\' . $path . $class; + return App::$namespace . '\\' . ($module ? $module . '\\' : '') . $layer . '\\' . $path . $class; + } + + /** + * 初始化类的实例 + * @return void + */ + public static function clearInstance() + { + self::$instance = []; } } + +/** + * 作用范围隔离 + * + * @param $file + * @return mixed + */ +function __include_file($file) +{ + return include $file; +} + +function __require_file($file) +{ + return require $file; +} \ No newline at end of file diff --git a/library/think/Log.php b/library/think/Log.php index 7b41c6ee..135f6547 100644 --- a/library/think/Log.php +++ b/library/think/Log.php @@ -11,6 +11,19 @@ namespace think; +use think\exception\ClassNotFoundException; + +/** + * Class Log + * @package think + * + * @method void log($msg) static + * @method void error($msg) static + * @method void info($msg) static + * @method void sql($msg) static + * @method void notice($msg) static + * @method void alert($msg) static + */ class Log { const LOG = 'log'; @@ -22,62 +35,54 @@ class Log // 日志信息 protected static $log = []; + // 配置参数 + protected static $config = []; // 日志类型 protected static $type = ['log', 'error', 'info', 'sql', 'notice', 'alert']; // 日志写入驱动 - protected static $driver = null; - // 通知发送驱动 - protected static $alarm = null; + protected static $driver; + + // 当前日志授权key + protected static $key; /** * 日志初始化 - * @return void + * @param array $config */ public static function init($config = []) { - $type = isset($config['type']) ? $config['type'] : 'File'; - $class = (!empty($config['namespace']) ? $config['namespace'] : '\\think\\log\\driver\\') . ucwords($type); + $type = isset($config['type']) ? $config['type'] : 'File'; + $class = false !== strpos($type, '\\') ? $type : '\\think\\log\\driver\\' . ucwords($type); + self::$config = $config; unset($config['type']); - self::$driver = new $class($config); + if (class_exists($class)) { + self::$driver = new $class($config); + } else { + throw new ClassNotFoundException('class not exists:' . $class, $class); + } // 记录初始化信息 - APP_DEBUG && Log::record('[ LOG ] INIT ' . $type . ': ' . var_export($config, true), 'info'); + App::$debug && Log::record('[ LOG ] INIT ' . $type . ': ' . var_export($config, true), 'info'); } /** - * 通知初始化 - * @return void - */ - public static function alarm($config = []) - { - $type = isset($config['type']) ? $config['type'] : 'Email'; - $class = (!empty($config['namespace']) ? $config['namespace'] : '\\think\\log\\alarm\\') . ucwords($type); - unset($config['type']); - self::$alarm = new $class($config['alarm']); - // 记录初始化信息 - APP_DEBUG && Log::record('[ CACHE ] ALARM ' . $type . ': ' . var_export($config, true), 'info'); - } - - /** - * 获取全部日志信息 + * 获取日志信息 + * @param string $type 信息类型 * @return array */ - public static function getLog() + public static function getLog($type = '') { - return self::$log; + return $type ? self::$log[$type] : self::$log; } /** * 记录调试信息 - * @param mixed $msg 调试信息 + * @param mixed $msg 调试信息 * @param string $type 信息类型 * @return void */ public static function record($msg, $type = 'log') { - if (!is_string($msg)) { - $msg = var_export($msg, true); - } - self::$log[] = ['type' => $type, 'msg' => $msg]; + self::$log[$type][] = $msg; } /** @@ -89,34 +94,88 @@ class Log self::$log = []; } + /** + * 当前日志记录的授权key + * @param string $key 授权key + * @return void + */ + public static function key($key) + { + self::$key = $key; + } + + /** + * 检查日志写入权限 + * @param array $config 当前日志配置参数 + * @return bool + */ + public static function check($config) + { + if (self::$key && !empty($config['allow_key']) && !in_array(self::$key, $config['allow_key'])) { + return false; + } + return true; + } + /** * 保存调试信息 * @return bool */ public static function save() { - if (is_null(self::$driver)) { - self::init(Config::get('log')); + if (!empty(self::$log)) { + if (is_null(self::$driver)) { + self::init(Config::get('log')); + } + + if (!self::check(self::$config)) { + // 检测日志写入权限 + return false; + } + + if (empty(self::$config['level'])) { + // 获取全部日志 + $log = self::$log; + } else { + // 记录允许级别 + $log = []; + foreach (self::$config['level'] as $level) { + if (isset(self::$log[$level])) { + $log[$level] = self::$log[$level]; + } + } + } + + $result = self::$driver->save($log); + if ($result) { + self::$log = []; + } + + return $result; } - return self::$driver->save(self::$log); + return true; } /** * 实时写入日志信息 并支持行为 - * @param mixed $msg 调试信息 + * @param mixed $msg 调试信息 * @param string $type 信息类型 + * @param bool $force 是否强制写入 * @return bool */ - public static function write($msg, $type = 'log') + public static function write($msg, $type = 'log', $force = false) { - if (!is_string($msg)) { - $msg = var_export($msg, true); - } // 封装日志信息 - $log[] = ['type' => $type, 'msg' => $msg]; + if (true === $force || empty(self::$config['level'])) { + $log[$type][] = $msg; + } elseif (in_array($type, self::$config['level'])) { + $log[$type][] = $msg; + } else { + return false; + } // 监听log_write - APP_HOOK && Hook::listen('log_write', $log); + Hook::listen('log_write', $log); if (is_null(self::$driver)) { self::init(Config::get('log')); } @@ -124,19 +183,11 @@ class Log return self::$driver->save($log); } - /** - * 发送预警通知 - * @param mixed $msg 调试信息 - * @return void - */ - public static function send($msg) - { - self::$alarm && self::$alarm->send($msg); - } - /** * 静态调用 - * @return void + * @param $method + * @param $args + * @return mixed */ public static function __callStatic($method, $args) { diff --git a/library/think/Model.php b/library/think/Model.php index 778fc531..47d422e5 100644 --- a/library/think/Model.php +++ b/library/think/Model.php @@ -11,9 +11,12 @@ namespace think; +use InvalidArgumentException; use think\Cache; use think\Db; use think\db\Query; +use think\Exception; +use think\Exception\ValidateException; use think\Loader; use think\model\Relation; use think\paginator\Collection as PaginatorCollection; @@ -21,23 +24,34 @@ use think\paginator\Collection as PaginatorCollection; /** * Class Model * @package think - * @method PaginatorCollection paginate(integer $listRows = 15, boolean $simple = false, array $config = []) static 分页查询 + * @method static PaginatorCollection paginate(integer $listRows = 15, boolean $simple = false, array $config = []) 分页查询 + * @method static mixed value($field, $default = null) 得到某个字段的值 + * @method static array column($field, $key = '') 得到某个列的数组 + * @method static integer count($field = '*') COUNT查询 + * @method static integer sum($field = '*') SUM查询 + * @method static integer min($field = '*') MIN查询 + * @method static integer max($field = '*') MAX查询 + * @method static integer avg($field = '*') AVG查询 + * @method static setField($field, $value = '') + * @method static Query where($field, $op = null, $condition = null) 指定AND查询条件 + * @method static static findOrFail($data = null) 查找单条记录 如果不存在则抛出异常 + * */ abstract class Model implements \JsonSerializable, \ArrayAccess { // 数据库对象池 private static $links = []; - // 对象实例 - private static $instance = []; // 数据库配置 protected $connection = []; // 当前模型名称 protected $name; // 数据表名称 protected $table; + // 当前类名称 + protected $class; // 回调事件 - protected static $event = []; + private static $event = []; // 数据表主键 复合主键使用数组定义 protected $pk; @@ -48,12 +62,14 @@ abstract class Model implements \JsonSerializable, \ArrayAccess // 字段属性 protected $field = []; + // 显示属性 + protected $visible = []; // 隐藏属性 protected $hidden = []; + // 追加属性 + protected $append = []; // 数据信息 protected $data = []; - // 缓存数据 - protected $cache = []; // 记录改变字段 protected $change = []; @@ -63,11 +79,16 @@ abstract class Model implements \JsonSerializable, \ArrayAccess protected $insert = []; // 更新自动完成列表 protected $update = []; - // 自动写入的时间戳字段列表 - protected $autoTimeField = ['create_time', 'update_time', 'delete_time']; + // 是否需要自动写入时间戳 如果设置为字符串 则表示时间字段的类型 + protected $autoWriteTimestamp; + // 创建时间字段 + protected $createTime = 'create_time'; + // 更新时间字段 + protected $updateTime = 'update_time'; + // 删除时间字段 + protected $deleteTime = 'delete_time'; // 时间字段取出后的默认时间格式 protected $dateFormat = 'Y-m-d H:i:s'; - // 字段类型或者格式转换 protected $type = []; // 是否为更新数据 @@ -76,6 +97,8 @@ abstract class Model implements \JsonSerializable, \ArrayAccess protected $updateWhere; // 当前执行的关联对象 protected $relation; + // 验证失败是否抛出异常 + protected $failException = false; /** * 初始化过的模型. @@ -96,17 +119,75 @@ abstract class Model implements \JsonSerializable, \ArrayAccess } else { $this->data = $data; } + + // 当前类名 + $this->class = get_class($this); + if (empty($this->name)) { - $this->name = basename(str_replace('\\', '/', get_class($this))); + // 当前模型名 + $this->name = basename(str_replace('\\', '/', $this->class)); } + if (is_null($this->autoWriteTimestamp)) { + // 自动写入时间戳 + $this->autoWriteTimestamp = $this->db()->getConfig('auto_timestamp'); + } + + // 执行初始化操作 $this->initialize(); - $this->relation = new Relation($this); + } + + /** + * 获取当前模型的数据库查询对象 + * @access public + * @return Query + */ + public function db() + { + $model = $this->class; + if (!isset(self::$links[$model])) { + // 设置当前模型 确保查询返回模型对象 + $query = Db::connect($this->connection)->model($model); + + // 设置当前数据表和模型名 + if (!empty($this->table)) { + $query->setTable($this->table); + } else { + $query->name($this->name); + } + // 全局作用域 + if (method_exists($this, 'base')) { + call_user_func_array([$this, 'base'], [ & $query]); + } + self::$links[$model] = $query; + } + // 返回当前模型的数据库查询对象 + return self::$links[$model]; + } + + /** + * 获取关联模型实例 + * @access protected + * @param string|array $relation 关联查询 + * @return Relation|Query + */ + protected function relation($relation = null) + { + if (!is_null($relation)) { + // 执行关联查询 + return $this->db()->relation($relation); + } + + // 获取关联对象实例 + if (is_null($this->relation)) { + $this->relation = new Relation($this); + } + return $this->relation; } /** * 初始化模型 - * + * @access protected * @return void */ protected function initialize() @@ -120,7 +201,7 @@ abstract class Model implements \JsonSerializable, \ArrayAccess /** * 初始化处理 - * + * @access protected * @return void */ protected static function init() @@ -129,33 +210,257 @@ abstract class Model implements \JsonSerializable, \ArrayAccess /** * 设置数据对象值 * @access public - * @param mixed $data 数据 + * @param mixed $data 数据或者属性名 + * @param mixed $value 值 * @return $this */ - public function data($data = '') + public function data($data, $value = null) { - if (is_object($data)) { - $data = get_object_vars($data); - } elseif (!is_array($data)) { - throw new Exception('data type invalid', 10300); + if (is_string($data)) { + $this->data[$data] = $value; + } else { + if (is_object($data)) { + $data = get_object_vars($data); + } + if (true === $value) { + // 数据对象赋值 + foreach ($data as $key => $value) { + $this->setAttr($key, $value, $data); + } + } else { + $this->data = $data; + } } - $this->data = $data; return $this; } /** - * 获取对象原始数据 + * 获取对象原始数据 如果不存在指定字段返回false * @access public * @param string $name 字段名 留空获取全部 - * @return array + * @return mixed + * @throws InvalidArgumentException */ - public function getData($name = '') + public function getData($name = null) { - return isset($this->data[$name]) ? $this->data[$name] : $this->data; + if (is_null($name)) { + return $this->data; + } elseif (array_key_exists($name, $this->data)) { + return $this->data[$name]; + } else { + throw new InvalidArgumentException('property not exists:' . $this->class . '->' . $name); + } } /** - * 设置需要隐藏的属性 + * 修改器 设置数据对象值 + * @access public + * @param string $name 属性名 + * @param mixed $value 属性值 + * @param array $data 数据 + * @return $this + */ + public function setAttr($name, $value, $data = []) + { + if (is_null($value) && $this->autoWriteTimestamp && in_array($name, [$this->createTime, $this->updateTime, $this->deleteTime])) { + // 自动写入的时间戳字段 + if (isset($this->type[$name])) { + $type = $this->type[$name]; + if (strpos($type, ':')) { + list($type, $param) = explode(':', $type, 2); + } + switch ($type) { + case 'datetime': + $format = !empty($param) ? $param : $this->dateFormat; + $value = date($format, $_SERVER['REQUEST_TIME']); + break; + case 'timestamp': + $value = $_SERVER['REQUEST_TIME']; + break; + } + } elseif (is_string($this->autoWriteTimestamp) && in_array(strtolower($this->autoWriteTimestamp), ['datetime', 'date', 'timestamp'])) { + $value = date($this->dateFormat, $_SERVER['REQUEST_TIME']); + } else { + $value = $_SERVER['REQUEST_TIME']; + } + } else { + // 检测修改器 + $method = 'set' . Loader::parseName($name, 1) . 'Attr'; + if (method_exists($this, $method)) { + $value = $this->$method($value, array_merge($data, $this->data)); + } elseif (isset($this->type[$name])) { + // 类型转换 + $value = $this->writeTransform($value, $this->type[$name]); + } + } + + // 标记字段更改 + if (!isset($this->data[$name]) || ($this->data[$name] != $value && !in_array($name, $this->change))) { + $this->change[] = $name; + } + // 设置数据对象属性 + $this->data[$name] = $value; + return $this; + } + + /** + * 数据写入 类型转换 + * @access public + * @param mixed $value 值 + * @param string|array $type 要转换的类型 + * @return mixed + */ + protected function writeTransform($value, $type) + { + if (is_array($type)) { + list($type, $param) = $type; + } elseif (strpos($type, ':')) { + list($type, $param) = explode(':', $type, 2); + } + switch ($type) { + case 'integer': + $value = (int) $value; + break; + case 'float': + if (empty($param)) { + $value = (float) $value; + } else { + $value = (float) number_format($value, $param); + } + break; + case 'boolean': + $value = (bool) $value; + break; + case 'timestamp': + if (!is_numeric($value)) { + $value = strtotime($value); + } + break; + case 'datetime': + $format = !empty($param) ? $param : $this->dateFormat; + $value = date($format, is_numeric($value) ? $value : strtotime($value)); + break; + case 'object': + if (is_object($value)) { + $value = json_encode($value, JSON_FORCE_OBJECT); + } + break; + case 'array': + $value = (array) $value; + case 'json': + $option = !empty($param) ? (int) $param : JSON_UNESCAPED_UNICODE; + $value = json_encode($value, $option); + break; + case 'serialize': + $value = serialize($value); + break; + } + return $value; + } + + /** + * 获取器 获取数据对象的值 + * @access public + * @param string $name 名称 + * @return mixed + * @throws InvalidArgumentException + */ + public function getAttr($name) + { + try { + $notFound = false; + $value = $this->getData($name); + } catch (InvalidArgumentException $e) { + $notFound = true; + $value = null; + } + + // 检测属性获取器 + $method = 'get' . Loader::parseName($name, 1) . 'Attr'; + if (method_exists($this, $method)) { + $value = $this->$method($value, $this->data); + } elseif (isset($this->type[$name])) { + // 类型转换 + $value = $this->readTransform($value, $this->type[$name]); + } elseif ($notFound) { + if (method_exists($this, $name) && !method_exists('\think\Model', $name)) { + // 不存在该字段 获取关联数据 + $value = $this->relation()->getRelation($name); + // 保存关联对象值 + $this->data[$name] = $value; + } else { + throw new InvalidArgumentException('property not exists:' . $this->class . '->' . $name); + } + } + return $value; + } + + /** + * 数据读取 类型转换 + * @access public + * @param mixed $value 值 + * @param string|array $type 要转换的类型 + * @return mixed + */ + protected function readTransform($value, $type) + { + if (is_array($type)) { + list($type, $param) = $type; + } elseif (strpos($type, ':')) { + list($type, $param) = explode(':', $type, 2); + } + switch ($type) { + case 'integer': + $value = (int) $value; + break; + case 'float': + if (empty($param)) { + $value = (float) $value; + } else { + $value = (float) number_format($value, $param); + } + break; + case 'boolean': + $value = (bool) $value; + break; + case 'timestamp': + $format = !empty($param) ? $param : $this->dateFormat; + $value = date($format, $value); + break; + case 'datetime': + $format = !empty($param) ? $param : $this->dateFormat; + $value = date($format, strtotime($value)); + break; + case 'json': + $value = json_decode($value, true); + break; + case 'array': + $value = is_null($value) ? [] : json_decode($value, true); + break; + case 'object': + $value = empty($value) ? new \stdClass() : json_decode($value); + break; + case 'serialize': + $value = unserialize($value); + break; + } + return $value; + } + + /** + * 设置需要追加的输出属性 + * @access public + * @param array $append 属性列表 + * @return $this + */ + public function append($append = []) + { + $this->append = $append; + return $this; + } + + /** + * 设置需要隐藏的输出属性 * @access public * @param array $hidden 属性列表 * @return $this @@ -166,6 +471,17 @@ abstract class Model implements \JsonSerializable, \ArrayAccess return $this; } + /** + * 设置需要输出的属性 + * @param array $visible + * @return $this + */ + public function visible($visible = []) + { + $this->visible = $visible; + return $this; + } + /** * 转换当前模型对象为数组 * @access public @@ -174,25 +490,36 @@ abstract class Model implements \JsonSerializable, \ArrayAccess public function toArray() { $item = []; - foreach ($this->data as $key => $val) { - // 如果是隐藏属性不输出 - if (in_array($key, $this->hidden)) { - continue; - } + //过滤属性 + if (!empty($this->visible)) { + $data = array_intersect_key($this->data, array_flip($this->visible)); + } elseif (!empty($this->hidden)) { + $data = array_diff_key($this->data, array_flip($this->hidden)); + } else { + $data = $this->data; + } + + foreach ($data as $key => $val) { if ($val instanceof Model || $val instanceof Collection) { // 关联模型对象 $item[$key] = $val->toArray(); - } elseif (is_array($val) && isset($val[0]) && $val[0] instanceof Model) { + } elseif (is_array($val) && reset($val) instanceof Model) { // 关联模型数据集 - $data = []; + $arr = []; foreach ($val as $k => $value) { - $data[$k] = $value->toArray(); + $arr[$k] = $value->toArray(); } - $item[$key] = $data; + $item[$key] = $arr; } else { // 模型属性 - $item[$key] = $this->__get($key); + $item[$key] = $this->getAttr($key); + } + } + // 追加属性(必须定义获取器) + if (!empty($this->append)) { + foreach ($this->append as $name) { + $item[$name] = $this->getAttr($name); } } return !empty($item) ? $item : []; @@ -201,7 +528,7 @@ abstract class Model implements \JsonSerializable, \ArrayAccess /** * 转换当前模型对象为JSON字符串 * @access public - * @param integer $options json参数 + * @param integer $options json参数 * @return string */ public function toJson($options = JSON_UNESCAPED_UNICODE) @@ -210,15 +537,18 @@ abstract class Model implements \JsonSerializable, \ArrayAccess } /** - * 获取当前模型对象的主键 + * 获取模型对象的主键 * @access public - * @param string $table 数据表名 + * @param string $name 模型名 * @return mixed */ - public function getPk($table = '') + public function getPk($name = '') { - if (empty($this->pk)) { - $this->pk = self::db()->getTableInfo($table, 'pk'); + if (!empty($name)) { + $table = $this->db()->getTable($name); + return $this->db()->getPk($table); + } elseif (empty($this->pk)) { + $this->pk = $this->db()->getPk(); } return $this->pk; } @@ -243,29 +573,33 @@ abstract class Model implements \JsonSerializable, \ArrayAccess /** * 保存当前数据对象 * @access public - * @param array $data 数据 - * @param array $where 更新条件 - * @param bool $getId 新增的时候是否获取id + * @param array $data 数据 + * @param array $where 更新条件 + * @param bool $getId 新增的时候是否获取id + * @param bool $replace 是否replace * @return integer */ - public function save($data = [], $where = [], $getId = true) + public function save($data = [], $where = [], $getId = true, $replace = false) { if (!empty($data)) { + // 数据自动验证 + if (!$this->validateData($data)) { + return false; + } // 数据对象赋值 foreach ($data as $key => $value) { - $this->__set($key, $value); + $this->setAttr($key, $value, $data); } if (!empty($where)) { $this->isUpdate = true; } } - // 数据自动验证 - if (!$this->validateData()) { - return false; - } // 检测字段 if (!empty($this->field)) { + if (true === $this->field) { + $this->field = $this->db()->getTableInfo('', 'fields'); + } foreach ($this->data as $key => $val) { if (!in_array($key, $this->field)) { unset($this->data[$key]); @@ -276,6 +610,11 @@ abstract class Model implements \JsonSerializable, \ArrayAccess // 数据自动完成 $this->autoCompleteData($this->auto); + // 自动写入更新时间 + if ($this->autoWriteTimestamp && $this->updateTime) { + $this->setAttr($this->updateTime, null); + } + // 事件回调 if (false === $this->trigger('before_write', $this)) { return false; @@ -284,6 +623,7 @@ abstract class Model implements \JsonSerializable, \ArrayAccess if ($this->isUpdate) { // 自动更新 $this->autoCompleteData($this->update); + // 事件回调 if (false === $this->trigger('before_update', $this)) { return false; @@ -308,60 +648,76 @@ abstract class Model implements \JsonSerializable, \ArrayAccess } } - $result = self::db()->where($where)->update($data); - + $result = $this->db()->where($where)->update($data); + // 清空change + $this->change = []; // 更新回调 $this->trigger('after_update', $this); } else { // 自动写入 $this->autoCompleteData($this->insert); + // 自动写入创建时间 + if ($this->autoWriteTimestamp && $this->createTime) { + $this->setAttr($this->createTime, null); + } + if (false === $this->trigger('before_insert', $this)) { return false; } - $result = self::db()->insert($this->data); + $result = $this->db()->insert($this->data, $replace); // 获取自动增长主键 if ($result && $getId) { - $insertId = self::db()->getLastInsID(); + $insertId = $this->db()->getLastInsID(); $pk = $this->getPk(); if (is_string($pk) && $insertId) { $this->data[$pk] = $insertId; } $result = $insertId; } + // 标记为更新 + $this->isUpdate = true; + // 清空change + $this->change = []; // 新增回调 $this->trigger('after_insert', $this); } // 写入回调 $this->trigger('after_write', $this); - // 标记为更新 - $this->isUpdate = true; - // 清空change - $this->change = []; return $result; } /** * 保存多个数据到当前数据对象 * @access public - * @param array $data 数据 - * @return integer + * @param array $dataSet 数据 + * @param bool $replace 是否replace + * @return array|false */ - public function saveAll($dataSet) + public function saveAll($dataSet, $replace = false) { - foreach ($dataSet as $data) { - $result = $this->isUpdate(false)->save($data, [], false); + $result = []; + $db = $this->db(); + $db->startTrans(); + try { + foreach ($dataSet as $key => $data) { + $result[$key] = self::create($data, $replace); + } + $db->commit(); + return $result; + } catch (\Exception $e) { + $db->rollback(); + throw $e; } - return $result; } /** * 设置允许写入的字段 * @access public - * @param bool $update + * @param bool|array $field 允许写入的字段 如果为true只允许写入数据表字段 * @return $this */ public function allowField($field) @@ -373,7 +729,7 @@ abstract class Model implements \JsonSerializable, \ArrayAccess /** * 是否为更新数据 * @access public - * @param bool $update + * @param bool $update * @param mixed $where * @return $this */ @@ -400,11 +756,7 @@ abstract class Model implements \JsonSerializable, \ArrayAccess $value = null; } if (!in_array($field, $this->change)) { - if(in_array($field, $this->autoTimeField)) { - $this->__set($field, $value); - } else { - $this->__set($field, isset($this->data[$field]) ? $this->data[$field] : $value); - } + $this->setAttr($field, isset($this->data[$field]) ? $this->data[$field] : $value); } } } @@ -420,16 +772,16 @@ abstract class Model implements \JsonSerializable, \ArrayAccess return false; } - $result = self::db()->delete($this->data); + $result = $this->db()->delete($this->data); $this->trigger('after_delete', $this); return $result; } /** - * 设置自动完成的字段 + * 设置自动完成的字段( 规则通过修改器定义) * @access public - * @param array $fields 需要自动完成的字段( 规则通过修改器定义) + * @param array $fields 需要自动完成的字段 * @return $this */ public function auto($fields) @@ -442,7 +794,7 @@ abstract class Model implements \JsonSerializable, \ArrayAccess * 设置字段验证 * @access public * @param array|string|bool $rule 验证规则 true表示自动读取验证器类 - * @param array $msg 提示信息 + * @param array $msg 提示信息 * @return $this */ public function validate($rule = true, $msg = []) @@ -459,16 +811,29 @@ abstract class Model implements \JsonSerializable, \ArrayAccess } /** - * 自动验证当前数据对象值 + * 设置验证失败后是否抛出异常 * @access public + * @param bool $fail 是否抛出异常 + * @return $this + */ + public function validateFailException($fail = true) + { + $this->failException = $fail; + return $this; + } + + /** + * 自动验证数据 + * @access protected + * @param array $data 验证数据 * @return bool */ - public function validateData() + protected function validateData($data) { if (!empty($this->validate)) { $info = $this->validate; if (is_array($info)) { - $validate = Loader::validate(Config::get('default_validate')); + $validate = Loader::validate(); $validate->rule($info['rule']); $validate->message($info['msg']); } else { @@ -481,9 +846,13 @@ abstract class Model implements \JsonSerializable, \ArrayAccess $validate->scene($scene); } } - if (!$validate->check($this->data)) { + if (!$validate->check($data)) { $this->error = $validate->getError(); - return false; + if ($this->failException) { + throw new ValidateException($this->error); + } else { + return false; + } } $this->validate = null; } @@ -503,30 +872,31 @@ abstract class Model implements \JsonSerializable, \ArrayAccess /** * 注册回调方法 * @access public - * @param string $event 事件名 - * @param callable $callback 回调方法 - * @param bool $override 是否覆盖 + * @param string $event 事件名 + * @param callable $callback 回调方法 + * @param bool $override 是否覆盖 * @return void */ public static function event($event, $callback, $override = false) { + $class = get_called_class(); if ($override) { - static::$event[$event] = []; + self::$event[$class][$event] = []; } - static::$event[$event][] = $callback; + self::$event[$class][$event][] = $callback; } /** * 触发事件 * @access protected - * @param string $event 事件名 - * @param mixed $params 传入参数(引用) + * @param string $event 事件名 + * @param mixed $params 传入参数(引用) * @return bool */ protected function trigger($event, &$params) { - if (isset(static::$event[$event])) { - foreach (static::$event[$event] as $callback) { + if (isset(self::$event[$this->class][$event])) { + foreach (self::$event[$this->class][$event] as $callback) { if (is_callable($callback)) { $result = call_user_func_array($callback, [ & $params]); if (false === $result) { @@ -541,21 +911,22 @@ abstract class Model implements \JsonSerializable, \ArrayAccess /** * 写入数据 * @access public - * @param array $data 数据数组 + * @param array $data 数据数组 + * @param bool $replace 是否replace * @return $this */ - public static function create($data = []) + public static function create($data = [], $replace = false) { $model = new static(); - $model->isUpdate(false)->save($data); + $model->isUpdate(false)->save($data, [], true, $replace); return $model; } /** * 更新数据 * @access public - * @param array $data 数据数组 - * @param array $where 更新条件 + * @param array $data 数据数组 + * @param array $where 更新条件 * @return $this */ public static function update($data = [], $where = []) @@ -568,12 +939,13 @@ abstract class Model implements \JsonSerializable, \ArrayAccess /** * 查找单条记录 * @access public - * @param mixed $data 主键值或者查询条件(闭包) - * @param string $with 关联预查询 - * @param bool $cache 是否缓存 - * @return \think\Model + * @param mixed $data 主键值或者查询条件(闭包) + * @param array|string $with 关联预查询 + * @param bool $cache 是否缓存 + * @return static + * @throws exception\DbException */ - public static function get($data = '', $with = [], $cache = false) + public static function get($data = null, $with = [], $cache = false) { $query = self::parseQuery($data, $with, $cache); return $query->find($data); @@ -582,12 +954,13 @@ abstract class Model implements \JsonSerializable, \ArrayAccess /** * 查找所有记录 * @access public - * @param mixed $data 主键列表或者查询条件(闭包) - * @param string $with 关联预查询 - * @param bool $cache 是否缓存 - * @return array|false|string + * @param mixed $data 主键列表或者查询条件(闭包) + * @param array|string $with 关联预查询 + * @param bool $cache 是否缓存 + * @return static[]|false + * @throws exception\DbException */ - public static function all($data = [], $with = [], $cache = false) + public static function all($data = null, $with = [], $cache = false) { $query = self::parseQuery($data, $with, $cache); return $query->select($data); @@ -596,23 +969,23 @@ abstract class Model implements \JsonSerializable, \ArrayAccess /** * 分析查询表达式 * @access public - * @param mixed $data 主键列表或者查询条件(闭包) - * @param string $with 关联预查询 - * @param bool $cache 是否缓存 - * @return \think\db\Query + * @param mixed $data 主键列表或者查询条件(闭包) + * @param string $with 关联预查询 + * @param bool $cache 是否缓存 + * @return Query */ protected static function parseQuery(&$data, $with, $cache) { $result = self::with($with)->cache($cache); if (is_array($data) && key($data) !== 0) { $result = $result->where($data); - $data = []; + $data = null; } elseif ($data instanceof \Closure) { call_user_func_array($data, [ & $result]); - $data = []; + $data = null; } elseif ($data instanceof Query) { $result = $data->with($with)->cache($cache); - $data = []; + $data = null; } return $result; } @@ -621,46 +994,55 @@ abstract class Model implements \JsonSerializable, \ArrayAccess * 删除记录 * @access public * @param mixed $data 主键列表 支持闭包查询条件 - * @return integer + * @return integer 成功删除的记录数 */ public static function destroy($data) { - $db = self::db(); - if ($data instanceof \Closure) { - call_user_func_array($data, [ & $db]); - $data = []; + $model = new static(); + $query = $model->db(); + if (is_array($data) && key($data) !== 0) { + $query->where($data); + $data = null; + } elseif ($data instanceof \Closure) { + call_user_func_array($data, [ & $query]); + $data = null; } - $resultSet = $db->select($data); - $result = false; + $resultSet = $query->select($data); + $count = 0; if ($resultSet) { foreach ($resultSet as $data) { $result = $data->delete(); + $count += $result; } } - return $result; + return $count; } /** * 命名范围 * @access public - * @param string|Closure $name 命名范围名称 逗号分隔 - * @param mixed $params 参数调用 - * @return \think\Model + * @param string|array|Closure $name 命名范围名称 逗号分隔 + * @param mixed ...$params 参数调用 + * @return Model */ - public static function scope($name, $params = []) + public static function scope($name) { - $model = new static(); - $class = self::db(); - if ($name instanceof \Closure) { - call_user_func_array($name, [ & $class, $params]); - } elseif ($name instanceof Query) { + if ($name instanceof Query) { return $name; - } else { - $names = explode(',', $name); - foreach ($names as $scope) { - $method = 'scope' . $scope; + } + $model = new static(); + $params = func_get_args(); + $params[0] = $model->db(); + if ($name instanceof \Closure) { + call_user_func_array($name, $params); + } elseif (is_string($name)) { + $name = explode(',', $name); + } + if (is_array($name)) { + foreach ($name as $scope) { + $method = 'scope' . trim($scope); if (method_exists($model, $method)) { - $model->$method($class, $params); + call_user_func_array([$model, $method], $params); } } } @@ -670,49 +1052,57 @@ abstract class Model implements \JsonSerializable, \ArrayAccess /** * 根据关联条件查询当前模型 * @access public - * @param string $relation 关联方法名 - * @param string $operator 比较操作符 - * @param integer $count 个数 - * @param string $id 关联表的统计字段 - * @return \think\Model + * @param string $relation 关联方法名 + * @param string $operator 比较操作符 + * @param integer $count 个数 + * @param string $id 关联表的统计字段 + * @return Model */ public static function has($relation, $operator = '>=', $count = 1, $id = '*') { - $class = new static(); - $model = $class->$relation(); - $info = $class->getRelationInfo(); + $model = new static(); + $info = $model->$relation()->getRelationInfo(); $table = $info['model']::getTable(); - return self::db()->alias('a') - ->join($table . ' b', 'a.' . $info['localKey'] . '=b.' . $info['foreignKey']) - ->group('b.' . $info['foreignKey']) - ->having('count(' . $id . ')' . $operator . $count); + switch ($info['type']) { + case Relation::HAS_MANY: + return $model->db()->alias('a') + ->join($table . ' b', 'a.' . $info['localKey'] . '=b.' . $info['foreignKey']) + ->group('b.' . $info['foreignKey']) + ->having('count(' . $id . ')' . $operator . $count); + case Relation::HAS_MANY_THROUGH: + // TODO + } } /** * 根据关联条件查询当前模型 * @access public - * @param string $relation 关联方法名 - * @param mixed $where 查询条件(数组或者闭包) - * @return \think\Model + * @param string $relation 关联方法名 + * @param mixed $where 查询条件(数组或者闭包) + * @return Model */ public static function hasWhere($relation, $where = []) { - $class = new static(); - $model = $class->$relation(); - $info = $class->getRelationInfo(); - $table = $info['model']::getTable(); - if (is_array($where)) { - foreach ($where as $key => $val) { - if (false === strpos($key, '.')) { - $where['b.' . $key] = $val; - unset($where[$key]); + $model = new static(); + $info = $model->$relation()->getRelationInfo(); + switch ($info['type']) { + case Relation::HAS_MANY: + $table = $info['model']::getTable(); + if (is_array($where)) { + foreach ($where as $key => $val) { + if (false === strpos($key, '.')) { + $where['b.' . $key] = $val; + unset($where[$key]); + } + } } - } + return $model->db()->alias('a') + ->field('a.*') + ->join($table . ' b', 'a.' . $info['localKey'] . '=b.' . $info['foreignKey']) + ->where($where); + case Relation::HAS_MANY_THROUGH: + // TODO } - return self::db()->alias('a') - ->field('a.*') - ->join($table . ' b', 'a.' . $info['localKey'] . '=b.' . $info['foreignKey']) - ->where($where); } /** @@ -743,46 +1133,35 @@ abstract class Model implements \JsonSerializable, \ArrayAccess if (is_string($relations)) { $relations = explode(',', $relations); } - + $this->relation(); foreach ($relations as $relation) { $this->data[$relation] = $this->relation->getRelation($relation); } return $this; } - /** - * 获取当前关联信息 - * @access public - * @param string $name 关联信息 - * @return array|string|integer - */ - public function getRelationInfo($name = '') - { - return $this->relation->getRelationInfo($name); - } - /** * 预载入关联查询 返回数据集 * @access public - * @param array $resultSet 数据集 - * @param string $relation 关联名 + * @param array $resultSet 数据集 + * @param string $relation 关联名 * @return array */ public function eagerlyResultSet($resultSet, $relation) { - return $this->relation->eagerlyResultSet($resultSet, $relation); + return $this->relation()->eagerlyResultSet($resultSet, $relation); } /** * 预载入关联查询 返回模型对象 * @access public - * @param Model $result 数据对象 - * @param string $relation 关联名 - * @return \think\Model + * @param Model $result 数据对象 + * @param string $relation 关联名 + * @return Model */ public function eagerlyResult($result, $relation) { - return $this->relation->eagerlyResult($result, $relation); + return $this->relation()->eagerlyResult($result, $relation); } /** @@ -791,15 +1170,17 @@ abstract class Model implements \JsonSerializable, \ArrayAccess * @param string $model 模型名 * @param string $foreignKey 关联外键 * @param string $localKey 关联主键 - * @return \think\db\Query|string + * @param array $alias 别名定义 + * @param string $joinType JOIN类型 + * @return Relation */ - public function hasOne($model, $foreignKey = '', $localKey = '') + public function hasOne($model, $foreignKey = '', $localKey = '', $alias = [], $joinType = 'INNER') { // 记录当前关联信息 $model = $this->parseModel($model); $localKey = $localKey ?: $this->getPk(); $foreignKey = $foreignKey ?: Loader::parseName($this->name) . '_id'; - return $this->relation->hasOne($model, $foreignKey, $localKey); + return $this->relation()->hasOne($model, $foreignKey, $localKey, $alias, $joinType); } /** @@ -808,15 +1189,17 @@ abstract class Model implements \JsonSerializable, \ArrayAccess * @param string $model 模型名 * @param string $foreignKey 关联外键 * @param string $otherKey 关联主键 - * @return \think\db\Query|string + * @param array $alias 别名定义 + * @param string $joinType JOIN类型 + * @return Relation */ - public function belongsTo($model, $foreignKey = '', $otherKey = '') + public function belongsTo($model, $foreignKey = '', $otherKey = '', $alias = [], $joinType = 'INNER') { // 记录当前关联信息 $model = $this->parseModel($model); $foreignKey = $foreignKey ?: Loader::parseName(basename(str_replace('\\', '/', $model))) . '_id'; $otherKey = $otherKey ?: (new $model)->getPk(); - return $this->relation->belongsTo($model, $foreignKey, $otherKey); + return $this->relation()->belongsTo($model, $foreignKey, $otherKey, $alias, $joinType); } /** @@ -825,15 +1208,39 @@ abstract class Model implements \JsonSerializable, \ArrayAccess * @param string $model 模型名 * @param string $foreignKey 关联外键 * @param string $localKey 关联主键 - * @return \think\db\Query|string + * @param array $alias 别名定义 + * @return Relation */ - public function hasMany($model, $foreignKey = '', $localKey = '') + public function hasMany($model, $foreignKey = '', $localKey = '', $alias = []) { // 记录当前关联信息 $model = $this->parseModel($model); $localKey = $localKey ?: $this->getPk(); $foreignKey = $foreignKey ?: Loader::parseName($this->name) . '_id'; - return $this->relation->hasMany($model, $foreignKey, $localKey); + return $this->relation()->hasMany($model, $foreignKey, $localKey, $alias); + } + + /** + * HAS MANY 远程关联定义 + * @access public + * @param string $model 模型名 + * @param string $through 中间模型名 + * @param string $foreignKey 关联外键 + * @param string $throughKey 关联外键 + * @param string $localKey 关联主键 + * @param array $alias 别名定义 + * @return Relation + */ + public function hasManyThrough($model, $through, $foreignKey = '', $throughKey = '', $localKey = '', $alias = []) + { + // 记录当前关联信息 + $model = $this->parseModel($model); + $through = $this->parseModel($through); + $localKey = $localKey ?: $this->getPk(); + $foreignKey = $foreignKey ?: Loader::parseName($this->name) . '_id'; + $name = Loader::parseName(basename(str_replace('\\', '/', $through))); + $throughKey = $throughKey ?: $name . '_id'; + return $this->relation()->hasManyThrough($model, $through, $foreignKey, $throughKey, $localKey, $alias); } /** @@ -843,47 +1250,18 @@ abstract class Model implements \JsonSerializable, \ArrayAccess * @param string $table 中间表名 * @param string $foreignKey 关联外键 * @param string $localKey 当前模型关联键 - * @return \think\db\Query|string + * @param array $alias 别名定义 + * @return Relation */ - public function belongsToMany($model, $table = '', $foreignKey = '', $localKey = '') + public function belongsToMany($model, $table = '', $foreignKey = '', $localKey = '', $alias = []) { // 记录当前关联信息 $model = $this->parseModel($model); $name = Loader::parseName(basename(str_replace('\\', '/', $model))); - $table = $table ?: Db::name(Loader::parseName($this->name) . '_' . $name)->getTable(); + $table = $table ?: $this->db()->getTable(Loader::parseName($this->name) . '_' . $name); $foreignKey = $foreignKey ?: $name . '_id'; $localKey = $localKey ?: Loader::parseName($this->name) . '_id'; - return $this->relation->belongsToMany($model, $table, $foreignKey, $localKey); - } - - /** - * 初始化数据库对象 - * @access public - * @return \think\db\Driver - */ - public static function db() - { - $model = get_called_class(); - - if (!isset(self::$links[$model])) { - $class = new static; - self::$links[$model] = Db::connect($class->connection, $model); - self::$instance[$model] = $class; - } else { - $class = self::$instance[$model]; - } - - // 设置当前数据表和模型名 - if (!empty($class->table)) { - self::$links[$model]->table($class->table); - } else { - $name = !empty($class->name) ? $class->name : basename(str_replace('\\', '/', $model)); - self::$links[$model]->name($name); - } - // 设置当前模型 确保查询返回模型对象 - self::$links[$model]->model($model); - // 返回当前数据库对象 - return self::$links[$model]; + return $this->relation()->belongsToMany($model, $table, $foreignKey, $localKey, $alias); } public function __call($method, $args) @@ -891,83 +1269,34 @@ abstract class Model implements \JsonSerializable, \ArrayAccess if (method_exists($this, 'scope' . $method)) { // 动态调用命名范围 $method = 'scope' . $method; - $class = self::db(); - array_unshift($args, $class); + array_unshift($args, $this->db()); call_user_func_array([$this, $method], $args); return $this; } else { - throw new Exception(__CLASS__ . ':' . $method . ' method not exist'); + return call_user_func_array([$this->db(), $method], $args); } } public static function __callStatic($method, $params) { - return call_user_func_array([self::db(), $method], $params); + $model = get_called_class(); + if (!isset(self::$links[$model])) { + self::$links[$model] = (new static())->db(); + } + $query = self::$links[$model]; + return call_user_func_array([$query, $method], $params); } /** * 修改器 设置数据对象的值 * @access public - * @param string $name 名称 - * @param mixed $value 值 + * @param string $name 名称 + * @param mixed $value 值 * @return void */ public function __set($name, $value) { - if (is_null($value) && in_array($name, $this->autoTimeField)) { - // 自动写入的时间戳字段 - $value = NOW_TIME; - } else { - // 检测修改器 - $method = 'set' . Loader::parseName($name, 1) . 'Attr'; - if (method_exists($this, $method)) { - $value = $this->$method($value, $this->data); - } elseif (isset($this->type[$name])) { - // 类型转换 - $type = $this->type[$name]; - if (strpos($type, ':')) { - list($type, $param) = explode(':', $type, 2); - } - switch ($type) { - case 'integer': - $value = (int) $value; - break; - case 'float': - if (empty($param)) { - $value = (float) $value; - } else { - $value = (float) number_format($value, $param); - } - break; - case 'boolean': - $value = (bool) $value; - break; - case 'datetime': - if (!is_numeric($value)) { - $value = strtotime($value); - } - break; - case 'object': - if (is_object($value)) { - $value = json_encode($value, JSON_FORCE_OBJECT); - } - break; - case 'json': - case 'array': - if (is_array($value)) { - $value = json_encode($value, JSON_UNESCAPED_UNICODE); - } - break; - } - } - } - - // 标记字段更改 - if (!isset($this->data[$name]) || ($this->data[$name] != $value && !in_array($name, $this->change))) { - $this->change[] = $name; - } - // 设置数据对象属性 - $this->data[$name] = $value; + $this->setAttr($name, $value); } /** @@ -978,51 +1307,7 @@ abstract class Model implements \JsonSerializable, \ArrayAccess */ public function __get($name) { - $value = isset($this->data[$name]) ? $this->data[$name] : null; - - // 检测属性获取器 - $method = 'get' . Loader::parseName($name, 1) . 'Attr'; - if (method_exists($this, $method)) { - return $this->$method($value, $this->data); - } elseif (!is_null($value) && isset($this->type[$name])) { - // 类型转换 - $type = $this->type[$name]; - if (strpos($type, ':')) { - list($type, $param) = explode(':', $type, 2); - } - switch ($type) { - case 'integer': - $value = (int) $value; - break; - case 'float': - if (empty($param)) { - $value = (float) $value; - } else { - $value = (float) number_format($value, $param); - } - break; - case 'boolean': - $value = (bool) $value; - break; - case 'datetime': - $format = !empty($param) ? $param : $this->dateFormat; - $value = date($format, $value); - break; - case 'json': - case 'array': - $value = json_decode($value, true); - break; - case 'object': - $value = json_decode($value); - break; - } - } elseif (is_null($value) && method_exists($this, $name)) { - // 获取关联数据 - $value = $this->relation->getRelation($name); - // 保存关联对象值 - $this->data[$name] = $value; - } - return $value; + return $this->getAttr($name); } /** @@ -1033,7 +1318,17 @@ abstract class Model implements \JsonSerializable, \ArrayAccess */ public function __isset($name) { - return isset($this->data[$name]); + try { + if (array_key_exists($name, $this->data)) { + return true; + } else { + $this->getAttr($name); + return true; + } + } catch (InvalidArgumentException $e) { + return false; + } + } /** @@ -1061,7 +1356,7 @@ abstract class Model implements \JsonSerializable, \ArrayAccess // ArrayAccess public function offsetSet($name, $value) { - $this->__set($name, $value); + $this->setAttr($name, $value); } public function offsetExists($name) @@ -1076,7 +1371,7 @@ abstract class Model implements \JsonSerializable, \ArrayAccess public function offsetGet($name) { - return $this->__get($name); + return $this->getAttr($name); } /** diff --git a/library/think/Paginator.php b/library/think/Paginator.php index 4f1772ae..7385ebb0 100644 --- a/library/think/Paginator.php +++ b/library/think/Paginator.php @@ -12,6 +12,7 @@ namespace think; use think\paginator\Collection as PaginatorCollection; +use think\Request; abstract class Paginator { @@ -44,7 +45,7 @@ abstract class Paginator 'fragment' => '' ]; - public function __construct($items, $listRows, $currentPage = null, $simple = false, $total = null, $options = []) + protected function __construct($items, $listRows, $currentPage = null, $total = null, $simple = false, $options = []) { $this->options = array_merge($this->options, $options); @@ -53,12 +54,13 @@ abstract class Paginator $this->simple = $simple; $this->listRows = $listRows; - $this->items = PaginatorCollection::make($items, $this); - if ($simple) { + if (!$items instanceof Collection) { + $items = Collection::make($items); + } $this->currentPage = $this->setCurrentPage($currentPage); - $this->hasMore = count($this->items) > ($this->listRows); - $this->items = $this->items->slice(0, $this->listRows); + $this->hasMore = count($items) > ($this->listRows); + $items = $items->slice(0, $this->listRows); } else { $this->total = $total; $this->lastPage = (int)ceil($total / $listRows); @@ -66,14 +68,24 @@ abstract class Paginator $this->hasMore = $this->currentPage < $this->lastPage; } + $this->items = PaginatorCollection::make($items, $this); } - public function items() + /** + * @param $items + * @param $listRows + * @param null $currentPage + * @param bool $simple + * @param null $total + * @param array $options + * @return PaginatorCollection + */ + public static function make($items, $listRows, $currentPage = null, $total = null, $simple = false, $options = []) { - return $this->items; + $paginator = new static($items, $listRows, $currentPage, $total, $simple, $options); + return $paginator->items; } - protected function setCurrentPage($currentPage) { if (!$this->simple && $currentPage > $this->lastPage) { @@ -120,7 +132,7 @@ abstract class Paginator */ public static function getCurrentPage($varPage = 'page', $default = 1) { - $page = Input::request($varPage); + $page = Request::instance()->request($varPage); if (filter_var($page, FILTER_VALIDATE_INT) !== false && (int)$page >= 1) { return $page; @@ -135,14 +147,13 @@ abstract class Paginator */ public static function getCurrentPath() { - //TODO 待Request类完善后这里再完善 - return '/' . $_SERVER['PATH_INFO']; + return Request::instance()->baseUrl(); } public function total() { if ($this->simple) { - throw new \Exception('简洁模式下不能获取数据总数'); + throw new \DomainException('not support total'); } return $this->total; } @@ -160,7 +171,7 @@ abstract class Paginator public function lastPage() { if ($this->simple) { - throw new \Exception('简洁模式下不能获取最后一页'); + throw new \DomainException('not support last'); } return $this->lastPage; } diff --git a/library/think/Request.php b/library/think/Request.php index f502869a..41b079c4 100644 --- a/library/think/Request.php +++ b/library/think/Request.php @@ -12,7 +12,9 @@ namespace think; use think\Config; -use think\Input; +use think\Exception; +use think\File; +use think\Session; class Request { @@ -21,6 +23,12 @@ class Request */ protected static $instance; + protected $method; + /** + * @var string 域名 + */ + protected $domain; + /** * @var string URL地址 */ @@ -32,7 +40,12 @@ class Request protected $baseUrl; /** - * @var string 根目录 + * @var string 当前执行的文件 + */ + protected $baseFile; + + /** + * @var string 访问的ROOT地址 */ protected $root; @@ -47,26 +60,40 @@ class Request protected $path; /** - * @var array 路由 + * @var array 当前路由信息 */ - protected $route = []; + protected $routeInfo = []; /** - * @var array 调度信息 + * @var array 当前调度信息 */ protected $dispatch = []; + protected $module; + protected $controller; + protected $action; + // 当前语言集 + protected $langset; + + /** + * @var array 请求参数 + */ + protected $param = []; + protected $get = []; + protected $post = []; + protected $request = []; + protected $route = []; + protected $put; + protected $delete; + protected $session = []; + protected $file = []; + protected $cookie = []; + protected $server = []; + protected $header = []; - protected $get = []; - protected $post = []; - protected $put = []; - protected $delete = []; - protected $file = []; - protected $cookie = []; - protected $server = []; /** * @var array 资源类型 */ - protected $mime = [ + protected $mimeType = [ 'html' => 'text/html,application/xhtml+xml,*/*', 'xml' => 'application/xml,text/xml,application/x-xml', 'json' => 'application/json,text/x-json,application/jsonrequest,text/json', @@ -83,6 +110,13 @@ class Request 'csv' => 'text/csv', ]; + protected $content; + + // 全局过滤规则 + protected $filter; + // Hook扩展方法 + protected static $hook = []; + /** * 架构函数 * @access public @@ -95,13 +129,40 @@ class Request $this->$name = $item; } } + $this->filter = Config::get('default_filter'); + } + + public function __call($method, $args) + { + if (array_key_exists($method, self::$hook)) { + array_unshift($args, $this); + return call_user_func_array(self::$hook[$method], $args); + } else { + throw new Exception('method not exists:' . __CLASS__ . '->' . $method); + } + } + + /** + * Hook 方法注入 + * @access public + * @param string|array $method 方法名 + * @param mixed $callback callable + * @return void + */ + public static function hook($method, $callback = null) + { + if (is_array($method)) { + self::$hook = array_merge(self::$hook, $method); + } else { + self::$hook[$method] = $callback; + } } /** * 初始化 * @access public * @param array $options 参数 - * @return object + * @return \think\Request */ public static function instance($options = []) { @@ -114,15 +175,16 @@ class Request /** * 创建一个URL请求 * @access public - * @param string $uri URL地址 - * @param string $method 请求类型 - * @param array $params 请求参数 - * @param array $cookie - * @param array $files - * @param array $server - * @return object + * @param string $uri URL地址 + * @param string $method 请求类型 + * @param array $params 请求参数 + * @param array $cookie + * @param array $files + * @param array $server + * @param string $content + * @return \think\Request */ - public static function create($uri, $method = 'GET', $params = [], $cookie = [], $files = [], $server = []) + public static function create($uri, $method = 'GET', $params = [], $cookie = [], $files = [], $server = [], $content = null) { $server['PATH_INFO'] = ''; $server['REQUEST_METHOD'] = strtoupper($method); @@ -153,76 +215,149 @@ class Request if (!isset($info['path'])) { $info['path'] = '/'; } - $options = []; - $options[strtolower($method)] = $params; - $queryString = ''; + $options = []; + $queryString = ''; if (isset($info['query'])) { parse_str(html_entity_decode($info['query']), $query); - if (isset($options['get'])) { - $options['get'] = array_replace($query, $options['get']); - $queryString = http_build_query($query, '', '&'); + if (!empty($params)) { + $params = array_replace($query, $params); + $queryString = http_build_query($query, '', '&'); } else { - $options['get'] = $query; - $queryString = $info['query']; + $params = $query; + $queryString = $info['query']; } - } elseif (isset($options['get'])) { - $queryString = http_build_query($options['get'], '', '&'); + } elseif (!empty($params)) { + $queryString = http_build_query($params, '', '&'); } $server['REQUEST_URI'] = $info['path'] . ('' !== $queryString ? '?' . $queryString : ''); $server['QUERY_STRING'] = $queryString; $options['cookie'] = $cookie; + $options['param'] = $params; $options['file'] = $files; $options['server'] = $server; - return new self($options); + $options['url'] = $server['REQUEST_URI']; + $options['baseUrl'] = $info['path']; + $options['pathinfo'] = ltrim($info['path'], '/'); + $options['method'] = $server['REQUEST_METHOD']; + $options['domain'] = $server['HTTP_HOST']; + $options['content'] = $content; + self::$instance = new self($options); + return self::$instance; } /** - * 获取当前URL + * 获取当前包含协议的域名 * @access public - * @param string $url URL地址 + * @param string $domain 域名 * @return string */ - public function url($url = '') + public function domain($domain = null) { - if (!empty($url)) { + if (!is_null($domain)) { + $this->domain = $domain; + return $this; + } elseif (!$this->domain) { + $this->domain = $this->scheme() . '://' . $this->host(); + } + return $this->domain; + } + + /** + * 获取当前完整URL 包括QUERY_STRING + * @access public + * @param string|true $url URL地址 true 带域名获取 + * @return string + */ + public function url($url = null) + { + if (!is_null($url) && true !== $url) { $this->url = $url; - } else { - return $this->url ?: $_SERVER[Config::get('url_request_uri')]; + return $this; + } elseif (!$this->url) { + if (IS_CLI) { + $this->url = isset($_SERVER['argv'][1]) ? $_SERVER['argv'][1] : ''; + } elseif (isset($_SERVER['HTTP_X_REWRITE_URL'])) { + $this->url = $_SERVER['HTTP_X_REWRITE_URL']; + } elseif (isset($_SERVER['REQUEST_URI'])) { + $this->url = $_SERVER['REQUEST_URI']; + } elseif (isset($_SERVER['ORIG_PATH_INFO'])) { + $this->url = $_SERVER['ORIG_PATH_INFO'] . (!empty($_SERVER['QUERY_STRING']) ? '?' . $_SERVER['QUERY_STRING'] : ''); + } else { + $this->url = ''; + } } + return true === $url ? $this->domain() . $this->url : $this->url; } /** - * 获取基础URL + * 获取当前URL 不含QUERY_STRING * @access public * @param string $url URL地址 * @return string */ - public function baseUrl($url = '') + public function baseUrl($url = null) { - if (!empty($url)) { + if (!is_null($url) && true !== $url) { $this->baseUrl = $url; - } else { - return $this->baseUrl ?: rtrim($_SERVER['SCRIPT_NAME'], '/'); + return $this; + } elseif (!$this->baseUrl) { + $str = $this->url(); + $this->baseUrl = strpos($str, '?') ? strstr($str, '?', true) : $str; } + return true === $url ? $this->domain() . $this->baseUrl : $this->baseUrl; } /** - * 获取URL访问根目录 + * 获取当前执行的文件 SCRIPT_NAME + * @access public + * @param string $file 当前执行的文件 + * @return string + */ + public function baseFile($file = null) + { + if (!is_null($file) && true !== $file) { + $this->baseFile = $file; + return $this; + } elseif (!$this->baseFile) { + $url = ''; + if (!IS_CLI) { + $script_name = basename($_SERVER['SCRIPT_FILENAME']); + if (basename($_SERVER['SCRIPT_NAME']) === $script_name) { + $url = $_SERVER['SCRIPT_NAME']; + } elseif (basename($_SERVER['PHP_SELF']) === $script_name) { + $url = $_SERVER['PHP_SELF']; + } elseif (isset($_SERVER['ORIG_SCRIPT_NAME']) && basename($_SERVER['ORIG_SCRIPT_NAME']) === $script_name) { + $url = $_SERVER['ORIG_SCRIPT_NAME']; + } elseif (($pos = strpos($_SERVER['PHP_SELF'], '/' . $script_name)) !== false) { + $url = substr($_SERVER['SCRIPT_NAME'], 0, $pos) . '/' . $script_name; + } elseif (isset($_SERVER['DOCUMENT_ROOT']) && strpos($_SERVER['SCRIPT_FILENAME'], $_SERVER['DOCUMENT_ROOT']) === 0) { + $url = str_replace('\\', '/', str_replace($_SERVER['DOCUMENT_ROOT'], '', $_SERVER['SCRIPT_FILENAME'])); + } + } + $this->baseFile = $url; + } + return true === $file ? $this->domain() . $this->baseFile : $this->baseFile; + } + + /** + * 获取URL访问根地址 * @access public * @param string $url URL地址 * @return string */ - public function root($url = '') + public function root($url = null) { - if (!empty($url)) { + if (!is_null($url) && true !== $url) { $this->root = $url; - - } elseif ($this->root) { - return $this->root; - } else { - $_root = rtrim(dirname($this->baseUrl()), '/'); - return ('/' == $_root || '\\' == $_root) ? '' : $_root; + return $this; + } elseif (!$this->root) { + $file = $this->baseFile(); + if ($file && 0 !== strpos($this->url(), $file)) { + $file = str_replace('\\', '/', dirname($file)); + } + $this->root = rtrim($file, '/'); } + return true === $url ? $this->domain() . $this->root : $this->root; } /** @@ -252,7 +387,7 @@ class Request } } } - $this->pathinfo = empty($_SERVER['PATH_INFO']) ? '/' : trim($_SERVER['PATH_INFO'], '/'); + $this->pathinfo = empty($_SERVER['PATH_INFO']) ? '/' : ltrim($_SERVER['PATH_INFO'], '/'); } return $this->pathinfo; } @@ -265,8 +400,18 @@ class Request public function path() { if (is_null($this->path)) { - // 去除正常的URL后缀 - $this->path = preg_replace(Config::get('url_html_suffix') ? '/\.(' . trim(Config::get('url_html_suffix'), '.') . ')$/i' : '/\.' . $this->ext() . '$/i', '', $this->pathinfo()); + $suffix = Config::get('url_html_suffix'); + $pathinfo = $this->pathinfo(); + if (false === $suffix) { + // 禁止伪静态访问 + $this->path = $pathinfo; + } elseif ($suffix) { + // 去除正常的URL后缀 + $this->path = preg_replace('/\.(' . ltrim($suffix, '.') . ')$/i', '', $pathinfo); + } else { + // 允许任何后缀访问 + $this->path = preg_replace('/\.' . $this->ext() . '$/i', '', $pathinfo); + } } return $this->path; } @@ -299,14 +444,15 @@ class Request */ public function type() { - if (!isset($_SERVER['HTTP_ACCEPT'])) { + $accept = isset($this->server['HTTP_ACCEPT']) ? $this->server['HTTP_ACCEPT'] : $_SERVER['HTTP_ACCEPT']; + if (empty($accept)) { return false; } foreach ($this->mimeType as $key => $val) { $array = explode(',', $val); foreach ($array as $k => $v) { - if (stristr($_SERVER['HTTP_ACCEPT'], $v)) { + if (stristr($accept, $v)) { return $key; } } @@ -317,14 +463,13 @@ class Request /** * 设置资源类型 * @access public - * @param string|array $type 资源类型名 - * @param string $val 资源类型 + * @param string|array $type 资源类型名 + * @param string $val 资源类型 * @return void */ public function mimeType($type, $val = '') { if (is_array($type)) { - $this->mimeType = array_merge($this->mimeType, $type); } else { $this->mimeType[$type] = $val; @@ -334,123 +479,652 @@ class Request /** * 当前的请求类型 * @access public + * @param bool $method true 获取原始请求类型 * @return string */ - public function method() + public function method($method = false) { - return IS_CLI ? 'GET' : $_SERVER['REQUEST_METHOD']; - } - - /** - * 当前请求的参数 - * @access public - * @param string $name 变量名 - * @return mixed - */ - public function param($name = '') - { - $method = $this->method(); - return $this->$method($name); - } - - /** - * 当前请求的get参数 - * @access public - * @param string $name 变量名 - * @return mixed - */ - public function get($name = '') - { - return Input::data($this->get ?: $_GET, $name); - } - - /** - * 当前请求的post参数 - * @access public - * @param string $name 变量名 - * @return mixed - */ - public function post($name = '') - { - return Input::data($this->post ?: $_POST, $name); - } - - /** - * 当前请求的put参数 - * @access public - * @param string $name 变量名 - * @return mixed - */ - public function put($name = '') - { - static $_PUT = null; - if (is_null($_PUT)) { - parse_str(file_get_contents('php://input'), $_PUT); + if (true === $method) { + // 获取原始请求类型 + return IS_CLI ? 'GET' : (isset($this->server['REQUEST_METHOD']) ? $this->server['REQUEST_METHOD'] : $_SERVER['REQUEST_METHOD']); + } elseif (!$this->method) { + if (isset($_POST[Config::get('var_method')])) { + $this->method = strtoupper($_POST[Config::get('var_method')]); + } elseif (isset($_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'])) { + $this->method = strtoupper($_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE']); + } else { + $this->method = IS_CLI ? 'GET' : (isset($this->server['REQUEST_METHOD']) ? $this->server['REQUEST_METHOD'] : $_SERVER['REQUEST_METHOD']); + } } - return Input::data($this->put ?: $_PUT, $name); + return $this->method; } /** - * 当前请求的delete参数 + * 是否为GET请求 * @access public - * @param string $name 变量名 + * @return bool + */ + public function isGet() + { + return $this->method() == 'GET'; + } + + /** + * 是否为POST请求 + * @access public + * @return bool + */ + public function isPost() + { + return $this->method() == 'POST'; + } + + /** + * 是否为PUT请求 + * @access public + * @return bool + */ + public function isPut() + { + return $this->method() == 'PUT'; + } + + /** + * 是否为DELTE请求 + * @access public + * @return bool + */ + public function isDelete() + { + return $this->method() == 'DELETE'; + } + + /** + * 是否为HEAD请求 + * @access public + * @return bool + */ + public function isHead() + { + return $this->method() == 'HEAD'; + } + + /** + * 是否为PATCH请求 + * @access public + * @return bool + */ + public function isPatch() + { + return $this->method() == 'PATCH'; + } + + /** + * 是否为OPTIONS请求 + * @access public + * @return bool + */ + public function isOptions() + { + return $this->method() == 'OPTIONS'; + } + + /** + * 是否为cli + * @access public + * @return bool + */ + public function isCli() + { + return PHP_SAPI == 'cli'; + } + + /** + * 是否为cgi + * @access public + * @return bool + */ + public function isCgi() + { + return strpos(PHP_SAPI, 'cgi') === 0; + } + + /** + * 设置获取获取当前请求的参数 + * @access public + * @param string|array $name 变量名 + * @param mixed $default 默认值 + * @param string|array $filter 过滤方法 * @return mixed */ - public function delete($name = '') + public function param($name = '', $default = null, $filter = null) { - static $_DELETE = null; - if (is_null($_DELETE)) { - parse_str(file_get_contents('php://input'), $_DELETE); - $_DELETE = array_merge($_DELETE, $_GET); + if (is_array($name)) { + // 设置param + $this->param = array_merge($this->param, $name); + return; } - return Input::data($this->delete ?: $_DELETE, $name); + if (empty($this->param)) { + $method = $this->method(true); + // 自动获取请求变量 + switch ($method) { + case 'POST': + $vars = $this->post(false); + break; + case 'PUT': + $vars = $this->put(false); + break; + case 'DELETE': + $vars = $this->delete(false); + break; + default: + $vars = []; + } + // 当前请求参数和URL地址中的参数合并 + $this->param = array_merge($this->route(false), $this->get(false), $vars); + } + return $this->input($this->param, $name, $default, $filter); + } + + /** + * 设置获取获取路由参数 + * @access public + * @param string|array $name 变量名 + * @param mixed $default 默认值 + * @param string|array $filter 过滤方法 + * @return mixed + */ + public function route($name = '', $default = null, $filter = null) + { + if (is_array($name)) { + return $this->route = array_merge($this->route, $name); + } + return $this->input($this->route, $name, $default, $filter); + } + + /** + * 设置获取获取GET参数 + * @access public + * @param string|array $name 变量名 + * @param mixed $default 默认值 + * @param string|array $filter 过滤方法 + * @return mixed + */ + public function get($name = '', $default = null, $filter = null) + { + if (is_array($name)) { + return $this->get = array_merge($this->get, $name); + } elseif (empty($this->get)) { + $this->get = $_GET; + } + return $this->input($this->get, $name, $default, $filter); + } + + /** + * 设置获取获取POST参数 + * @access public + * @param string $name 变量名 + * @param mixed $default 默认值 + * @param string|array $filter 过滤方法 + * @return mixed + */ + public function post($name = '', $default = null, $filter = null) + { + if (is_array($name)) { + return $this->post = array_merge($this->post, $name); + } elseif (empty($this->post)) { + $this->post = $_POST; + } + return $this->input($this->post, $name, $default, $filter); + } + + /** + * 设置获取获取PUT参数 + * @access public + * @param string|array $name 变量名 + * @param mixed $default 默认值 + * @param string|array $filter 过滤方法 + * @return mixed + */ + public function put($name = '', $default = null, $filter = null) + { + if (is_array($name)) { + return $this->put = is_null($this->put) ? $name : array_merge($this->put, $name); + } + if (is_null($this->put)) { + $content = file_get_contents('php://input'); + if (strpos($content, '":')) { + $this->put = json_decode($content, true); + } else { + parse_str($content, $this->put); + } + } + return $this->input($this->put, $name, $default, $filter); + } + + /** + * 设置获取获取DELETE参数 + * @access public + * @param string|array $name 变量名 + * @param mixed $default 默认值 + * @param string|array $filter 过滤方法 + * @return mixed + */ + public function delete($name = '', $default = null, $filter = null) + { + if (is_array($name)) { + return $this->delete = is_null($this->delete) ? $name : array_merge($this->delete, $name); + } + if (is_null($this->delete)) { + parse_str(file_get_contents('php://input'), $this->delete); + } + return $this->input($this->delete, $name, $default, $filter); + } + + /** + * 获取request变量 + * @param string $name 数据名称 + * @param string $default 默认值 + * @param string|array $filter 过滤方法 + * @return mixed + */ + public function request($name = '', $default = null, $filter = null) + { + if (is_array($name)) { + return $this->request = array_merge($this->request, $name); + } elseif (empty($this->request)) { + $this->request = $_REQUEST; + } + return $this->input($this->request ?: $_REQUEST, $name, $default, $filter); } /** * 获取session数据 * @access public - * @param string $name 变量名 + * @param string|array $name 数据名称 + * @param string $default 默认值 + * @param string|array $filter 过滤方法 * @return mixed */ - public function session($name = '') + public function session($name = '', $default = null, $filter = null) { - if (PHP_SESSION_DISABLED == session_status()) { - session_start(); + if (is_array($name)) { + return $this->session = array_merge($this->session, $name); + } elseif (empty($this->session)) { + $this->session = Session::get(); } - return Input::data($this->session ?: $_SESSION, $name); + return $this->input($this->session, $name, $default, $filter); } /** * 获取cookie参数 * @access public - * @param string $name 变量名 + * @param string|array $name 数据名称 + * @param string $default 默认值 + * @param string|array $filter 过滤方法 * @return mixed */ - public function cookie($name = '') + public function cookie($name = '', $default = null, $filter = null) { - return Input::data($this->cookie ?: $_COOKIE, $name); + if (is_array($name)) { + return $this->cookie = array_merge($this->cookie, $name); + } elseif (empty($this->cookie)) { + $this->cookie = $_COOKIE; + } + return $this->input($this->cookie, $name, $default, $filter); } /** * 获取server参数 * @access public - * @param string $name 变量名 + * @param string|array $name 数据名称 + * @param string $default 默认值 + * @param string|array $filter 过滤方法 * @return mixed */ - public function server($name = '') + public function server($name = '', $default = null, $filter = null) { - return Input::data($this->server ?: $_SERVER, $name); + if (is_array($name)) { + return $this->server = array_merge($this->server, $name); + } elseif (empty($this->server)) { + $this->server = $_SERVER; + } + return $this->input($this->server, false === $name ? false : strtoupper($name), $default, $filter); } /** * 获取上传的文件信息 * @access public - * @param string $name 名称 + * @param string|array $name 名称 * @return null|array|\think\File */ public function file($name = '') { - return Input::file($name, $this->file ?: $_FILES); + if (is_array($name)) { + return $this->file = array_merge($this->file, $name); + } elseif (empty($this->file)) { + $this->file = isset($_FILES) ? $_FILES : []; + } + $files = $this->file; + if (!empty($files)) { + // 处理上传文件 + $array = []; + foreach ($files as $key => $file) { + if (is_array($file['name'])) { + $item = []; + $keys = array_keys($file); + $count = count($file['name']); + for ($i = 0; $i < $count; $i++) { + if (empty($file['tmp_name'][$i])) { + continue; + } + $temp['key'] = $key; + foreach ($keys as $_key) { + $temp[$_key] = $file[$_key][$i]; + } + $item[] = (new File($temp['tmp_name']))->setUploadInfo($temp); + } + $array[$key] = $item; + } else { + if ($file instanceof File) { + $array[$key] = $file; + } else { + if (empty($file['tmp_name'])) { + continue; + } + $array[$key] = (new File($file['tmp_name']))->setUploadInfo($file); + } + } + } + if (strpos($name, '.')) { + list($name, $sub) = explode('.', $name); + } + if ('' === $name) { + // 获取全部文件 + return $array; + } elseif (isset($sub) && isset($array[$name][$sub])) { + return $array[$name][$sub]; + } elseif (isset($array[$name])) { + return $array[$name]; + } + } + return null; + } + + /** + * 获取环境变量 + * @param string|array $name 数据名称 + * @param string $default 默认值 + * @param string|array $filter 过滤方法 + * @return mixed + */ + public function env($name = '', $default = null, $filter = null) + { + if (is_array($name)) { + return $this->env = array_merge($this->env, $name); + } elseif (empty($this->env)) { + $this->env = $_ENV; + } + return $this->input($this->env, false === $name ? false : strtoupper($name), $default, $filter); + } + + /** + * 设置或者获取当前的Header + * @access public + * @param string|array $name header名称 + * @param string $default 默认值 + * @return string + */ + public function header($name = '', $default = null) + { + if (is_array($name)) { + return $this->header = array_merge($this->header, $name); + } elseif (empty($this->header)) { + $header = []; + $server = $this->server ?: $_SERVER; + foreach ($server as $key => $val) { + if (0 === strpos($key, 'HTTP_')) { + $key = str_replace('_', '-', strtolower(substr($key, 5))); + $header[$key] = $val; + } + } + if (isset($server['CONTENT_TYPE'])) { + $header['content-type'] = $server['CONTENT_TYPE']; + } + if (isset($server['CONTENT_LENGTH'])) { + $header['content-length'] = $server['CONTENT_LENGTH']; + } + $this->header = array_change_key_case($header); + } + if ('' === $name) { + return $this->header; + } + $name = str_replace('_', '-', strtolower($name)); + return isset($this->header[$name]) ? $this->header[$name] : $default; + } + + /** + * 获取变量 支持过滤和默认值 + * @param array $data 数据源 + * @param string|false $name 字段名 + * @param mixed $default 默认值 + * @param string|array $filter 过滤函数 + * @return mixed + */ + public function input($data = [], $name = '', $default = null, $filter = null) + { + if (false === $name) { + // 获取原始数据 + return $data; + } + $name = (string) $name; + if ('' != $name) { + // 解析name + if (strpos($name, '/')) { + list($name, $type) = explode('/', $name); + } else { + $type = 's'; + } + // 按.拆分成多维数组进行判断 + foreach (explode('.', $name) as $val) { + if (isset($data[$val])) { + $data = $data[$val]; + } else { + // 无输入数据,返回默认值 + return $default; + } + } + } + + // 解析过滤器 + $filter = $filter ?: $this->filter; + + if (is_string($filter)) { + $filter = explode(',', $filter); + } else { + $filter = (array) $filter; + } + $filter[] = $default; + if (is_array($data)) { + array_walk_recursive($data, [$this, 'filterValue'], $filter); + } else { + $this->filterValue($data, $name, $filter); + } + + if (isset($type) && $data !== $default) { + // 强制类型转换 + $this->typeCast($data, $type); + } + return $data; + } + + /** + * 设置或获取当前的过滤规则 + * @param mixed $filter 过滤规则 + * @return mixed + */ + public function filter($filter = null) + { + if (is_null($filter)) { + return $this->filter; + } else { + $this->filter = $filter; + } + } + + /** + * 递归过滤给定的值 + * @param mixed $value 键值 + * @param mixed $key 键名 + * @param array $filters 过滤方法+默认值 + * @return mixed + */ + private function filterValue(&$value, $key, $filters) + { + $default = array_pop($filters); + foreach ($filters as $filter) { + if (is_callable($filter)) { + // 调用函数或者方法过滤 + $value = call_user_func($filter, $value); + } else { + if (strpos($filter, '/')) { + // 正则过滤 + if (!preg_match($filter, $value)) { + // 匹配不成功返回默认值 + $value = $default; + break; + } + } elseif (!empty($filter)) { + // filter函数不存在时, 则使用filter_var进行过滤 + // filter为非整形值时, 调用filter_id取得过滤id + $value = filter_var($value, is_int($filter) ? $filter : filter_id($filter)); + if (false === $value) { + $value = $default; + break; + } + } + } + } + return $this->filterExp($value); + } + + /** + * 过滤表单中的表达式 + * @param string $value + * @return void + */ + public function filterExp(&$value) + { + // 过滤查询特殊字符 + if (is_string($value) && preg_match('/^(EXP|NEQ|GT|EGT|LT|ELT|OR|XOR|LIKE|NOTLIKE|NOT BETWEEN|NOTBETWEEN|BETWEEN|NOTIN|NOT IN|IN)$/i', $value)) { + $value .= ' '; + } + // TODO 其他安全过滤 + } + + /** + * 强制类型转换 + * @param string $data + * @param string $type + * @return mixed + */ + private function typeCast(&$data, $type) + { + switch (strtolower($type)) { + // 数组 + case 'a': + $data = (array) $data; + break; + // 数字 + case 'd': + $data = (int) $data; + break; + // 浮点 + case 'f': + $data = (float) $data; + break; + // 布尔 + case 'b': + $data = (boolean) $data; + break; + // 字符串 + case 's': + default: + if (is_scalar($data)) { + $data = (string) $data; + } else { + throw new \InvalidArgumentException('variable type error:' . gettype($data)); + } + } + } + + /** + * 是否存在某个请求参数 + * @access public + * @param string $name 变量名 + * @param string $type 变量类型 + * @param bool $checkEmpty 是否检测空值 + * @return mixed + */ + public function has($name, $type = 'param', $checkEmpty = false) + { + if (empty($this->$type)) { + $param = $this->$type(); + } else { + $param = $this->$type; + } + // 按.拆分成多维数组进行判断 + foreach (explode('.', $name) as $val) { + if (isset($param[$val])) { + $param = $param[$val]; + } else { + return false; + } + } + return ($checkEmpty && '' === $param) ? false : true; + } + + /** + * 获取指定的参数 + * @access public + * @param string|array $name 变量名 + * @param string $type 变量类型 + * @return mixed + */ + public function only($name, $type = 'param') + { + $param = $this->$type(); + if (is_string($name)) { + $name = explode(',', $name); + } + $item = []; + foreach ($name as $key) { + if (isset($param[$key])) { + $item[$key] = $param[$key]; + } + } + return $item; + } + + /** + * 排除指定参数获取 + * @access public + * @param string|array $name 变量名 + * @param string $type 变量类型 + * @return mixed + */ + public function except($name, $type = 'param') + { + $param = $this->$type(); + if (is_string($name)) { + $name = explode(',', $name); + } + foreach ($name as $key) { + if (isset($param[$key])) { + unset($param[$key]); + } + } + return $param; } /** @@ -460,16 +1134,21 @@ class Request */ public function isSsl() { - if (isset($_SERVER['HTTPS']) && ('1' == $_SERVER['HTTPS'] || 'on' == strtolower($_SERVER['HTTPS']))) { + $server = array_merge($_SERVER, $this->server); + if (isset($server['HTTPS']) && ('1' == $server['HTTPS'] || 'on' == strtolower($server['HTTPS']))) { return true; - } elseif (isset($_SERVER['SERVER_PORT']) && ('443' == $_SERVER['SERVER_PORT'])) { + } elseif (isset($server['REQUEST_SCHEME']) && 'https' == $server['REQUEST_SCHEME']) { + return true; + } elseif (isset($server['SERVER_PORT']) && ('443' == $server['SERVER_PORT'])) { + return true; + } elseif (isset($server['HTTP_X_FORWARDED_PROTO']) && 'https' == $server['HTTP_X_FORWARDED_PROTO']) { return true; } return false; } /** - * 当前是否ajax请求 + * 当前是否Ajax请求 * @access public * @return bool */ @@ -478,10 +1157,20 @@ class Request return (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) == 'xmlhttprequest') ? true : false; } + /** + * 当前是否Pjax请求 + * @access public + * @return bool + */ + public function isPjax() + { + return (isset($_SERVER['HTTP_X_PJAX']) && $_SERVER['HTTP_X_PJAX']) ? true : false; + } + /** * 获取客户端IP地址 - * @param integer $type 返回类型 0 返回IP地址 1 返回IPV4地址数字 - * @param boolean $adv 是否进行高级模式获取(有可能被伪装) + * @param integer $type 返回类型 0 返回IP地址 1 返回IPV4地址数字 + * @param boolean $adv 是否进行高级模式获取(有可能被伪装) * @return mixed */ public function ip($type = 0, $adv = false) @@ -515,6 +1204,26 @@ class Request return $ip[$type]; } + /** + * 检测是否使用手机访问 + * @access public + * @return bool + */ + public function isMobile() + { + if (isset($_SERVER['HTTP_VIA']) && stristr($_SERVER['HTTP_VIA'], "wap")) { + return true; + } elseif (strpos(strtoupper($_SERVER['HTTP_ACCEPT']), "VND.WAP.WML")) { + return true; + } elseif (isset($_SERVER['HTTP_X_WAP_PROFILE']) || isset($_SERVER['HTTP_PROFILE'])) { + return true; + } elseif (isset($_SERVER['HTTP_USER_AGENT']) && preg_match('/(blackberry|configuration\/cldc|hp |hp-|htc |htc_|htc-|iemobile|kindle|midp|mmp|motorola|mobile|nokia|opera mini|opera |Googlebot-Mobile|YahooSeeker\/M1A1-R2D2|android|iphone|ipod|mobi|palm|palmos|pocket|portalmmm|ppc;|smartphone|sonyericsson|sqh|spv|symbian|treo|up.browser|up.link|vodafone|windows ce|xda |xda_)/i', $_SERVER['HTTP_USER_AGENT'])) { + return true; + } else { + return false; + } + } + /** * 当前URL地址中的scheme参数 * @access public @@ -522,7 +1231,7 @@ class Request */ public function scheme() { - return $_SERVER['REQUEST_SCHEME']; + return $this->isSsl() ? 'https' : 'http'; } /** @@ -532,7 +1241,7 @@ class Request */ public function query() { - return $_SERVER['QUERY_STRING']; + return $this->server('QUERY_STRING'); } /** @@ -542,7 +1251,7 @@ class Request */ public function host() { - return $_SERVER['SERVER_NAME']; + return $this->server('HTTP_HOST'); } /** @@ -552,36 +1261,132 @@ class Request */ public function port() { - return $_SERVER['SERVER_PORT']; + return $this->server('SERVER_PORT'); } /** - * 获取当前请求的路由 + * 当前请求 SERVER_PROTOCOL + * @access public + * @return integer + */ + public function protocol() + { + return $this->server('SERVER_PROTOCOL'); + } + + /** + * 当前请求 REMOTE_PORT + * @access public + * @return integer + */ + public function remotePort() + { + return $this->server('REMOTE_PORT'); + } + + /** + * 获取当前请求的路由信息 * @access public * @param array $route 路由名称 * @return array */ - public function route($route = []) + public function routeInfo($route = []) { if (!empty($route)) { - $this->route = $route; + $this->routeInfo = $route; } else { - return $this->route; + return $this->routeInfo; } } /** - * 获取当前请求的调度信息 + * 设置或者获取当前请求的调度信息 * @access public - * @param array $dispatch 调度信息 + * @param array $dispatch 调度信息 * @return array */ - public function dispatch($dispatch = []) + public function dispatch($dispatch = null) { - if (!empty($dispatch)) { + if (!is_null($dispatch)) { $this->dispatch = $dispatch; } return $this->dispatch; } + /** + * 设置或者获取当前的模块名 + * @access public + * @param string $module 模块名 + * @return string|$this + */ + public function module($module = null) + { + if (!is_null($module)) { + $this->module = $module; + return $this; + } else { + return $this->module ?: ''; + } + } + + /** + * 设置或者获取当前的控制器名 + * @access public + * @param string $controller 控制器名 + * @return string|$this + */ + public function controller($controller = null) + { + if (!is_null($controller)) { + $this->controller = $controller; + return $this; + } else { + return $this->controller ?: ''; + } + } + + /** + * 设置或者获取当前的操作名 + * @access public + * @param string $action 操作名 + * @return string + */ + public function action($action = null) + { + if (!is_null($action)) { + $this->action = $action; + return $this; + } else { + return $this->action ?: ''; + } + } + + /** + * 设置或者获取当前的语言 + * @access public + * @param string $lang 语言名 + * @return string + */ + public function langset($lang = null) + { + if (!is_null($lang)) { + $this->langset = $lang; + return $this; + } else { + return $this->langset ?: ''; + } + } + + /** + * 设置或者获取当前请求的content + * @access public + * @return string + */ + public function getContent() + { + if (is_null($this->content)) { + $this->content = file_get_contents('php://input'); + } + return $this->content; + } } diff --git a/library/think/Response.php b/library/think/Response.php index 45eaf327..59d6a0cf 100644 --- a/library/think/Response.php +++ b/library/think/Response.php @@ -11,135 +11,126 @@ namespace think; -use think\Config; -use think\Url; +use think\response\Json as JsonResponse; +use think\response\Jsonp as JsonpResponse; +use think\response\Redirect as RedirectResponse; +use think\response\View as ViewResponse; +use think\response\Xml as XmlResponse; class Response { - // 输出数据的转换方法 - protected static $transform = null; - // 输出数据 - protected static $data = ''; - // 是否exit - protected static $isExit = false; - // 输出类型 - protected static $type = ''; - // contentType - protected static $contentType = [ - 'json' => 'application/json', - 'xml' => 'text/xml', - 'html' => 'text/html', - 'jsonp' => 'application/javascript', - 'script' => 'application/javascript', - 'text' => 'text/plain', - ]; - // HTTP status - protected static $code = [ - // Informational 1xx - 100 => 'Continue', - 101 => 'Switching Protocols', - // Success 2xx - 200 => 'OK', - 201 => 'Created', - 202 => 'Accepted', - 203 => 'Non-Authoritative Information', - 204 => 'No Content', - 205 => 'Reset Content', - 206 => 'Partial Content', - // Redirection 3xx - 300 => 'Multiple Choices', - 301 => 'Moved Permanently', - 302 => 'Moved Temporarily ', // 1.1 - 303 => 'See Other', - 304 => 'Not Modified', - 305 => 'Use Proxy', - // 306 is deprecated but reserved - 307 => 'Temporary Redirect', - // Client Error 4xx - 400 => 'Bad Request', - 401 => 'Unauthorized', - 402 => 'Payment Required', - 403 => 'Forbidden', - 404 => 'Not Found', - 405 => 'Method Not Allowed', - 406 => 'Not Acceptable', - 407 => 'Proxy Authentication Required', - 408 => 'Request Timeout', - 409 => 'Conflict', - 410 => 'Gone', - 411 => 'Length Required', - 412 => 'Precondition Failed', - 413 => 'Request Entity Too Large', - 414 => 'Request-URI Too Long', - 415 => 'Unsupported Media Type', - 416 => 'Requested Range Not Satisfiable', - 417 => 'Expectation Failed', - // Server Error 5xx - 500 => 'Internal Server Error', - 501 => 'Not Implemented', - 502 => 'Bad Gateway', - 503 => 'Service Unavailable', - 504 => 'Gateway Timeout', - 505 => 'HTTP Version Not Supported', - 509 => 'Bandwidth Limit Exceeded', - ]; + // 原始数据 + protected $data; + + // 当前的contentType + protected $contentType = 'text/html'; + + // 字符集 + protected $charset = 'utf-8'; + + //状态 + protected $code = 200; + + // 输出参数 + protected $options = []; + // header参数 + protected $header = []; + + protected $content = null; + + /** + * 架构函数 + * @access public + * @param mixed $data 输出数据 + * @param int $code + * @param array $header + * @param array $options 输出参数 + */ + public function __construct($data = '', $code = 200, array $header = [], $options = []) + { + $this->data($data); + $this->header = $header; + $this->code = $code; + if (!empty($options)) { + $this->options = array_merge($this->options, $options); + } + $this->contentType($this->contentType, $this->charset); + } + + /** + * 创建Response对象 + * @access public + * @param mixed $data 输出数据 + * @param string $type 输出类型 + * @param int $code + * @param array $header + * @param array $options 输出参数 + * @return Response|JsonResponse|ViewResponse|XmlResponse|RedirectResponse|JsonpResponse + */ + public static function create($data = '', $type = '', $code = 200, array $header = [], $options = []) + { + $type = empty($type) ? 'null' : strtolower($type); + + $class = false !== strpos($type, '\\') ? $type : '\\think\\response\\' . ucfirst($type); + if (class_exists($class)) { + $response = new $class($data, $code, $header, $options); + } else { + $response = new static($data, $code, $header, $options); + } + + return $response; + } /** * 发送数据到客户端 * @access public - * @param mixed $data 数据 - * @param string $type 返回类型 - * @param bool $return 是否返回数据 * @return mixed + * @throws \InvalidArgumentException */ - public static function send($data = [], $type = '', $return = false) + public function send() { - if ('' == $type) { - $type = self::$type ?: (IS_AJAX ? Config::get('default_ajax_return') : Config::get('default_return_type')); - } - $type = strtolower($type); - $data = $data ?: self::$data; + // 处理输出数据 + $data = $this->getContent(); - if (!headers_sent() && isset(self::$contentType[$type])) { - header('Content-Type:' . self::$contentType[$type] . '; charset=utf-8'); - } - - if (is_callable(self::$transform)) { - $data = call_user_func_array(self::$transform, [$data]); - } else { - switch ($type) { - case 'json': - // 返回JSON数据格式到客户端 包含状态信息 - $data = json_encode($data, JSON_UNESCAPED_UNICODE); - break; - case 'jsonp': - // 返回JSON数据格式到客户端 包含状态信息 - $handler = !empty($_GET[Config::get('var_jsonp_handler')]) ? $_GET[Config::get('var_jsonp_handler')] : Config::get('default_jsonp_handler'); - $data = $handler . '(' . json_encode($data, JSON_UNESCAPED_UNICODE) . ');'; - break; + if (!headers_sent() && !empty($this->header)) { + // 发送状态码 + http_response_code($this->code); + // 发送头部信息 + foreach ($this->header as $name => $val) { + header($name . ':' . $val); } } + echo $data; - APP_HOOK && Hook::listen('return_data', $data); - - if ($return) { - return $data; + if (function_exists('fastcgi_finish_request')) { + // 提高页面响应 + fastcgi_finish_request(); } - echo $data; - self::isExit() && exit(); } /** - * 转换控制器输出的数据 + * 处理数据 + * @access protected + * @param mixed $data 要处理的数据 + * @return mixed + */ + protected function output($data) + { + return $data; + } + + /** + * 输出的参数 * @access public - * @param mixed $callback 调用的转换方法 + * @param mixed $options 输出参数 * @return $this */ - public static function transform($callback) + public function options($options = []) { - self::$transform = $callback; + $this->options = array_merge($this->options, $options); + return $this; } /** @@ -148,97 +139,163 @@ class Response * @param mixed $data 输出数据 * @return $this */ - public static function data($data) + public function data($data) { - self::$data = $data; - } - - /** - * 输出类型设置 - * @access public - * @param string $type 输出内容的格式类型 - * @return mixed - */ - public static function type($type) - { - self::$type = $type; - } - - /** - * 输出是否exit设置 - * @access public - * @param bool $exit 是否退出 - * @return mixed - */ - public static function isExit($exit = null) - { - if (is_null($exit)) { - return self::$isExit; - } - self::$isExit = (boolean) $exit; - } - - /** - * 返回封装后的API数据到客户端 - * @access public - * @param mixed $data 要返回的数据 - * @param integer $code 返回的code - * @param string $msg 提示信息 - * @return mixed - */ - public static function result($data, $code = 0, $msg = '', $type = '') - { - $result = [ - 'code' => $code, - 'msg' => $msg, - 'time' => NOW_TIME, - 'data' => $data, - ]; - self::$type = $type; - return $result; - } - - /** - * URL重定向 - * @access public - * @param string $url 跳转的URL表达式 - * @param array|int $params 其它URL参数或http code - * @return void - */ - public static function redirect($url, $params = []) - { - $http_response_code = 301; - if (is_int($params) && in_array($params, [301, 302])) { - $http_response_code = $params; - $params = []; - } - $url = preg_match('/^(https?:|\/)/', $url) ? $url : Url::build($url, $params); - header('Location: ' . $url, true, $http_response_code); + $this->data = $data; + return $this; } /** * 设置响应头 * @access public - * @param string $name 参数名 - * @param string $value 参数值 - * @return void + * @param string|array $name 参数名 + * @param string $value 参数值 + * @return $this */ - public static function header($name, $value) + public function header($name, $value = null) { - header($name . ':' . $value); + if (is_array($name)) { + $this->header = array_merge($this->header, $name); + } else { + $this->header[$name] = $value; + } + return $this; + } + + /** + * 设置页面输出内容 + * @param $content + * @return $this + */ + public function content($content) + { + if (null !== $content && !is_string($content) && !is_numeric($content) && !is_callable([ + $content, + '__toString', + ]) + ) { + throw new \InvalidArgumentException(sprintf('variable type error: %s', gettype($content))); + } + + $this->content = (string)$content; + + return $this; } /** * 发送HTTP状态 * @param integer $code 状态码 - * @return void + * @return $this */ - public static function code($code) + public function code($code) { - if (isset(self::$statusCode[$code])) { - header('HTTP/1.1 ' . $code . ' ' . self::$statusCode[$code]); - // 确保FastCGI模式下正常 - header('Status:' . $code . ' ' . self::$statusCode[$code]); + $this->code = $code; + return $this; + } + + /** + * LastModified + * @param string $time + * @return $this + */ + public function lastModified($time) + { + $this->header['Last-Modified'] = $time; + return $this; + } + + /** + * Expires + * @param string $time + * @return $this + */ + public function expires($time) + { + $this->header['Expires'] = $time; + return $this; + } + + /** + * ETag + * @param string $eTag + * @return $this + */ + public function eTag($eTag) + { + $this->header['ETag'] = $eTag; + return $this; + } + + /** + * 页面缓存控制 + * @param string $cache 状态码 + * @return $this + */ + public function cacheControl($cache) + { + $this->header['Cache-control'] = $cache; + return $this; + } + + /** + * 页面输出类型 + * @param string $contentType 输出类型 + * @param string $charset 输出编码 + * @return $this + */ + public function contentType($contentType, $charset = 'utf-8') + { + $this->header['Content-Type'] = $contentType . '; charset=' . $charset; + return $this; + } + + /** + * 获取头部信息 + * @param string $name 头部名称 + * @return mixed + */ + public function getHeader($name = '') + { + return !empty($name) ? $this->header[$name] : $this->header; + } + + /** + * 获取原始数据 + * @return mixed + */ + public function getData() + { + return $this->data; + } + + /** + * 获取输出数据 + * @return mixed + */ + public function getContent() + { + if ($this->content == null) { + $content = $this->output($this->data); + + if (null !== $content && !is_string($content) && !is_numeric($content) && !is_callable([ + $content, + '__toString', + ]) + ) { + throw new \InvalidArgumentException(sprintf('variable type error: %s', gettype($content))); + } + + $this->content = (string)$content; } + return $this->content; + } + + /** + * 获取状态码 + * @return integer + */ + public function getCode() + { + return $this->code; } } diff --git a/library/think/Route.php b/library/think/Route.php index 17505a98..55fde12c 100644 --- a/library/think/Route.php +++ b/library/think/Route.php @@ -11,7 +11,12 @@ namespace think; +use think\App; +use think\Config; +use think\Hook; +use think\Log; use think\Request; +use think\Response; class Route { @@ -24,14 +29,18 @@ class Route 'HEAD' => [], 'OPTIONS' => [], '*' => [], + 'map' => [], + 'alias' => [], + 'domain' => [], + 'pattern' => [], ]; // REST路由操作方法定义 private static $rest = [ 'index' => ['GET', '', 'index'], 'create' => ['GET', '/create', 'create'], - 'read' => ['GET', '/:id', 'read'], 'edit' => ['GET', '/:id/edit', 'edit'], + 'read' => ['GET', '/:id', 'read'], 'save' => ['POST', '', 'save'], 'update' => ['PUT', '/:id', 'update'], 'delete' => ['DELETE', '/:id', 'delete'], @@ -45,153 +54,369 @@ class Route 'DELETE' => 'delete', ]; - // URL映射规则 - private static $map = []; - // 子域名部署规则 - private static $domain = []; // 子域名 private static $subDomain = ''; - // 变量规则 - private static $pattern = []; // 域名绑定 private static $bind = []; + // 当前分组 + private static $group; + // 当前参数 + private static $option = []; - // 添加URL映射规则 - public static function map($map = '', $route = '') - { - return self::setting('map', $map, $route); - } - - // 添加变量规则 - public static function pattern($name = '', $rule = '') - { - return self::setting('pattern', $name, $rule); - } - - // 添加子域名部署规则 - public static function domain($domain = '', $rule = '') - { - return self::setting('domain', $domain, $rule); - } - - // 属性设置 - private static function setting($var, $name = '', $value = '') + /** + * 注册变量规则 + * @access public + * @param string|array $name 变量名 + * @param string $rule 变量规则 + * @return void + */ + public static function pattern($name = null, $rule = '') { if (is_array($name)) { - self::${$var} = self::${$var}+$name; - } elseif (empty($value)) { - return empty($name) ? self::${$var} : self::${$var}[$name]; + self::$rules['pattern'] = array_merge(self::$rules['pattern'], $name); } else { - self::${$var}[$name] = $value; + self::$rules['pattern'][$name] = $rule; } } - // 对路由进行绑定和获取绑定信息 - public static function bind($type, $bind = '') + /** + * 注册子域名部署规则 + * @access public + * @param string|array $domain 子域名 + * @param mixed $rule 路由规则 + * @return void + */ + public static function domain($domain = null, $rule = '') { - if ('' == $bind) { - return isset(self::$bind[$type]) ? self::$bind[$type] : null; + if (is_array($domain)) { + self::$rules['domain'] = array_merge(self::$rules['domain'], $domain); } else { - self::$bind = ['type' => $type, $type => $bind]; + self::$rules['domain'][$domain] = $rule; } } - // 注册路由规则 - public static function register($rule, $route = '', $type = '*', $option = [], $pattern = []) + /** + * 设置路由绑定 + * @access public + * @param mixed $bind 绑定信息 + * @param string $type 绑定类型 默认为module + * @return mixed + */ + public static function bind($bind, $type = 'module') { + self::$bind = ['type' => $type, $type => $bind]; + } + + /** + * 读取路由绑定 + * @access public + * @param string $type 绑定类型 + * @return mixed + */ + public static function getBind($type) + { + return isset(self::$bind[$type]) ? self::$bind[$type] : null; + } + + /** + * 导入配置文件的路由规则 + * @access public + * @param array $rule 路由规则 + * @param string $type 请求类型 + * @return void + */ + public static function import(array $rule, $type = '*') + { + // 检查域名部署 + if (isset($rule['__domain__'])) { + self::domain($rule['__domain__']); + unset($rule['__domain__']); + } + + // 检查变量规则 + if (isset($rule['__pattern__'])) { + self::pattern($rule['__pattern__']); + unset($rule['__pattern__']); + } + + // 检查路由别名 + if (isset($rule['__alias__'])) { + self::alias($rule['__alias__']); + unset($rule['__alias__']); + } + + // 检查资源路由 + if (isset($rule['__rest__'])) { + self::resource($rule['__rest__']); + unset($rule['__rest__']); + } + + $type = strtoupper($type); + foreach ($rule as $key => $val) { + if (is_numeric($key)) { + $key = array_shift($val); + } + if (empty($val)) { + continue; + } + if (0 === strpos($key, '[')) { + $key = substr($key, 1, -1); + self::group($key, $val); + + } elseif (is_array($val)) { + self::setRule($key, $val[0], $type, $val[1], isset($val[2]) ? $val[2] : []); + } else { + self::setRule($key, $val, $type); + } + } + } + + /** + * 注册路由规则 + * @access public + * @param string $rule 路由规则 + * @param string $route 路由地址 + * @param string $type 请求类型 + * @param array $option 路由参数 + * @param array $pattern 变量规则 + * @return void + */ + public static function rule($rule, $route = '', $type = '*', $option = [], $pattern = []) + { + $group = self::$group; + $option = array_merge(self::$option, $option); + $type = strtoupper($type); + if (strpos($type, '|')) { - foreach (explode('|', $type) as $val) { - self::register($rule, $route, $val, $option); + $option['method'] = $type; + $type = '*'; + } + if (is_array($rule)) { + foreach ($rule as $key => $val) { + if (is_numeric($key)) { + $key = array_shift($val); + } + if (is_array($val)) { + $route = $val[0]; + $option1 = array_merge($option, $val[1]); + $pattern1 = array_merge($pattern, isset($val[2]) ? $val[2] : []); + } else { + $route = $val; + } + self::setRule($key, $route, $type, isset($option1) ? $option1 : $option, isset($pattern1) ? $pattern1 : $pattern, $group); } } else { - if (is_array($rule)) { - // 检查域名部署 - if (isset($rule['__domain__'])) { - self::domain($rule['__domain__']); - unset($rule['__domain__']); - } - // 检查变量规则 - if (isset($rule['__pattern__'])) { - self::pattern($rule['__pattern__']); - unset($rule['__pattern__']); - } - // 检查路由映射 - if (isset($rule['__map__'])) { - self::map($rule['__map__']); - unset($rule['__map__']); - } - // 检查资源路由 - if (isset($rule['__rest__'])) { - self::resource($rule['__rest__']); - unset($rule['__rest__']); - } + self::setRule($rule, $route, $type, $option, $pattern, $group); + } - foreach ($rule as $key => $val) { + } + + /** + * 设置路由规则 + * @access public + * @param string $rule 路由规则 + * @param string $route 路由地址 + * @param string $type 请求类型 + * @param array $option 路由参数 + * @param array $pattern 变量规则 + * @param string $group 所属分组 + * @return void + */ + protected static function setRule($rule, $route, $type = '*', $option = [], $pattern = [], $group = '') + { + if ('$' == substr($rule, -1, 1)) { + // 是否完整匹配 + $option['complete_match'] = true; + $rule = substr($rule, 0, -1); + } + if ('/' != $rule) { + $rule = trim($rule, '/'); + } + $vars = self::parseVar($rule); + if ($group) { + self::$rules[$type][$group]['rule'][] = ['rule' => $rule, 'route' => $route, 'var' => $vars, 'option' => $option, 'pattern' => $pattern]; + } else { + if ('*' != $type && isset(self::$rules['*'][$rule])) { + unset(self::$rules['*'][$rule]); + } + self::$rules[$type][$rule] = ['rule' => $rule, 'route' => $route, 'var' => $vars, 'option' => $option, 'pattern' => $pattern]; + if ('*' == $type) { + // 注册路由快捷方式 + foreach (['GET', 'POST', 'PUT', 'DELETE', 'HEAD', 'OPTIONS'] as $method) { + self::$rules[$method][$rule] = true; + } + } + } + } + + /** + * 设置当前的路由分组 + * @access public + * @param array $option 路由参数 + * @return void + */ + public static function setGroup($name) + { + self::$group = $name; + } + + /** + * 设置当前的路由参数 + * @access public + * @param array $option 路由参数 + * @return void + */ + public static function setOption($option) + { + self::$option = $option; + } + + /** + * 注册路由分组 + * @access public + * @param string|array $name 分组名称或者参数 + * @param array|\Closure $routes 路由地址 + * @param array $option 路由参数 + * @param string $type 请求类型 + * @param array $pattern 变量规则 + * @return void + */ + public static function group($name, $routes, $option = [], $type = '*', $pattern = []) + { + if (is_array($name)) { + $option = $name; + $name = isset($option['name']) ? $option['name'] : ''; + } + $type = strtoupper($type); + if (!empty($name)) { + // 分组 + if ($routes instanceof \Closure) { + self::setGroup($name); + call_user_func_array($routes, []); + self::setGroup(null); + self::$rules[$type][$name]['route'] = ''; + self::$rules[$type][$name]['var'] = self::parseVar($name); + self::$rules[$type][$name]['option'] = $option; + self::$rules[$type][$name]['pattern'] = $pattern; + + } else { + foreach ($routes as $key => $val) { if (is_numeric($key)) { $key = array_shift($val); } - if (0 === strpos($key, '[')) { - if (empty($val)) { - continue; - } - $key = substr($key, 1, -1); - $result = ['routes' => $val, 'option' => $option, 'pattern' => $pattern]; - } elseif (is_array($val)) { - $result = ['route' => $val[0], 'option' => $val[1], 'pattern' => isset($val[2]) ? $val[2] : []]; + if (is_array($val)) { + $route = $val[0]; + $option1 = array_merge($option, $val[1]); + $pattern1 = array_merge($pattern, isset($val[2]) ? $val[2] : []); } else { - $result = ['route' => $val, 'option' => $option, 'pattern' => $pattern]; + $route = $val; } - self::$rules[$type][$key] = $result; + $vars = self::parseVar($key); + $item[] = ['rule' => $key, 'route' => $route, 'var' => $vars, 'option' => isset($option1) ? $option1 : $option, 'pattern' => isset($pattern1) ? $pattern1 : $pattern]; } + self::$rules[$type][$name] = ['rule' => $item, 'route' => '', 'var' => [], 'option' => $option, 'pattern' => $pattern]; + } + if ('*' == $type) { + foreach (['GET', 'POST', 'PUT', 'DELETE', 'HEAD', 'OPTIONS'] as $method) { + if (!isset(self::$rules[$method][$name])) { + self::$rules[$method][$name] = true; + } else { + self::$rules[$method][$name] = array_merge(self::$rules['*'][$name], self::$rules[$method][$name]); + } + } + } + } else { + if ($routes instanceof \Closure) { + // 闭包注册 + self::setOption($option); + call_user_func_array($routes, []); + self::setOption([]); } else { - if (0 === strpos($rule, '[')) { - $rule = substr($rule, 1, -1); - $result = ['routes' => $route, 'option' => $option, 'pattern' => $pattern]; - } else { - $result = ['route' => $route, 'option' => $option, 'pattern' => $pattern]; - } - self::$rules[$type][$rule] = $result; + // 批量注册路由 + self::rule($routes, '', $type, $option, $pattern); } } } - // 路由分组 - public static function group($name, $routes = [], $type = '*', $option = [], $pattern = []) - { - self::$rules[$type][$name] = ['routes' => $routes, 'option' => $option, 'pattern' => $pattern]; - } - - // 注册任意请求的路由规则 + /** + * 注册路由 + * @access public + * @param string $rule 路由规则 + * @param string $route 路由地址 + * @param array $option 路由参数 + * @param array $pattern 变量规则 + * @return void + */ public static function any($rule, $route = '', $option = [], $pattern = []) { - self::register($rule, $route, '*', $option, $pattern); + self::rule($rule, $route, '*', $option, $pattern); } - // 注册get请求的路由规则 + /** + * 注册GET路由 + * @access public + * @param string $rule 路由规则 + * @param string $route 路由地址 + * @param array $option 路由参数 + * @param array $pattern 变量规则 + * @return void + */ public static function get($rule, $route = '', $option = [], $pattern = []) { - self::register($rule, $route, 'GET', $option, $pattern); + self::rule($rule, $route, 'GET', $option, $pattern); } - // 注册post请求的路由规则 + /** + * 注册POST路由 + * @access public + * @param string $rule 路由规则 + * @param string $route 路由地址 + * @param array $option 路由参数 + * @param array $pattern 变量规则 + * @return void + */ public static function post($rule, $route = '', $option = [], $pattern = []) { - self::register($rule, $route, 'POST', $option, $pattern); + self::rule($rule, $route, 'POST', $option, $pattern); } - // 注册put请求的路由规则 + /** + * 注册PUT路由 + * @access public + * @param string $rule 路由规则 + * @param string $route 路由地址 + * @param array $option 路由参数 + * @param array $pattern 变量规则 + * @return void + */ public static function put($rule, $route = '', $option = [], $pattern = []) { - self::register($rule, $route, 'PUT', $option, $pattern); + self::rule($rule, $route, 'PUT', $option, $pattern); } - // 注册delete请求的路由规则 + /** + * 注册DELETE路由 + * @access public + * @param string $rule 路由规则 + * @param string $route 路由地址 + * @param array $option 路由参数 + * @param array $pattern 变量规则 + * @return void + */ public static function delete($rule, $route = '', $option = [], $pattern = []) { - self::register($rule, $route, 'DELETE', $option, $pattern); + self::rule($rule, $route, 'DELETE', $option, $pattern); } - // 注册资源路由 + /** + * 注册资源路由 + * @access public + * @param string $rule 路由规则 + * @param string $route 路由地址 + * @param array $option 路由参数 + * @param array $pattern 变量规则 + * @return void + */ public static function resource($rule, $route = '', $option = [], $pattern = []) { if (is_array($rule)) { @@ -218,23 +443,57 @@ class Route || (isset($option['except']) && in_array($key, $option['except']))) { continue; } - if (strpos($val[1], ':id') && isset($option['var'][$rule])) { + if (isset($last) && strpos($val[1], ':id') && isset($option['var'][$last])) { + $val[1] = str_replace(':id', ':' . $option['var'][$last], $val[1]); + } elseif (strpos($val[1], ':id') && isset($option['var'][$rule])) { $val[1] = str_replace(':id', ':' . $option['var'][$rule], $val[1]); } - self::register($rule . $val[1] . '$', $route . '/' . $val[2], $val[0], $option, $pattern); + $item = ltrim($rule . $val[1], '/'); + self::rule($item ? $item . '$' : '', $route . '/' . $val[2], $val[0], $option, $pattern); } } } - // 注册别名路由 - public static function alias($rule, $route = '', $option = [], $pattern = []) + /** + * 注册控制器路由 操作方法对应不同的请求后缀 + * @access public + * @param string $rule 路由规则 + * @param string $route 路由地址 + * @param array $option 路由参数 + * @param array $pattern 变量规则 + * @return void + */ + public static function controller($rule, $route = '', $option = [], $pattern = []) { foreach (self::$methodPrefix as $type => $val) { self::$type($rule . '/:action', $route . '/' . $val . ':action', $option, $pattern); } } - // 设置不同请求类型下面的方法前缀 + /** + * 注册别名路由 + * @access public + * @param string|array $rule 路由别名 + * @param string $route 路由地址 + * @param array $option 路由参数 + * @return void + */ + public static function alias($rule = null, $route = '', $option = []) + { + if (is_array($rule)) { + self::$rules['alias'] = array_merge(self::$rules['alias'], $rule); + } else { + self::$rules['alias'][$rule] = $option ? [$route, $option] : $route; + } + } + + /** + * 设置不同请求类型下面的方法前缀 + * @access public + * @param string $method 请求类型 + * @param string $prefix 类型前缀 + * @return void + */ public static function setMethodPrefix($method, $prefix = '') { if (is_array($method)) { @@ -244,7 +503,13 @@ class Route } } - // rest方法定义和修改 + /** + * rest方法定义和修改 + * @access public + * @param string $name 方法名称 + * @param array $resourece 资源 + * @return void + */ public static function rest($name, $resource = []) { if (is_array($name)) { @@ -254,33 +519,61 @@ class Route } } - // 获取路由定义 - public static function getRules($method = '') + /** + * 注册未匹配路由规则后的处理 + * @access public + * @param string $route 路由地址 + * @param string $method 请求类型 + * @param array $option 路由参数 + * @return void + */ + public static function miss($route, $method = '*', $option = []) { - if ($method) { - return self::$rules[$method]; + self::rule('__miss__', $route, $method, $option, []); + } + + /** + * 获取或者批量设置路由定义 + * @access public + * @param mixed $rules 请求类型或者路由定义数组 + * @return array + */ + public static function rules($rules = '') + { + if (is_array($rules)) { + self::$rules = $rules; + } elseif ($rules) { + return self::$rules[$rules]; } else { - return self::$rules['*'] + self::$rules['GET'] + self::$rules['POST'] + self::$rules['PUT'] + self::$rules['DELETE']; + $rules = self::$rules; + unset($rules['pattern'], $rules['alias'], $rules['domain']); + return $rules; } } - // 检测子域名部署 - public static function checkDomain() + /** + * 检测子域名部署 + * @access public + * @param Request $request Request请求对象 + * @return void + */ + public static function checkDomain($request) { // 域名规则 - $rules = self::$domain; + $rules = self::$rules['domain']; // 开启子域名部署 支持二级和三级域名 if (!empty($rules)) { - if (isset($rules[$_SERVER['HTTP_HOST']])) { + $host = $request->host(); + if (isset($rules[$host])) { // 完整域名或者IP配置 - $rule = $rules[$_SERVER['HTTP_HOST']]; + $rule = $rules[$host]; } else { $rootDomain = Config::get('url_domain_root'); if ($rootDomain) { // 配置域名根 例如 thinkphp.cn 163.com.cn 如果是国家级域名 com.cn net.cn 之类的域名需要配置 - $domain = explode('.', rtrim(stristr($_SERVER['HTTP_HOST'], $rootDomain, true), '.')); + $domain = explode('.', rtrim(stristr($host, $rootDomain, true), '.')); } else { - $domain = explode('.', $_SERVER['HTTP_HOST'], -2); + $domain = explode('.', $host, -2); } // 子域名配置 if (!empty($domain)) { @@ -351,119 +644,177 @@ class Route } } - // 检测URL路由 - public static function check($url, $depr = '/', $checkDomain = false) + /** + * 检测URL路由 + * @access public + * @param Request $request Request请求对象 + * @param string $url URL地址 + * @param string $depr URL分隔符 + * @param bool $checkDomain 是否检测域名规则 + * @return false|array + */ + public static function check($request, $url, $depr = '/', $checkDomain = false) { - // 检测域名部署 - if ($checkDomain) { - self::checkDomain(); - } - // 分隔符替换 确保路由定义使用统一的分隔符 if ('/' != $depr) { $url = str_replace($depr, '/', $url); } - if (isset(self::$map[$url])) { - // URL映射 - return self::parseUrl(self::$map[$url], $depr); + if (strpos($url, '/') && isset(self::$rules['alias'][strstr($url, '/', true)])) { + // 检测路由别名 + $result = self::checkRouteAlias($request, $url, $depr); + if (false !== $result) { + return $result; + } } + // 检测域名部署 + if ($checkDomain) { + self::checkDomain($request); + } // 获取当前请求类型的路由规则 - $rules = self::$rules[REQUEST_METHOD]; - - if (!empty(self::$rules['*'])) { - // 合并任意请求的路由规则 - $rules = array_merge(self::$rules['*'], $rules); - } + $rules = self::$rules[$request->method()]; // 检测URL绑定 - $return = self::checkUrlBind($url, $rules); - if ($return) { + $return = self::checkUrlBind($url, $rules, $depr); + if (false !== $return) { return $return; } + if (isset($rules[$url])) { + // 静态路由规则检测 + $rule = $rules[$url]; + if (true === $rule) { + $rule = self::$rules['*'][$url]; + } + return self::parseRule($url, $rule['route'], $url, $rule['option']); + } + // 路由规则检测 if (!empty($rules)) { - foreach ($rules as $rule => $val) { - $option = $val['option']; - $pattern = $val['pattern']; - - // 参数有效性检查 - if (!self::checkOption($option, $url)) { - continue; - } - - if (!empty($val['routes'])) { - // 分组路由 - if (0 !== strpos($url, $rule)) { - continue; - } - // 匹配到路由分组 - foreach ($val['routes'] as $key => $route) { - if (is_numeric($key)) { - $key = array_shift($route); - } - $url1 = substr($url, strlen($rule) + 1); - // 检查规则路由 - if (is_array($route)) { - $option1 = $route[1]; - // 检查参数有效性 - if (!self::checkOption($option1, $url)) { - continue; - } - $pattern = array_merge($pattern, isset($route[2]) ? $route[2] : []); - $route = $route[0]; - $option = array_merge($option, $option1); - } - $result = self::checkRule($key, $route, $url1, $pattern, $option); - if (false !== $result) { - Request::instance()->route(['rule' => $key, 'route' => $route, 'pattern' => $pattern, 'option' => $option]); - return $result; - } - } - } else { - if (is_numeric($rule)) { - $rule = array_shift($val); - } - // 单项路由 - $route = !empty($val['route']) ? $val['route'] : ''; - // 规则路由 - $result = self::checkRule($rule, $route, $url, $pattern, $option); - if (false !== $result) { - Request::instance()->route(['rule' => $rule, 'route' => $route, 'pattern' => $pattern, 'option' => $option]); - return $result; - } - } - } + return self::checkRoute($request, $rules, $url); } return false; } - // 检测URL绑定 - private static function checkUrlBind(&$url, &$rules) + /** + * 检测路由规则 + * @access private + * @param Request $request + * @param array $rules 路由规则 + * @param string $url URL地址 + * @param string $group 路由分组名 + * @return mixed + */ + private static function checkRoute($request, $rules, $url, $group = '') + { + foreach ($rules as $key => $item) { + if (true === $item) { + $item = self::$rules['*'][$key]; + } + $rule = $item['rule']; + $route = $item['route']; + $vars = $item['var']; + $option = $item['option']; + $pattern = $item['pattern']; + + // 检查参数有效性 + if (!self::checkOption($option, $url, $request)) { + continue; + } + + if (is_array($rule)) { + // 分组路由 + if (($pos = strpos($key, ':')) || ($pos = strpos($key, '<'))) { + $str = substr($key, 0, $pos); + } else { + $str = $key; + } + if (0 !== strpos($url, $str)) { + continue; + } + + $result = self::checkRoute($request, $rule, $url, $key); + if (false !== $result) { + return $result; + } + } else { + if ('__miss__' == $rule) { + // 指定MISS路由 + $miss = $item; + continue; + } + if ($group) { + $rule = $group . '/' . ltrim($rule, '/'); + } + $result = self::checkRule($rule, $route, $url, $pattern, $option); + if (false !== $result) { + return $result; + } + } + } + if (isset($miss)) { + // 未匹配所有路由的路由规则处理 + return self::parseRule('', $miss['route'], $url, $miss['option']); + } + return false; + } + + /** + * 检测路由别名 + * @access private + * @param Request $request + * @param string $url URL地址 + * @param string $depr URL分隔符 + * @return mixed + */ + private static function checkRouteAlias($request, $url, $depr) + { + $array = explode('/', $url, 2); + $item = self::$rules['alias'][$array[0]]; + + if (is_array($item)) { + list($rule, $option) = $item; + } else { + $rule = $item; + } + // 参数有效性检查 + if (isset($option) && !self::checkOption($option, $url, $request)) { + // 路由不匹配 + return false; + } elseif (0 === strpos($rule, '\\')) { + // 路由到类 + return self::bindToClass($array[1], substr($rule, 1), $depr); + } elseif (0 === strpos($url, '@')) { + // 路由到控制器类 + return self::bindToController($array[1], substr($rule, 1), $depr); + } else { + // 路由到模块/控制器 + return self::bindToModule($array[1], $rule, $depr); + } + } + + /** + * 检测URL绑定 + * @access private + * @param string $url URL地址 + * @param array $rules 路由规则 + * @param string $depr URL分隔符 + * @return mixed + */ + private static function checkUrlBind(&$url, &$rules, $depr = '/') { if (!empty(self::$bind['type'])) { // 记录绑定信息 - APP_DEBUG && Log::record('[ BIND ] ' . var_export(self::$bind, true), 'info'); + App::$debug && Log::record('[ BIND ] ' . var_export(self::$bind, true), 'info'); // 如果有URL绑定 则进行绑定检测 switch (self::$bind['type']) { case 'class': // 绑定到类 - $array = explode('/', $url, 2); - if (!empty($array[1])) { - self::parseUrlParams($array[1]); - } - return ['type' => 'method', 'method' => [self::$bind['class'], $array[0] ?: Config::get('default_action')], 'params' => []]; + return self::bindToClass($url, self::$bind['class'], $depr); case 'namespace': // 绑定到命名空间 - $array = explode('/', $url, 3); - $class = !empty($array[0]) ? $array[0] : Config::get('default_controller'); - $method = !empty($array[1]) ? $array[1] : Config::get('default_action'); - if (!empty($array[2])) { - self::parseUrlParams($array[2]); - } - return ['type' => 'method', 'method' => [self::$bind['namespace'] . '\\' . $class, $method], 'params' => []]; + return self::bindToNamespace($url, self::$bind['namespace'], $depr); case 'module': // 如果有模块/控制器绑定 针对路由到 模块/控制器 有效 $url = self::$bind['module'] . '/' . $url; @@ -479,15 +830,95 @@ class Route return false; } - // 路由参数有效性检查 - private static function checkOption($option, $url) + /** + * 绑定到类 + * @access public + * @param string $url URL地址 + * @param string $class 类名(带命名空间) + * @param string $depr URL分隔符 + * @return array + */ + public static function bindToClass($url, $class, $depr = '/') + { + $array = explode($depr, $url, 2); + $action = !empty($array[0]) ? $array[0] : Config::get('default_action'); + if (!empty($array[1])) { + self::parseUrlParams($array[1]); + } + return ['type' => 'method', 'method' => [$class, $action], 'params' => []]; + } + + /** + * 绑定到命名空间 + * @access public + * @param string $url URL地址 + * @param string $namespace 命名空间 + * @param string $depr URL分隔符 + * @return array + */ + public static function bindToNamespace($url, $namespace, $depr = '/') + { + $array = explode($depr, $url, 3); + $class = !empty($array[0]) ? $array[0] : Config::get('default_controller'); + $method = !empty($array[1]) ? $array[1] : Config::get('default_action'); + if (!empty($array[2])) { + self::parseUrlParams($array[2]); + } + return ['type' => 'method', 'method' => [$namespace . '\\' . $class, $method], 'params' => []]; + } + + /** + * 绑定到控制器类 + * @access public + * @param string $url URL地址 + * @param string $controller 控制器名 (支持带模块名 index/user ) + * @param string $depr URL分隔符 + * @return array + */ + public static function bindToController($url, $controller, $depr = '/') + { + $array = explode($depr, $url, 2); + $action = !empty($array[0]) ? $array[0] : Config::get('default_action'); + if (!empty($array[1])) { + self::parseUrlParams($array[1]); + } + return ['type' => 'controller', 'controller' => $controller . '/' . $action, 'params' => []]; + } + + /** + * 绑定到模块/控制器 + * @access public + * @param string $url URL地址 + * @param string $class 控制器类名(带命名空间) + * @param string $depr URL分隔符 + * @return array + */ + public static function bindToModule($url, $controller, $depr = '/') + { + $array = explode($depr, $url, 2); + $action = !empty($array[0]) ? $array[0] : Config::get('default_action'); + if (!empty($array[1])) { + self::parseUrlParams($array[1]); + } + return ['type' => 'module', 'module' => $controller . '/' . $action]; + } + + /** + * 路由参数有效性检查 + * @access private + * @param array $option 路由参数 + * @param string $url URL地址 + * @param Request $request Request对象 + * @return bool + */ + private static function checkOption($option, $url, $request) { // 请求类型检测 - if ((isset($option['method']) && false === stripos($option['method'], REQUEST_METHOD)) - || (isset($option['ext']) && false === stripos($option['ext'], __EXT__)) // 伪静态后缀检测 + if ((isset($option['method']) && false === stripos($option['method'], $request->method())) + || (isset($option['ext']) && false === stripos($option['ext'], $request->ext())) // 伪静态后缀检测 || (isset($option['domain']) && !in_array($option['domain'], [$_SERVER['HTTP_HOST'], self::$subDomain])) // 域名检测 - || (!empty($option['https']) && !self::isSsl()) // https检测 - || (!empty($option['before_behavior']) && false === Hook::exec($option['before_behavior'], $url)) // 行为检测 + || (!empty($option['https']) && !$request->isSsl()) // https检测 + || (!empty($option['before_behavior']) && false === Hook::exec($option['before_behavior'], '', $url)) // 行为检测 || (!empty($option['callback']) && is_callable($option['callback']) && false === call_user_func($option['callback'])) // 自定义检测 ) { return false; @@ -496,7 +927,14 @@ class Route } /** - * 检查规则路由 + * 检测路由规则 + * @access private + * @param string $rule 路由规则 + * @param string $route 路由地址 + * @param string $url URL地址 + * @param array $pattern 变量规则 + * @param array $option 路由参数 + * @return array|false */ private static function checkRule($rule, $route, $url, $pattern, $option) { @@ -509,125 +947,122 @@ class Route $url = str_replace($depr, '/', $url); $rule = str_replace($depr, '/', $rule); } + $len1 = substr_count($url, '/'); $len2 = substr_count($rule, '/'); + // 多余参数是否合并 + $merge = !empty($option['merge_extra_vars']) ? true : false; + if ($len1 >= $len2 || strpos($rule, '[')) { - if ('$' == substr($rule, -1, 1)) { + if (!empty($option['complete_match'])) { // 完整匹配 - if ($len1 != $len2 && false === strpos($rule, '[')) { + if (!$merge && $len1 != $len2 && (false === strpos($rule, '[') || $len1 > $len2 || $len1 < $len2 - substr_count($rule, '['))) { return false; - } else { - $rule = substr($rule, 0, -1); } } - $pattern = array_merge(self::$pattern, $pattern); - if (false !== $match = self::match($url, $rule, $pattern)) { + $pattern = array_merge(self::$rules['pattern'], $pattern); + if (false !== $match = self::match($url, $rule, $pattern, $merge)) { // 匹配到路由规则 - // 检测是否定义路由 - if (!empty($option['after_behavior'])) { - $result = Hook::exec($option['after_behavior'], $route); - if (false === $result) { - return ['type' => 'finish']; - } - } - if ($route instanceof \Closure) { - // 执行闭包 - return ['type' => 'function', 'function' => $route, 'params' => $match]; - } - return self::parseRule($rule, $route, $url, $match); + return self::parseRule($rule, $route, $url, $option, $match, $merge); } } return false; } /** - * 判断是否SSL协议 - * @return boolean + * 解析模块的URL地址 [模块/控制器/操作?]参数1=值1&参数2=值2... + * @access public + * @param string $url URL地址 + * @param string $depr URL分隔符 + * @param bool $autoSearch 是否自动深度搜索控制器 + * @return array */ - public static function isSsl() - { - if (isset($_SERVER['HTTPS']) && ('1' == $_SERVER['HTTPS'] || 'on' == strtolower($_SERVER['HTTPS']))) { - return true; - } elseif (isset($_SERVER['SERVER_PORT']) && ('443' == $_SERVER['SERVER_PORT'])) { - return true; - } else { - return false; - } - } - - // 解析模块的URL地址 [模块/控制器/操作?]参数1=值1&参数2=值2... - public static function parseUrl($url, $depr = '/') + public static function parseUrl($url, $depr = '/', $autoSearch = false) { if (isset(self::$bind['module'])) { // 如果有模块/控制器绑定 $url = self::$bind['module'] . '/' . $url; } + + list($path, $var) = self::parseUrlPath($url, $depr); + $route = [null, null, null]; + if (isset($path)) { + // 解析模块 + $module = Config::get('app_multi_module') ? array_shift($path) : null; + if ($autoSearch) { + // 自动搜索控制器 + $dir = APP_PATH . ($module ? $module . DS : '') . 'controller'; + $suffix = App::$suffix || Config::get('controller_suffix') ? ucfirst(Config::get('url_controller_layer')) : ''; + $item = []; + foreach ($path as $val) { + $item[] = array_shift($path); + if (is_file($dir . DS . $val . $suffix . EXT)) { + break; + } else { + $dir .= DS . $val; + } + } + $controller = implode('.', $item); + } else { + // 解析控制器 + $controller = !empty($path) ? array_shift($path) : null; + } + // 解析操作 + $action = !empty($path) ? array_shift($path) : null; + // 解析额外参数 + self::parseUrlParams(empty($path) ? '' : implode('/', $path)); + // 封装路由 + $route = [$module, $controller, $action]; + } + return ['type' => 'module', 'module' => $route]; + } + + /** + * 解析URL的pathinfo参数和变量 + * @access private + * @param string $url URL地址 + * @param string $depr URL分隔符 + * @return array + */ + private static function parseUrlPath($url, $depr = '/') + { // 分隔符替换 确保路由定义使用统一的分隔符 if ('/' != $depr) { $url = str_replace($depr, '/', $url); } - $result = self::parseRoute($url, true); - if (!empty($result['var'])) { - $_GET = array_merge($result['var'], $_GET); - } - return ['type' => 'module', 'module' => $result['route']]; - } - - // 解析规范的路由地址 - // 地址格式 [模块/控制器/操作?]参数1=值1&参数2=值2... - private static function parseRoute($url, $reverse = false) - { $url = trim($url, '/'); $var = []; if (false !== strpos($url, '?')) { // [模块/控制器/操作?]参数1=值1&参数2=值2... $info = parse_url($url); - $path = explode('/', $info['path'], APP_MULTI_MODULE ? 4 : 3); + $path = explode('/', $info['path']); parse_str($info['query'], $var); } elseif (strpos($url, '/')) { // [模块/控制器/操作] - $path = explode('/', $url, APP_MULTI_MODULE ? 4 : 3); + $path = explode('/', $url); } elseif (false !== strpos($url, '=')) { // 参数1=值1&参数2=值2... parse_str($url, $var); } else { $path = [$url]; } - $route = [null, null, null]; - if (isset($path)) { - // 解析path额外的参数 - if (!empty($path[APP_MULTI_MODULE ? 3 : 2])) { - preg_replace_callback('/([^\/]+)\/([^\/]+)/', function ($match) use (&$var) { - $var[strtolower($match[1])] = strip_tags($match[2]); - }, array_pop($path)); - } - // 解析[模块/控制器/操作] - if ($reverse) { - $module = APP_MULTI_MODULE ? array_shift($path) : null; - $controller = !empty($path) ? array_shift($path) : null; - $action = !empty($path) ? array_shift($path) : null; - } else { - $action = array_pop($path); - $controller = !empty($path) ? array_pop($path) : null; - $module = APP_MULTI_MODULE && !empty($path) ? array_pop($path) : null; - // REST 操作方法支持 - if ('[rest]' == $action) { - $action = REQUEST_METHOD; - } elseif (Config::get('use_action_prefix') && !empty(self::$methodPrefix[REQUEST_METHOD])) { - // 操作方法前缀支持 - $action = 0 !== strpos($action, self::$methodPrefix[REQUEST_METHOD]) ? self::$methodPrefix[REQUEST_METHOD] . $action : $action; - } - } - $route = [$module, $controller, $action]; - } - return ['route' => $route, 'var' => $var]; + return [$path, $var]; } - // 检测URL和规则路由是否匹配 - private static function match($url, $rule, $pattern) + /** + * 检测URL和规则路由是否匹配 + * @access private + * @param string $url URL地址 + * @param string $rule 路由规则 + * @param array $pattern 变量规则 + * @param bool $merge 合并额外变量 + * @return array|false + */ + private static function match($url, $rule, $pattern, $merge) { - $m1 = explode('/', $url); - $m2 = explode('/', $rule); + $m2 = explode('/', $rule); + $m1 = $merge ? explode('/', $url, count($m2)) : explode('/', $url); + $var = []; foreach ($m2 as $key => $val) { // val中定义了多个变量 @@ -673,68 +1108,170 @@ class Route return $var; } - // 解析规则路由 - private static function parseRule($rule, $route, $pathinfo, $matches) + /** + * 解析规则路由 + * @access private + * @param string $rule 路由规则 + * @param string $route 路由地址 + * @param string $pathinfo URL地址 + * @param array $option 路由参数 + * @param array $matches 匹配的变量 + * @param bool $merge 合并额外变量 + * @return array + */ + private static function parseRule($rule, $route, $pathinfo, $option = [], $matches = [], $merge = false) { - // 获取URL地址中的参数 - $paths = explode('/', $pathinfo); - // 获取路由地址规则 - $url = is_array($route) ? $route[0] : $route; - // 解析路由规则 - $rule = explode('/', $rule); - foreach ($rule as $item) { - $fun = ''; - if (0 === strpos($item, '[:')) { - $item = substr($item, 1, -1); - } - if (0 === strpos($item, ':')) { - $var = substr($item, 1); - $matches[$var] = array_shift($paths); + // 检测是否定义路由 + if (!empty($option['after_behavior'])) { + if ($option['after_behavior'] instanceof \Closure) { + $result = call_user_func_array($option['after_behavior'], [$route]); } else { - // 过滤URL中的静态变量 - array_shift($paths); + foreach ((array) $option['after_behavior'] as $behavior) { + $result = Hook::exec($behavior, '', $route); + if (!is_null($result)) { + break; + } + } + } + // 路由规则重定向 + if ($result instanceof Response) { + return ['type' => 'response', 'response' => $result, 'params' => $matches]; + } elseif (is_array($result)) { + return $result; } } + + // 解析路由规则 + if ($rule) { + $rule = explode('/', $rule); + // 获取URL地址中的参数 + $paths = $merge ? explode('/', $pathinfo, count($rule)) : explode('/', $pathinfo); + foreach ($rule as $item) { + $fun = ''; + if (0 === strpos($item, '[:')) { + $item = substr($item, 1, -1); + } + if (0 === strpos($item, ':')) { + $var = substr($item, 1); + $matches[$var] = array_shift($paths); + } else { + // 过滤URL中的静态变量 + array_shift($paths); + } + } + } else { + $paths = explode('/', $pathinfo); + } + // 获取路由地址规则 + $url = $route; // 替换路由地址中的变量 - foreach ($matches as $key => $val) { - if (false !== strpos($url, ':' . $key)) { - $url = str_replace(':' . $key, $val, $url); - unset($matches[$key]); + if (is_string($url) && !empty($matches)) { + foreach ($matches as $key => $val) { + if (false !== strpos($url, ':' . $key)) { + $url = str_replace(':' . $key, $val, $url); + unset($matches[$key]); + } } } - if (0 === strpos($url, '/') || 0 === strpos($url, 'http')) { + if ($url instanceof \Closure) { + // 执行闭包 + $result = ['type' => 'function', 'function' => $url, 'params' => $matches]; + } elseif (0 === strpos($url, '/') || 0 === strpos($url, 'http')) { // 路由到重定向地址 - $result = ['type' => 'redirect', 'url' => $url, 'status' => (is_array($route) && isset($route[1])) ? $route[1] : 301]; + $result = ['type' => 'redirect', 'url' => $url, 'status' => isset($option['status']) ? $option['status'] : 301]; } elseif (0 === strpos($url, '\\')) { // 路由到方法 - $result = ['type' => 'method', 'method' => is_array($route) ? [$url, $route[1]] : $url, 'params' => $matches]; + $method = strpos($url, '@') ? explode('@', $url) : $url; + $result = ['type' => 'method', 'method' => $method, 'params' => $matches]; } elseif (0 === strpos($url, '@')) { // 路由到控制器 $result = ['type' => 'controller', 'controller' => substr($url, 1), 'params' => $matches]; } else { - // 解析路由地址 - $result = self::parseRoute($url); - $var = array_merge($matches, $result['var']); - // 解析剩余的URL参数 - self::parseUrlParams(implode('/', $paths), $var); // 路由到模块/控制器/操作 - $result = ['type' => 'module', 'module' => $result['route']]; - // 路由地址中的控制器和操作关闭自动转换 - Config::set('url_controller_convert', false); - Config::set('url_action_convert', false); + $result = self::parseModule($url); } + // 解析额外参数 + self::parseUrlParams(empty($paths) ? '' : implode('/', $paths), $matches); + // 记录匹配的路由信息 + Request::instance()->routeInfo(['rule' => $rule, 'route' => $route, 'option' => $option]); return $result; } - // 解析URL地址中的参数到$_GET - private static function parseUrlParams($url, $var) + /** + * 解析URL地址为 模块/控制器/操作 + * @access private + * @param string $url URL地址 + * @param string $depr URL分隔符 + * @return array + */ + private static function parseModule($url, $depr = '/') { - if ($url) { - preg_replace_callback('/(\w+)\/([^\/]+)/', function ($match) use (&$var) { - $var[strtolower($match[1])] = strip_tags($match[2]); - }, $url); + list($path, $var) = self::parseUrlPath($url, $depr); + $action = array_pop($path); + $controller = !empty($path) ? array_pop($path) : null; + $module = Config::get('app_multi_module') && !empty($path) ? array_pop($path) : null; + $method = Request::instance()->method(); + if (Config::get('use_action_prefix') && !empty(self::$methodPrefix[$method])) { + // 操作方法前缀支持 + $action = 0 !== strpos($action, self::$methodPrefix[$method]) ? self::$methodPrefix[$method] . $action : $action; } - $_GET = array_merge($var, $_GET); + $_GET = array_merge($_GET, $var); + // 路由到模块/控制器/操作 + return ['type' => 'module', 'module' => [$module, $controller, $action], 'convert' => false]; } + /** + * 解析URL地址中的参数Request对象 + * @access private + * @param string $rule 路由规则 + * @param array $var 变量 + * @return void + */ + private static function parseUrlParams($url, $var = []) + { + if ($url) { + if (Config::get('url_param_type')) { + $var += explode('/', $url); + } else { + preg_replace_callback('/(\w+)\/([^\/]+)/', function ($match) use (&$var) { + $var[$match[1]] = strip_tags($match[2]); + }, $url); + } + } + // 设置当前请求的参数 + Request::instance()->route($var); + } + + // 分析路由规则中的变量 + private static function parseVar($rule) + { + // 提取路由规则中的变量 + $var = []; + foreach (explode('/', $rule) as $val) { + $optional = false; + if (false !== strpos($val, '<') && preg_match_all('/<(\w+(\??))>/', $val, $matches)) { + foreach ($matches[1] as $name) { + if (strpos($name, '?')) { + $name = substr($name, 0, -1); + $optional = true; + } else { + $optional = false; + } + $var[$name] = $optional ? 2 : 1; + } + } + + if (0 === strpos($val, '[:')) { + // 可选参数 + $optional = true; + $val = substr($val, 1, -1); + } + if (0 === strpos($val, ':')) { + // URL变量 + $name = substr($val, 1); + $var[$name] = $optional ? 2 : 1; + } + } + return $var; + } } diff --git a/library/think/Session.php b/library/think/Session.php index 1889cef5..1b419763 100644 --- a/library/think/Session.php +++ b/library/think/Session.php @@ -11,11 +11,12 @@ namespace think; +use think\App; +use think\exception\ClassNotFoundException; + class Session { - protected static $prefix = ''; - protected static $active = false; /** * 设置或者获取session作用域(前缀) @@ -43,7 +44,7 @@ class Session $config = Config::get('session'); } // 记录初始化信息 - APP_DEBUG && Log::record('[ SESSION ] INIT ' . var_export($config, true), 'info'); + App::$debug && Log::record('[ SESSION ] INIT ' . var_export($config, true), 'info'); $isDoStart = false; if (isset($config['use_trans_sid'])) { ini_set('session.use_trans_sid', $config['use_trans_sid'] ? 1 : 0); @@ -88,29 +89,28 @@ class Session } if (!empty($config['type'])) { // 读取session驱动 - $class = (!empty($config['namespace']) ? $config['namespace'] : '\\think\\session\\driver\\') . ucwords($config['type']); + $class = false !== strpos($config['type'], '\\') ? $config['type'] : '\\think\\session\\driver\\' . ucwords($config['type']); // 检查驱动类 if (!class_exists($class) || !session_set_save_handler(new $class($config))) { - throw new \think\Exception('error session handler', 11700); + throw new ClassNotFoundException('error session handler:' . $class, $class); } } if ($isDoStart) { session_start(); - self::$active = true; } } /** * session设置 - * @param string $name session名称 - * @param mixed $value session值 - * @param string|null $prefix 作用域(前缀) + * @param string $name session名称 + * @param mixed $value session值 + * @param string|null $prefix 作用域(前缀) * @return void */ public static function set($name, $value = '', $prefix = null) { - !self::$active && self::init(); + !isset($_SESSION) && self::init(); $prefix = !is_null($prefix) ? $prefix : self::$prefix; if (strpos($name, '.')) { // 二维数组赋值 @@ -129,13 +129,13 @@ class Session /** * session获取 - * @param string $name session名称 - * @param string|null $prefix 作用域(前缀) + * @param string $name session名称 + * @param string|null $prefix 作用域(前缀) * @return mixed */ public static function get($name = '', $prefix = null) { - !self::$active && self::init(); + !isset($_SESSION) && self::init(); $prefix = !is_null($prefix) ? $prefix : self::$prefix; if ('' == $name) { // 获取全部的session @@ -161,13 +161,13 @@ class Session /** * 删除session数据 - * @param string $name session名称 - * @param string|null $prefix 作用域(前缀) + * @param string $name session名称 + * @param string|null $prefix 作用域(前缀) * @return void */ public static function delete($name, $prefix = null) { - !self::$active && self::init(); + !isset($_SESSION) && self::init(); $prefix = !is_null($prefix) ? $prefix : self::$prefix; if (strpos($name, '.')) { list($name1, $name2) = explode('.', $name); @@ -187,12 +187,12 @@ class Session /** * 清空session数据 - * @param string|null $prefix 作用域(前缀) + * @param string|null $prefix 作用域(前缀) * @return void */ public static function clear($prefix = null) { - !self::$active && self::init(); + !isset($_SESSION) && self::init(); $prefix = !is_null($prefix) ? $prefix : self::$prefix; if ($prefix) { unset($_SESSION[$prefix]); @@ -203,14 +203,13 @@ class Session /** * 判断session数据 - * @param string $name session名称 - * @param string|null $prefix + * @param string $name session名称 + * @param string|null $prefix * @return bool - * @internal param mixed $value session值 */ public static function has($name, $prefix = null) { - !self::$active && self::init(); + !isset($_SESSION) && self::init(); $prefix = !is_null($prefix) ? $prefix : self::$prefix; if (strpos($name, '.')) { // 支持数组 @@ -221,16 +220,6 @@ class Session } } - /** - * 暂停session - * @return void - */ - public static function pause() - { - // 暂停session - session_write_close(); - } - /** * 启动session * @return void @@ -238,7 +227,6 @@ class Session public static function start() { session_start(); - self::$active = true; } /** @@ -247,17 +235,30 @@ class Session */ public static function destroy() { - $_SESSION = []; + if (!empty($_SESSION)) { + $_SESSION = []; + } session_unset(); session_destroy(); } /** * 重新生成session_id + * @param bool $delete 是否删除关联会话文件 * @return void */ - private static function regenerate() + private static function regenerate($delete = false) { - session_regenerate_id(); + session_regenerate_id($delete); + } + + /** + * 暂停session + * @return void + */ + public static function pause() + { + // 暂停session + session_write_close(); } } diff --git a/library/think/Template.php b/library/think/Template.php index 3124e755..97c01d4e 100644 --- a/library/think/Template.php +++ b/library/think/Template.php @@ -11,6 +11,8 @@ namespace think; +use think\exception\TemplateNotFoundException; + /** * ThinkPHP分离出来的模板引擎 * 支持XML标签和普通标签的模板解析 @@ -23,9 +25,9 @@ class Template // 引擎配置 protected $config = [ 'view_path' => '', // 模板路径 - 'view_suffix' => '.html', // 默认模板文件后缀 + 'view_suffix' => 'html', // 默认模板文件后缀 'view_depr' => DS, - 'cache_suffix' => '.php', // 默认模板缓存后缀 + 'cache_suffix' => 'php', // 默认模板缓存后缀 'tpl_deny_func_list' => 'echo,exit', // 模板引擎禁用函数 'tpl_deny_php' => false, // 默认模板引擎是否禁用PHP原生代码 'tpl_begin' => '{', // 模板引擎普通标签开始标记 @@ -47,12 +49,11 @@ class Template 'cache_id' => '', // 模板缓存ID 'tpl_replace_string' => [], 'tpl_var_identify' => 'array', // .语法变量识别,array|object|'', 为空时自动识别 - 'namespace' => '\\think\\template\\driver\\', ]; private $literal = []; private $includeFile = []; // 记录所有模板包含的文件路径及更新时间 - protected $storage = null; + protected $storage; /** * 架构函数 @@ -69,7 +70,7 @@ class Template // 初始化模板编译存储器 $type = $this->config['compile_type'] ? $this->config['compile_type'] : 'File'; - $class = $this->config['namespace'] . ucwords($type); + $class = false !== strpos($type, '\\') ? $type : '\\think\\template\\driver\\' . ucwords($type); $this->storage = new $class(); } @@ -156,9 +157,9 @@ class Template /** * 渲染模板文件 * @access public - * @param string $template 模板文件 - * @param array $vars 模板变量 - * @param array $config 模板参数 + * @param string $template 模板文件 + * @param array $vars 模板变量 + * @param array $config 模板参数 * @return void */ public function fetch($template, $vars = [], $config = []) @@ -179,7 +180,7 @@ class Template } $template = $this->parseTemplateFile($template); if ($template) { - $cacheFile = $this->config['cache_path'] . $this->config['cache_prefix'] . md5($template) . $this->config['cache_suffix']; + $cacheFile = $this->config['cache_path'] . $this->config['cache_prefix'] . md5($template) . '.' . ltrim($this->config['cache_suffix'], '.'); if (!$this->checkCache($cacheFile)) { // 缓存无效 重新模板编译 $content = file_get_contents($template); @@ -203,9 +204,9 @@ class Template /** * 渲染模板内容 * @access public - * @param string $content 模板内容 - * @param array $vars 模板变量 - * @param array $config 模板参数 + * @param string $content 模板内容 + * @param array $vars 模板变量 + * @param array $config 模板参数 * @return void */ public function display($content, $vars = [], $config = []) @@ -216,7 +217,7 @@ class Template if ($config) { $this->config($config); } - $cacheFile = $this->config['cache_path'] . $this->config['cache_prefix'] . md5($content) . $this->config['cache_suffix']; + $cacheFile = $this->config['cache_path'] . $this->config['cache_prefix'] . md5($content) . '.' . ltrim($this->config['cache_suffix'], '.'); if (!$this->checkCache($cacheFile)) { // 缓存无效 模板编译 $this->compiler($content, $cacheFile); @@ -228,8 +229,8 @@ class Template /** * 设置布局 * @access public - * @param mixed $name 布局模板名称 false 则关闭布局 - * @param string $replace 布局模板内容替换标识 + * @param mixed $name 布局模板名称 false 则关闭布局 + * @param string $replace 布局模板内容替换标识 * @return object */ public function layout($name, $replace = '') @@ -310,8 +311,8 @@ class Template /** * 编译模板文件内容 * @access private - * @param string $content 模板内容 - * @param string $cacheFile 缓存文件名 + * @param string $content 模板内容 + * @param string $cacheFile 缓存文件名 * @return void */ private function compiler(&$content, $cacheFile) @@ -329,6 +330,8 @@ class Template $content = str_replace($this->config['layout_item'], $content, file_get_contents($layoutFile)); } } + } else { + $content = str_replace('{__NOLAYOUT__}', '', $content); } // 模板解析 @@ -553,9 +556,15 @@ class Template $children[$parent][] = $name; continue; } + } elseif (!empty($val['parent'])) { + // 如果子标签没有被继承则用原值 + $children[$val['parent']][] = $name; + $blocks[$name] = $val; + } + if (!$val['parent']) { + // 替换模板中的顶级block标签 + $extend = str_replace($val['begin'] . $val['content'] . $val['end'], $replace, $extend); } - // 替换模板中的block标签 - $extend = str_replace($val['begin'] . $val['content'] . $val['end'], $replace, $extend); } } $content = $extend; @@ -567,8 +576,8 @@ class Template /** * 替换页面中的literal标签 * @access private - * @param string $content 模板内容 - * @param boolean $restore 是否为还原 + * @param string $content 模板内容 + * @param boolean $restore 是否为还原 * @return void */ private function parseLiteral(&$content, $restore = false) @@ -599,8 +608,8 @@ class Template /** * 获取模板中的block标签 * @access private - * @param string $content 模板内容 - * @param boolean $sort 是否排序 + * @param string $content 模板内容 + * @param boolean $sort 是否排序 * @return array */ private function parseBlock(&$content, $sort = false) @@ -662,14 +671,14 @@ class Template /** * TagLib库解析 * @access public - * @param string $tagLib 要解析的标签库 - * @param string $content 要解析的模板内容 - * @param boolean $hide 是否隐藏标签库前缀 + * @param string $tagLib 要解析的标签库 + * @param string $content 要解析的模板内容 + * @param boolean $hide 是否隐藏标签库前缀 * @return void */ public function parseTagLib($tagLib, &$content, $hide = false) { - if (strpos($tagLib, '\\')) { + if (false !== strpos($tagLib, '\\')) { // 支持指定标签库的命名空间 $className = $tagLib; $tagLib = substr($tagLib, strrpos($tagLib, '\\') + 1); @@ -684,8 +693,8 @@ class Template /** * 分析标签属性 * @access public - * @param string $str 属性字符串 - * @param string $name 不为空时返回指定的属性名 + * @param string $str 属性字符串 + * @param string $name 不为空时返回指定的属性名 * @return array */ public function parseAttr($str, $name = null) @@ -732,20 +741,15 @@ class Template $str = trim(substr($str, $pos + 1)); $this->parseVar($str); $first = substr($str, 0, 1); - if (isset($array[1])) { - $this->parseVar($array[2]); - $name .= $array[1] . $array[2]; - if ('=' == $first) { - // {$varname?='xxx'} $varname为真时才输出xxx - $str = ''; - } else { - $str = ''; - } - } elseif (')' == substr($name, -1, 1)) { + if (strpos($name, ')')) { // $name为对象或是自动识别,或者含有函数 + if (isset($array[1])) { + $this->parseVar($array[2]); + $name .= $array[1] . $array[2]; + } switch ($first) { case '?': - $str = ''; + $str = ''; break; case '=': $str = ''; @@ -754,26 +758,32 @@ class Template $str = ''; } } else { + if (isset($array[1])) { + $this->parseVar($array[2]); + $_name = ' && ' . $name . $array[1] . $array[2]; + } else { + $_name = ''; + } // $name为数组 switch ($first) { case '?': // {$varname??'xxx'} $varname有定义则输出$varname,否则输出xxx - $str = ''; + $str = ''; break; case '=': // {$varname?='xxx'} $varname为真时才输出xxx - $str = ''; + $str = ''; break; case ':': // {$varname?:'xxx'} $varname为真时输出$varname,否则输出xxx - $str = ''; + $str = ''; break; default: if (strpos($str, ':')) { // {$varname ? 'a' : 'b'} $varname为真时输出a,否则输出b - $str = ''; + $str = ''; } else { - $str = ''; + $str = ''; } } } @@ -844,6 +854,18 @@ class Template if ('$Think' == $first) { // 所有以Think.打头的以特殊变量对待 无需模板赋值就可以输出 $parseStr = $this->parseThinkVar($vars); + } elseif ('$Request' == $first) { + // 获取Request请求对象参数 + $method = array_shift($vars); + if (!empty($vars)) { + $params = implode('.', $vars); + if ('true' != $params) { + $params = '\'' . $params . '\''; + } + } else { + $params = ''; + } + $parseStr = '\think\Request::instance()->' . $method . '(' . $params . ')'; } else { switch ($this->config['tpl_var_identify']) { case 'array': // 识别为数组 @@ -1047,22 +1069,24 @@ class Template */ private function parseTemplateFile($template) { - if (false === strpos($template, '.')) { + if ('' == pathinfo($template, PATHINFO_EXTENSION)) { if (strpos($template, '@')) { // 跨模块调用模板 $template = str_replace(['/', ':'], $this->config['view_depr'], $template); - $template = APP_PATH . str_replace('@', '/' . basename($this->config['view_path']) . '/', $template) . $this->config['view_suffix']; + $template = APP_PATH . str_replace('@', '/' . basename($this->config['view_path']) . '/', $template); } else { $template = str_replace(['/', ':'], $this->config['view_depr'], $template); - $template = $this->config['view_path'] . $template . $this->config['view_suffix']; + $template = $this->config['view_path'] . $template; } + $template .= '.' . ltrim($this->config['view_suffix'], '.'); } + if (is_file($template)) { // 记录模板文件的更新时间 $this->includeFile[$template] = filemtime($template); return $template; } else { - throw new Exception('template not exist:' . $template, 10700); + throw new TemplateNotFoundException('template not exists:' . $template, $template); } } diff --git a/library/think/Url.php b/library/think/Url.php index 509ef6c6..cf570af0 100644 --- a/library/think/Url.php +++ b/library/think/Url.php @@ -11,17 +11,26 @@ namespace think; +use think\App; +use think\Cache; +use think\Config; +use think\Request; +use think\Route; + class Url { + // 生成URL地址的root + protected static $root; + /** * URL生成 支持路由反射 - * @param string $url URL表达式, + * @param string $url URL表达式, * 格式:'[模块/控制器/操作]?参数1=值1&参数2=值2...@域名' * @控制器/操作?参数1=值1&参数2=值2... * \\命名空间类\\方法?参数1=值1&参数2=值2... - * @param string|array $vars 传入的参数,支持数组和字符串 - * @param string|bool $suffix 伪静态后缀,默认为true表示获取配置值 - * @param boolean|string $domain 是否显示域名 或者直接传入域名 + * @param string|array $vars 传入的参数,支持数组和字符串 + * @param string|bool $suffix 伪静态后缀,默认为true表示获取配置值 + * @param boolean|string $domain 是否显示域名 或者直接传入域名 * @return string */ public static function build($url = '', $vars = '', $suffix = true, $domain = false) @@ -72,9 +81,9 @@ class Url } // 检测URL绑定 - $type = Route::bind('type'); + $type = Route::getBind('type'); if ($type) { - $bind = Route::bind($type); + $bind = Route::getBind($type); if (0 === strpos($url, $bind)) { $url = substr($url, strlen($bind) + 1); } @@ -92,7 +101,7 @@ class Url // 添加参数 if (Config::get('url_common_param')) { $vars = urldecode(http_build_query($vars)); - $url .= $suffix . $anchor . '?' . $vars; + $url .= $suffix . '?' . $vars . $anchor; } else { foreach ($vars as $var => $val) { if ('' !== trim($val)) { @@ -107,13 +116,14 @@ class Url // 检测域名 $domain = self::parseDomain($url, $domain); // URL组装 - $url = $domain . Config::get('base_url') . '/' . ltrim($url, '/'); + $url = $domain . (self::$root ?: Request::instance()->root()) . '/' . ltrim($url, '/'); return $url; } // 直接解析URL地址 protected static function parseUrl($url) { + $request = Request::instance(); if (0 === strpos($url, '/')) { // 直接作为路由地址解析 $url = substr($url, 1); @@ -125,14 +135,16 @@ class Url $url = substr($url, 1); } else { // 解析到 模块/控制器/操作 - $module = MODULE_NAME ? MODULE_NAME . '/' : ''; + $module = $request->module(); + $module = $module ? $module . '/' : ''; + $controller = $request->controller(); if ('' == $url) { // 空字符串输出当前的 模块/控制器/操作 - $url = $module . CONTROLLER_NAME . '/' . ACTION_NAME; + $url = $module . $controller . '/' . $request->action(); } else { $path = explode('/', $url); - $action = array_pop($path); - $controller = empty($path) ? CONTROLLER_NAME : (Config::get('url_controller_convert') ? Loader::parseName(array_pop($path)) : array_pop($path)); + $action = Config::get('url_convert') ? strtolower(array_pop($path)) : array_pop($path); + $controller = empty($path) ? $controller : (Config::get('url_convert') ? Loader::parseName(array_pop($path)) : array_pop($path)); $module = empty($path) ? $module : array_pop($path) . '/'; $url = $module . $controller . '/' . $action; } @@ -143,44 +155,44 @@ class Url // 检测域名 protected static function parseDomain(&$url, $domain) { - if ($domain) { - if (true === $domain) { - // 自动判断域名 - $domain = $_SERVER['HTTP_HOST']; - if (Config::get('url_domain_deploy')) { - // 根域名 - $urlDomainRoot = Config::get('url_domain_root'); - $domains = Route::domain(); - $route_domain = array_keys($domains); - foreach ($route_domain as $domain_prefix) { - if (0 === strpos($domain_prefix, '*.') && strpos($domain, ltrim($domain_prefix, '*.')) !== false) { - foreach ($domains as $key => $rule) { - $rule = is_array($rule) ? $rule[0] : $rule; - if (false === strpos($key, '*') && 0 === strpos($url, $rule)) { - $url = ltrim($url, $rule); - $domain = $key; - // 生成对应子域名 - if (!empty($urlDomainRoot)) { - $domain .= $urlDomainRoot; - } - break; - } else if (false !== strpos($key, '*')) { - if (!empty($urlDomainRoot)) { - $domain .= $urlDomainRoot; - } - break; + if (!$domain) { + return ''; + } + $request = Request::instance(); + if (true === $domain) { + // 自动判断域名 + $domain = $request->host(); + if (Config::get('url_domain_deploy')) { + // 根域名 + $urlDomainRoot = Config::get('url_domain_root'); + $domains = Route::rules('domain'); + $route_domain = array_keys($domains); + foreach ($route_domain as $domain_prefix) { + if (0 === strpos($domain_prefix, '*.') && strpos($domain, ltrim($domain_prefix, '*.')) !== false) { + foreach ($domains as $key => $rule) { + $rule = is_array($rule) ? $rule[0] : $rule; + if (false === strpos($key, '*') && 0 === strpos($url, $rule)) { + $url = ltrim($url, $rule); + $domain = $key; + // 生成对应子域名 + if (!empty($urlDomainRoot)) { + $domain .= $urlDomainRoot; } + break; + } else if (false !== strpos($key, '*')) { + if (!empty($urlDomainRoot)) { + $domain .= $urlDomainRoot; + } + break; } } } } - } else { - $domain .= strpos($domain, '.') ? '' : strstr($_SERVER['HTTP_HOST'], '.'); } - $domain = (self::isSsl() ? 'https://' : 'http://') . $domain; } else { - $domain = ''; + $domain .= strpos($domain, '.') ? '' : strstr($request->host(), '.'); } + $domain = ($request->isSsl() ? 'https://' : 'http://') . $domain; return $domain; } @@ -208,20 +220,6 @@ class Url return (empty($suffix) || 0 === strpos($suffix, '.')) ? $suffix : '.' . $suffix; } - /** - * 判断是否SSL协议 - * @return boolean - */ - public static function isSsl() - { - if (isset($_SERVER['HTTPS']) && ('1' == $_SERVER['HTTPS'] || 'on' == strtolower($_SERVER['HTTPS']))) { - return true; - } elseif (isset($_SERVER['SERVER_PORT']) && ('443' == $_SERVER['SERVER_PORT'])) { - return true; - } - return false; - } - // 匹配路由地址 public static function getRouteUrl($alias, &$vars = []) { @@ -233,6 +231,7 @@ class Url } // 检查变量匹配 $array = $vars; + $match = false; if ($pattern && self::pattern($pattern, $vars)) { foreach ($pattern as $key => $val) { if (isset($vars[$key])) { @@ -243,36 +242,54 @@ class Url } } $match = true; + } elseif (empty($pattern) && array_intersect_assoc($param, $array) == $param) { + $match = true; } - if (empty($pattern) && empty($param)) { - // 没有任何变量 - return $url; - } elseif (!empty($match) || !empty($param) && array_intersect($param, $array) == $param) { + if ($match && !empty($param) && array_intersect_assoc($param, $array) != $param) { + $match = false; + } + if ($match) { // 存在变量定义 - $vars = array_diff($array, $param); + $vars = array_diff_key($array, $param); return $url; } } return false; } - // 生成路由别名并缓存 + // 生成路由映射并缓存 private static function getRouteAlias() { - if ($alias = Cache::get('think_route_alias')) { - return $alias; + if ($item = Cache::get('think_route_map')) { + return $item; } // 获取路由定义 - $rules = Route::getRules(); - foreach ($rules as $rule => $val) { - if (!empty($val['routes'])) { - foreach ($val['routes'] as $key => $route) { - if (is_numeric($key)) { - $key = array_shift($route); - } - if (is_array($route)) { - $route = $route[0]; + $array = Route::rules(); + foreach ($array as $type => $rules) { + foreach ($rules as $rule => $val) { + if (true === $val || empty($val['rule'])) { + continue; + } + $route = $val['route']; + $vars = $val['var']; + if (is_array($val['rule'])) { + foreach ($val['rule'] as $val) { + $key = $val['rule']; + $route = $val['route']; + $var = $val['var']; + $param = []; + if (is_array($route)) { + $route = implode('\\', $route); + } elseif ($route instanceof \Closure) { + continue; + } elseif (strpos($route, '?')) { + list($route, $str) = explode('?', $route, 2); + parse_str($str, $param); + } + $var = array_merge($vars, $var); + $item[$route][] = [$rule . '/' . $key, $var, $param]; } + } else { $param = []; if (is_array($route)) { $route = implode('\\', $route); @@ -282,79 +299,31 @@ class Url list($route, $str) = explode('?', $route, 2); parse_str($str, $param); } - $var = self::parseVar($rule . '/' . $key); - $alias[$route][] = [$rule . '/' . $key, $var, $param]; + $item[$route][] = [$rule, $vars, $param]; } - } else { - $route = $val['route']; - $param = []; - if (is_array($route)) { - $route = implode('\\', $route); - } elseif ($route instanceof \Closure) { - continue; - } elseif (strpos($route, '?')) { - list($route, $str) = explode('?', $route, 2); - parse_str($str, $param); - } - $var = self::parseVar($rule); - $alias[$route][] = [$rule, $var, $param]; } } - // 检测路由映射 - $maps = Route::map(); - foreach ($maps as $rule => $route) { - $param = []; - if (strpos($route, '?')) { - list($route, $str) = explode('?', $route, 2); - parse_str($str, $param); - } - $alias[$route][] = [$rule, [], $param]; + // 检测路由别名 + $alias = Route::rules('alias'); + foreach ($alias as $rule => $route) { + $route = is_array($route) ? $route[0] : $route; + $item[$route][] = [$rule, [], []]; } - !APP_DEBUG && Cache::set('think_route_alias', $alias); - return $alias; - } - - // 分析路由规则中的变量 - private static function parseVar($rule) - { - // 检测是否设置了参数分隔符 - if ($depr = Config::get('url_params_depr')) { - $rule = str_replace($depr, '/', $rule); - } - // 提取路由规则中的变量 - $var = []; - foreach (explode('/', $rule) as $val) { - $optional = false; - if (false !== strpos($val, '<') && preg_match_all('/<(\w+(\??))>/', $val, $matches)) { - foreach ($matches[1] as $name) { - if (strpos($name, '?')) { - $name = substr($name, 0, -1); - $optional = true; - } else { - $optional = false; - } - $var[$name] = $optional ? 2 : 1; - } - } - - if (0 === strpos($val, '[:')) { - // 可选参数 - $optional = true; - $val = substr($val, 1, -1); - } - if (0 === strpos($val, ':')) { - // URL变量 - $name = substr($val, 1); - $var[$name] = $optional ? 2 : 1; - } - } - return $var; + !App::$debug && Cache::set('think_route_map', $item); + return $item; } // 清空路由别名缓存 public static function clearAliasCache() { - Cache::rm('think_route_alias'); + Cache::rm('think_route_map'); + } + + // 指定当前生成URL地址的root + public static function root($root) + { + self::$root = $root; + Request::instance()->root($root); } } diff --git a/library/think/Validate.php b/library/think/Validate.php index bac3ccaf..f12e6c4f 100644 --- a/library/think/Validate.php +++ b/library/think/Validate.php @@ -11,12 +11,13 @@ namespace think; -use think\Input; +use think\Exception; +use think\Request; class Validate { // 实例 - protected static $instance = null; + protected static $instance; // 自定义的验证类型 protected static $type = []; @@ -111,8 +112,8 @@ class Validate /** * 实例化验证 * @access public - * @param array $rules 验证规则 - * @param array $message 验证提示信息 + * @param array $rules 验证规则 + * @param array $message 验证提示信息 * @return Validate */ public static function make($rules = [], $message = []) @@ -126,8 +127,8 @@ class Validate /** * 添加字段验证规则 * @access protected - * @param string|array $name 字段名称或者规则数组 - * @param mixed $rule 验证规则 + * @param string|array $name 字段名称或者规则数组 + * @param mixed $rule 验证规则 * @return Validate */ public function rule($name, $rule = '') @@ -143,8 +144,8 @@ class Validate /** * 注册验证(类型)规则 * @access public - * @param string $type 验证规则类型 - * @param mixed $callback callback方法(或闭包) + * @param string $type 验证规则类型 + * @param mixed $callback callback方法(或闭包) * @return void */ public static function extend($type, $callback = null) @@ -159,8 +160,8 @@ class Validate /** * 获取验证规则的默认提示信息 * @access protected - * @param string|array $type 验证规则类型名称或者数组 - * @param string $msg 验证提示信息 + * @param string|array $type 验证规则类型名称或者数组 + * @param string $msg 验证提示信息 * @return void */ public static function setTypeMsg($type, $msg = null) @@ -175,8 +176,8 @@ class Validate /** * 设置提示信息 * @access public - * @param string|array $name 字段名称 - * @param string $message 提示信息 + * @param string|array $name 字段名称 + * @param string $message 提示信息 * @return Validate */ public function message($name, $message = '') @@ -192,8 +193,8 @@ class Validate /** * 设置验证场景 * @access public - * @param string|array $name 场景名或者场景设置数组 - * @param mixed $fields 要验证的字段 + * @param string|array $name 场景名或者场景设置数组 + * @param mixed $fields 要验证的字段 * @return Validate */ public function scene($name, $fields = null) @@ -225,12 +226,12 @@ class Validate /** * 数据自动验证 * @access public - * @param array $data 数据 - * @param mixed $rules 验证规则 - * @param string $scene 验证场景 + * @param array $data 数据 + * @param mixed $rules 验证规则 + * @param string $scene 验证场景 * @return bool */ - public function check(&$data, $rules = [], $scene = '') + public function check($data, $rules = [], $scene = '') { $this->error = []; @@ -241,6 +242,19 @@ class Validate // 分析验证规则 $scene = $this->getScene($scene); + if (is_array($scene)) { + // 处理场景验证字段 + $change = []; + $array = []; + foreach ($scene as $k => $val) { + if (is_numeric($k)) { + $array[] = $val; + } else { + $array[] = $k; + $change[$k] = $val; + } + } + } foreach ($rules as $key => $item) { // field => rule1|rule2... field=>['rule1','rule2',...] @@ -266,10 +280,15 @@ class Validate // 场景检测 if (!empty($scene)) { - if ($scene instanceof \Closure && !call_user_func_array($scene, [$key, &$data])) { - continue; - } elseif (is_array($scene) && !in_array($key, $scene)) { + if ($scene instanceof \Closure && !call_user_func_array($scene, [$key, $data])) { continue; + } elseif (is_array($scene)) { + if (!in_array($key, $array)) { + continue; + } elseif (isset($change[$key])) { + // 重载某个验证规则 + $rule = $change[$key]; + } } } @@ -300,19 +319,19 @@ class Validate /** * 验证单个字段规则 * @access protected - * @param string $field 字段名 - * @param mixed $value 字段值 - * @param mixed $rules 验证规则 - * @param array $data 数据 - * @param string $title 字段描述 - * @param array $msg 提示信息 + * @param string $field 字段名 + * @param mixed $value 字段值 + * @param mixed $rules 验证规则 + * @param array $data 数据 + * @param string $title 字段描述 + * @param array $msg 提示信息 * @return mixed */ - protected function checkItem($field, $value, $rules, &$data, $title = '', $msg = []) + protected function checkItem($field, $value, $rules, $data, $title = '', $msg = []) { if ($rules instanceof \Closure) { // 匿名函数验证 支持传入当前字段和所有字段两个数据 - $result = call_user_func_array($rules, [$value, &$data]); + $result = call_user_func_array($rules, [$value, $data]); } else { // 支持多规则验证 require|in:a,b,c|... 或者 ['require','in'=>'a,b,c',...] if (is_string($rules)) { @@ -321,7 +340,7 @@ class Validate $i = 0; foreach ($rules as $key => $rule) { if ($rule instanceof \Closure) { - $result = call_user_func_array($rule, [$value, &$data]); + $result = call_user_func_array($rule, [$value, $data]); } else { // 判断验证类型 if (is_numeric($key) && strpos($rule, ':')) { @@ -339,11 +358,11 @@ class Validate } // 如果不是require 有数据才会行验证 - if (0 === strpos($info, 'require') || !empty($value)) { + if (0 === strpos($info, 'require') || (!is_null($value) && '' !== $value)) { // 验证类型 $callback = isset(self::$type[$type]) ? self::$type[$type] : [$this, $type]; // 验证数据 - $result = call_user_func_array($callback, [$value, $rule, &$data, $field]); + $result = call_user_func_array($callback, [$value, $rule, $data, $field]); } else { $result = true; } @@ -370,9 +389,9 @@ class Validate /** * 验证表单令牌(需要配置令牌生成行为) * @access protected - * @param mixed $value 字段值 - * @param mixed $rule 验证规则 - * @param array $data 数据 + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 + * @param array $data 数据 * @return bool */ protected function token($value, $rule, $data) @@ -397,9 +416,9 @@ class Validate /** * 验证是否和某个字段的值一致 * @access protected - * @param mixed $value 字段值 - * @param mixed $rule 验证规则 - * @param array $data 数据 + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 + * @param array $data 数据 * @return bool */ protected function confirm($value, $rule, $data) @@ -410,8 +429,8 @@ class Validate /** * 验证是否大于等于某个值 * @access protected - * @param mixed $value 字段值 - * @param mixed $rule 验证规则 + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 * @return bool */ protected function egt($value, $rule) @@ -422,8 +441,8 @@ class Validate /** * 验证是否大于某个值 * @access protected - * @param mixed $value 字段值 - * @param mixed $rule 验证规则 + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 * @return bool */ protected function gt($value, $rule) @@ -434,8 +453,8 @@ class Validate /** * 验证是否小于等于某个值 * @access protected - * @param mixed $value 字段值 - * @param mixed $rule 验证规则 + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 * @return bool */ protected function elt($value, $rule) @@ -446,8 +465,8 @@ class Validate /** * 验证是否小于某个值 * @access protected - * @param mixed $value 字段值 - * @param mixed $rule 验证规则 + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 * @return bool */ protected function lt($value, $rule) @@ -458,8 +477,8 @@ class Validate /** * 验证是否等于某个值 * @access protected - * @param mixed $value 字段值 - * @param mixed $rule 验证规则 + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 * @return bool */ protected function eq($value, $rule) @@ -470,8 +489,8 @@ class Validate /** * 验证字段值是否为有效格式 * @access protected - * @param mixed $value 字段值 - * @param string $rule 验证规则 + * @param mixed $value 字段值 + * @param string $rule 验证规则 * @return bool */ protected function is($value, $rule) @@ -551,7 +570,7 @@ class Validate $result = is_array($value); break; case 'file': - $file = Input::file($value); + $file = Request::instance()->file($value); $result = !empty($file); break; default: @@ -569,8 +588,8 @@ class Validate /** * 验证是否为合格的域名或者IP 支持A,MX,NS,SOA,PTR,CNAME,AAAA,A6, SRV,NAPTR,TXT 或者 ANY类型 * @access protected - * @param mixed $value 字段值 - * @param mixed $rule 验证规则 + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 * @return bool */ protected function activeUrl($value, $rule) @@ -581,8 +600,8 @@ class Validate /** * 验证是否有效IP * @access protected - * @param mixed $value 字段值 - * @param mixed $rule 验证规则 ipv4 ipv6 + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 ipv4 ipv6 * @return bool */ protected function ip($value, $rule) @@ -596,13 +615,13 @@ class Validate /** * 验证上传文件后缀 * @access protected - * @param mixed $value 字段值 - * @param mixed $rule 验证规则 + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 * @return bool */ protected function fileExt($value, $rule) { - $file = Input::file($value); + $file = Request::instance()->file($value); if (empty($file)) { return false; } @@ -624,13 +643,13 @@ class Validate /** * 验证上传文件类型 * @access protected - * @param mixed $value 字段值 - * @param mixed $rule 验证规则 + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 * @return bool */ protected function fileMime($value, $rule) { - $file = Input::file($value); + $file = Request::instance()->file($value); if (empty($file)) { return false; } @@ -652,13 +671,13 @@ class Validate /** * 验证上传文件大小 * @access protected - * @param mixed $value 字段值 - * @param mixed $rule 验证规则 + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 * @return bool */ protected function fileSize($value, $rule) { - $file = Input::file($value); + $file = Request::instance()->file($value); if (empty($file)) { return false; } @@ -680,20 +699,21 @@ class Validate /** * 验证请求类型 * @access protected - * @param mixed $value 字段值 - * @param mixed $rule 验证规则 + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 * @return bool */ protected function method($value, $rule) { - return REQUEST_METHOD == strtoupper($rule); + $method = Request::instance()->method(); + return strtoupper($rule) == $method; } /** * 验证时间和日期是否符合指定格式 * @access protected - * @param mixed $value 字段值 - * @param mixed $rule 验证规则 + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 * @return bool */ protected function dateFormat($value, $rule) @@ -705,10 +725,10 @@ class Validate /** * 验证是否唯一 * @access protected - * @param mixed $value 字段值 - * @param mixed $rule 验证规则 格式:数据表,字段名,排除ID,主键名 - * @param array $data 数据 - * @param string $field 验证字段名 + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 格式:数据表,字段名,排除ID,主键名 + * @param array $data 数据 + * @param string $field 验证字段名 * @return bool */ protected function unique($value, $rule, $data, $field) @@ -716,29 +736,29 @@ class Validate if (is_string($rule)) { $rule = explode(',', $rule); } - $db = Db::name($rule[0]); - $field = isset($rule[1]) ? $rule[1] : $field; + $db = Db::name($rule[0]); + $key = isset($rule[1]) ? $rule[1] : $field; - if (strpos($field, '^')) { + if (strpos($key, '^')) { // 支持多个字段验证 - $fields = explode('^', $field); - foreach ($fields as $field) { - $map[$field] = $data[$field]; + $fields = explode('^', $key); + foreach ($fields as $key) { + $map[$key] = $data[$key]; } - } elseif (strpos($field, '=')) { - parse_str($field, $map); + } elseif (strpos($key, '=')) { + parse_str($key, $map); } else { - $map[$field] = $data[$field]; + $map[$key] = $data[$field]; } - $key = strval(isset($rule[3]) ? $rule[3] : $db->getPk()); + $pk = strval(isset($rule[3]) ? $rule[3] : $db->getPk()); if (isset($rule[2])) { - $map[$key] = ['neq', $rule[2]]; - } elseif (isset($data[$key])) { - $map[$key] = ['neq', $data[$key]]; + $map[$pk] = ['neq', $rule[2]]; + } elseif (isset($data[$pk])) { + $map[$pk] = ['neq', $data[$pk]]; } - if ($db->where($map)->field($key)->find()) { + if ($db->where($map)->field($pk)->find()) { return false; } return true; @@ -747,9 +767,9 @@ class Validate /** * 使用行为类验证 * @access protected - * @param mixed $value 字段值 - * @param mixed $rule 验证规则 - * @param array $data 数据 + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 + * @param array $data 数据 * @return mixed */ protected function behavior($value, $rule, $data) @@ -760,8 +780,8 @@ class Validate /** * 使用filter_var方式验证 * @access protected - * @param mixed $value 字段值 - * @param mixed $rule 验证规则 + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 * @return bool */ protected function filter($value, $rule) @@ -779,9 +799,9 @@ class Validate /** * 验证某个字段等于某个值的时候必须 * @access protected - * @param mixed $value 字段值 - * @param mixed $rule 验证规则 - * @param array $data 数据 + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 + * @param array $data 数据 * @return bool */ protected function requireIf($value, $rule, $data) @@ -797,9 +817,9 @@ class Validate /** * 通过回调方法验证某个字段是否必须 * @access protected - * @param mixed $value 字段值 - * @param mixed $rule 验证规则 - * @param array $data 数据 + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 + * @param array $data 数据 * @return bool */ protected function requireCallback($value, $rule, $data) @@ -815,9 +835,9 @@ class Validate /** * 验证某个字段有值的情况下必须 * @access protected - * @param mixed $value 字段值 - * @param mixed $rule 验证规则 - * @param array $data 数据 + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 + * @param array $data 数据 * @return bool */ protected function requireWith($value, $rule, $data) @@ -833,8 +853,8 @@ class Validate /** * 验证是否在范围内 * @access protected - * @param mixed $value 字段值 - * @param mixed $rule 验证规则 + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 * @return bool */ protected function in($value, $rule) @@ -845,8 +865,8 @@ class Validate /** * 验证是否不在某个范围 * @access protected - * @param mixed $value 字段值 - * @param mixed $rule 验证规则 + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 * @return bool */ protected function notIn($value, $rule) @@ -857,8 +877,8 @@ class Validate /** * between验证数据 * @access protected - * @param mixed $value 字段值 - * @param mixed $rule 验证规则 + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 * @return bool */ protected function between($value, $rule) @@ -873,8 +893,8 @@ class Validate /** * 使用notbetween验证数据 * @access protected - * @param mixed $value 字段值 - * @param mixed $rule 验证规则 + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 * @return bool */ protected function notBetween($value, $rule) @@ -889,8 +909,8 @@ class Validate /** * 验证数据长度 * @access protected - * @param mixed $value 字段值 - * @param mixed $rule 验证规则 + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 * @return bool */ protected function length($value, $rule) @@ -909,8 +929,8 @@ class Validate /** * 验证数据最大长度 * @access protected - * @param mixed $value 字段值 - * @param mixed $rule 验证规则 + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 * @return bool */ protected function max($value, $rule) @@ -922,8 +942,8 @@ class Validate /** * 验证数据最小长度 * @access protected - * @param mixed $value 字段值 - * @param mixed $rule 验证规则 + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 * @return bool */ protected function min($value, $rule) @@ -935,8 +955,8 @@ class Validate /** * 验证日期 * @access protected - * @param mixed $value 字段值 - * @param mixed $rule 验证规则 + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 * @return bool */ protected function after($value, $rule) @@ -947,8 +967,8 @@ class Validate /** * 验证日期 * @access protected - * @param mixed $value 字段值 - * @param mixed $rule 验证规则 + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 * @return bool */ protected function before($value, $rule) @@ -959,8 +979,8 @@ class Validate /** * 验证有效期 * @access protected - * @param mixed $value 字段值 - * @param mixed $rule 验证规则 + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 * @return bool */ protected function expire($value, $rule) @@ -976,14 +996,14 @@ class Validate if (!is_numeric($end)) { $end = strtotime($end); } - return NOW_TIME >= $start && NOW_TIME <= $end; + return $_SERVER['REQUEST_TIME'] >= $start && $_SERVER['REQUEST_TIME'] <= $end; } /** * 验证IP许可 * @access protected - * @param string $value 字段值 - * @param mixed $rule 验证规则 + * @param string $value 字段值 + * @param mixed $rule 验证规则 * @return mixed */ protected function allowIp($value, $rule) @@ -994,8 +1014,8 @@ class Validate /** * 验证IP禁用 * @access protected - * @param string $value 字段值 - * @param mixed $rule 验证规则 + * @param string $value 字段值 + * @param mixed $rule 验证规则 * @return mixed */ protected function denyIp($value, $rule) @@ -1006,8 +1026,8 @@ class Validate /** * 使用正则验证数据 * @access protected - * @param mixed $value 字段值 - * @param mixed $rule 验证规则 正则规则或者预定义正则名 + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 正则规则或者预定义正则名 * @return mixed */ protected function regex($value, $rule) @@ -1031,8 +1051,8 @@ class Validate /** * 获取数据值 * @access protected - * @param array $data 数据 - * @param string $key 数据标识 支持二维 + * @param array $data 数据 + * @param string $key 数据标识 支持二维 * @return mixed */ protected function getDataValue($data, $key) @@ -1050,10 +1070,10 @@ class Validate /** * 获取验证规则的错误提示信息 * @access protected - * @param string $attribute 字段英文名 - * @param string $title 字段描述名 - * @param string $type 验证规则名称 - * @param mixed $rule 验证规则数据 + * @param string $attribute 字段英文名 + * @param string $title 字段描述名 + * @param string $type 验证规则名称 + * @param mixed $rule 验证规则数据 * @return string */ protected function getRuleMsg($attribute, $title, $type, $rule) @@ -1107,4 +1127,14 @@ class Validate } return $scene; } + + public static function __callStatic($method, $params) + { + $class = new static; + if (method_exists($class, $method)) { + return call_user_func_array([$class, $method], $params); + } else { + throw new \BadMethodCallException('method not exists:' . __CLASS__ . '->' . $method); + } + } } diff --git a/library/think/View.php b/library/think/View.php index 31ccd22d..65d9f72b 100644 --- a/library/think/View.php +++ b/library/think/View.php @@ -83,7 +83,7 @@ class View $type = !empty($options['type']) ? $options['type'] : 'Think'; } - $class = (!empty($options['namespace']) ? $options['namespace'] : '\\think\\view\\driver\\') . ucfirst($type); + $class = false !== strpos($type, '\\') ? $type : '\\think\\view\\driver\\' . ucfirst($type); if (isset($options['type'])) { unset($options['type']); } @@ -93,14 +93,15 @@ class View /** * 解析和获取模板内容 用于输出 - * @param string $template 模板文件名或者内容 - * @param array $vars 模板输出变量 - * @param array $config 模板参数 - * @param bool $renderContent 是否渲染内容 + * @param string $template 模板文件名或者内容 + * @param array $vars 模板输出变量 + * @param array $replace 替换内容 + * @param array $config 模板参数 + * @param bool $renderContent 是否渲染内容 * @return string * @throws Exception */ - public function fetch($template = '', $vars = [], $config = [], $renderContent = false) + public function fetch($template = '', $vars = [], $replace = [], $config = [], $renderContent = false) { // 模板变量 $vars = array_merge($this->data, $vars); @@ -116,10 +117,11 @@ class View // 获取并清空缓存 $content = ob_get_clean(); // 内容过滤标签 - APP_HOOK && Hook::listen('view_filter', $content); + Hook::listen('view_filter', $content); // 允许用户自定义模板的字符串替换 - if (!empty($this->replace)) { - $content = strtr($content, $this->replace); + $replace = array_merge($this->replace, $replace); + if (!empty($replace)) { + $content = strtr($content, $replace); } return $content; } @@ -127,8 +129,8 @@ class View /** * 视图内容替换 * @access public - * @param string|array $content 被替换内容(支持批量替换) - * @param string $replace 替换内容 + * @param string|array $content 被替换内容(支持批量替换) + * @param string $replace 替换内容 * @return $this */ public function replace($content, $replace = '') @@ -146,19 +148,20 @@ class View * @access public * @param string $content 内容 * @param array $vars 模板输出变量 + * @param array $replace 替换内容 * @param array $config 模板参数 * @return mixed */ - public function display($content, $vars = [], $config = []) + public function display($content, $vars = [], $replace = [], $config = []) { - return $this->fetch($content, $vars, $config, true); + return $this->fetch($content, $vars, $replace, $config, true); } /** * 模板变量赋值 * @access public - * @param string $name 变量名 - * @param mixed $value 变量值 + * @param string $name 变量名 + * @param mixed $value 变量值 */ public function __set($name, $value) { diff --git a/library/think/cache/driver/Apc.php b/library/think/cache/driver/Apc.php index ebe163cc..6da8f7cb 100644 --- a/library/think/cache/driver/Apc.php +++ b/library/think/cache/driver/Apc.php @@ -23,7 +23,6 @@ class Apc protected $options = [ 'expire' => 0, 'prefix' => '', - 'length' => 0, ]; /***************************** 需要支持apc_cli模式 @@ -37,7 +36,7 @@ class Apc public function __construct($options = []) { if (!function_exists('apc_cache_info')) { - throw new Exception('_NOT_SUPPERT_:Apc'); + throw new \BadFunctionCallException('not support: Apc'); } if (!empty($options)) { $this->options = array_merge($this->options, $options); @@ -58,9 +57,9 @@ class Apc /** * 写入缓存 * @access public - * @param string $name 缓存变量名 - * @param mixed $value 存储数据 - * @param integer $expire 有效时间(秒) + * @param string $name 缓存变量名 + * @param mixed $value 存储数据 + * @param integer $expire 有效时间(秒) * @return bool */ public function set($name, $value, $expire = null) diff --git a/library/think/cache/driver/File.php b/library/think/cache/driver/File.php index 2c0ddff1..29cd3773 100644 --- a/library/think/cache/driver/File.php +++ b/library/think/cache/driver/File.php @@ -25,22 +25,21 @@ class File 'cache_subdir' => false, 'path_level' => 1, 'prefix' => '', - 'length' => 0, 'path' => CACHE_PATH, 'data_compress' => false, ]; /** * 架构函数 - * @access public + * @param array $options */ public function __construct($options = []) { if (!empty($options)) { $this->options = array_merge($this->options, $options); } - if (substr($this->options['path'], -1) != '/') { - $this->options['path'] .= '/'; + if (substr($this->options['path'], -1) != DS) { + $this->options['path'] .= DS; } $this->init(); } @@ -54,10 +53,11 @@ class File { // 创建项目缓存目录 if (!is_dir($this->options['path'])) { - if (!mkdir($this->options['path'], 0755, true)) { - return false; + if (mkdir($this->options['path'], 0755, true)) { + return true; } } + return false; } /** @@ -74,7 +74,7 @@ class File $dir = ''; $len = $this->options['path_level']; for ($i = 0; $i < $len; $i++) { - $dir .= $name{$i} . '/'; + $dir .= $name{$i} . DS; } if (!is_dir($this->options['path'] . $dir)) { mkdir($this->options['path'] . $dir, 0755, true); @@ -101,7 +101,7 @@ class File $content = file_get_contents($filename); if (false !== $content) { $expire = (int) substr($content, 8, 12); - if (0 != $expire && time() > filemtime($filename) + $expire) { + if (0 != $expire && $_SERVER['REQUEST_TIME'] > filemtime($filename) + $expire) { //缓存过期删除缓存文件 $this->unlink($filename); return false; @@ -121,9 +121,9 @@ class File /** * 写入缓存 * @access public - * @param string $name 缓存变量名 - * @param mixed $value 存储数据 - * @param int $expire 有效时间 0为永久 + * @param string $name 缓存变量名 + * @param mixed $value 存储数据 + * @param int $expire 有效时间 0为永久 * @return boolean */ public function set($name, $value, $expire = null) diff --git a/library/think/cache/driver/Lite.php b/library/think/cache/driver/Lite.php index c2095368..0a3a838b 100644 --- a/library/think/cache/driver/Lite.php +++ b/library/think/cache/driver/Lite.php @@ -36,8 +36,8 @@ class Lite if (!empty($options)) { $this->options = array_merge($this->options, $options); } - if (substr($this->options['path'], -1) != '/') { - $this->options['path'] .= '/'; + if (substr($this->options['path'], -1) != DS) { + $this->options['path'] .= DS; } } @@ -65,7 +65,7 @@ class Lite if (is_file($filename)) { // 判断是否过期 $mtime = filemtime($filename); - if ($mtime < time()) { + if ($mtime < $_SERVER['REQUEST_TIME']) { // 清除已经过期的文件 unlink($filename); return false; @@ -79,9 +79,9 @@ class Lite /** * 写入缓存 * @access public - * @param string $name 缓存变量名 - * @param mixed $value 存储数据 - * @internal param int $expire 有效时间 0为永久 + * @param string $name 缓存变量名 + * @param mixed $value 存储数据 + * @param int $expire 有效时间 0为永久 * @return bool */ public function set($name, $value, $expire = null) @@ -97,7 +97,7 @@ class Lite $ret = file_put_contents($filename, (" 0, 'timeout' => 0, // 超时时间(单位:毫秒) 'persistent' => true, - 'length' => 0, 'prefix' => '', ]; @@ -31,12 +30,12 @@ class Memcache * 架构函数 * @param array $options 缓存参数 * @access public - * @throws Exception + * @throws \BadFunctionCallException */ public function __construct($options = []) { if (!extension_loaded('memcache')) { - throw new Exception('_NOT_SUPPERT_:memcache'); + throw new \BadFunctionCallException('not support: memcache'); } if (!empty($options)) { $this->options = array_merge($this->options, $options); @@ -71,9 +70,9 @@ class Memcache /** * 写入缓存 * @access public - * @param string $name 缓存变量名 - * @param mixed $value 存储数据 - * @param integer $expire 有效时间(秒) + * @param string $name 缓存变量名 + * @param mixed $value 存储数据 + * @param integer $expire 有效时间(秒) * @return bool */ public function set($name, $value, $expire = null) diff --git a/library/think/cache/driver/Memcached.php b/library/think/cache/driver/Memcached.php index f82500f5..50082a1e 100644 --- a/library/think/cache/driver/Memcached.php +++ b/library/think/cache/driver/Memcached.php @@ -12,18 +12,18 @@ namespace think\cache\driver; use think\Cache; -use think\Exception; class Memcached { - protected $handler = null; + protected $handler; protected $options = [ - 'host' => '127.0.0.1', - 'port' => 11211, - 'expire' => 0, - 'timeout' => 0, // 超时时间(单位:毫秒) - 'length' => 0, - 'prefix' => '', + 'host' => '127.0.0.1', + 'port' => 11211, + 'expire' => 0, + 'timeout' => 0, // 超时时间(单位:毫秒) + 'prefix' => '', + 'username' => '', //账号 + 'password' => '', //密码 ]; /** @@ -34,7 +34,7 @@ class Memcached public function __construct($options = []) { if (!extension_loaded('memcached')) { - throw new Exception('_NOT_SUPPERT_:memcached'); + throw new \BadFunctionCallException('not support: memcached'); } if (!empty($options)) { $this->options = array_merge($this->options, $options); @@ -56,6 +56,10 @@ class Memcached $servers[] = [$host, (isset($ports[$i]) ? $ports[$i] : $ports[0]), 1]; } $this->handler->addServers($servers); + if ('' != $this->options['username']) { + $this->handler->setOption(\Memcached::OPT_BINARY_PROTOCOL, true); + $this->handler->setSaslAuthData($this->options['username'], $this->options['password']); + } } /** @@ -72,9 +76,9 @@ class Memcached /** * 写入缓存 * @access public - * @param string $name 缓存变量名 - * @param mixed $value 存储数据 - * @param integer $expire 有效时间(秒) + * @param string $name 缓存变量名 + * @param mixed $value 存储数据 + * @param integer $expire 有效时间(秒) * @return bool */ public function set($name, $value, $expire = null) @@ -83,7 +87,7 @@ class Memcached $expire = $this->options['expire']; } $name = $this->options['prefix'] . $name; - $expire = 0 == $expire ? 0 : time() + $expire; + $expire = 0 == $expire ? 0 : $_SERVER['REQUEST_TIME'] + $expire; if ($this->handler->set($name, $value, $expire)) { return true; } diff --git a/library/think/cache/driver/Redis.php b/library/think/cache/driver/Redis.php index 23455a1d..25febbf0 100644 --- a/library/think/cache/driver/Redis.php +++ b/library/think/cache/driver/Redis.php @@ -17,7 +17,7 @@ use think\Exception; /** * Redis缓存驱动,适合单机部署、有前端代理实现高可用的场景,性能最好 * 有需要在业务层实现读写分离、或者使用RedisCluster的需求,请使用Redisd驱动 - * + * * 要求安装phpredis扩展:https://github.com/nicolasff/phpredis * @author 尘缘 <130775@qq.com> */ @@ -31,7 +31,6 @@ class Redis 'timeout' => 0, 'expire' => 0, 'persistent' => false, - 'length' => 0, 'prefix' => '', ]; @@ -43,7 +42,7 @@ class Redis public function __construct($options = []) { if (!extension_loaded('redis')) { - throw new Exception('_NOT_SUPPERT_:redis'); + throw new \BadFunctionCallException('not support: redis'); } if (!empty($options)) { $this->options = array_merge($this->options, $options); @@ -74,9 +73,9 @@ class Redis /** * 写入缓存 * @access public - * @param string $name 缓存变量名 - * @param mixed $value 存储数据 - * @param integer $expire 有效时间(秒) + * @param string $name 缓存变量名 + * @param mixed $value 存储数据 + * @param integer $expire 有效时间(秒) * @return boolean */ public function set($name, $value, $expire = null) diff --git a/library/think/cache/driver/Redisd.php b/library/think/cache/driver/Redisd.php index 67058975..e3bc80e7 100644 --- a/library/think/cache/driver/Redisd.php +++ b/library/think/cache/driver/Redisd.php @@ -11,6 +11,7 @@ namespace think\cache\driver; +use think\App; use think\Cache; use think\Exception; use think\Log; @@ -18,21 +19,21 @@ use think\Log; /** 配置参数: 'cache' => [ - 'type' => 'Redisd' - 'host' => 'A:6379,B:6379', //redis服务器ip,多台用逗号隔开;读写分离开启时,默认写A,当A主挂时,再尝试写B - 'slave' => 'B:6379,C:6379', //redis服务器ip,多台用逗号隔开;读写分离开启时,所有IP随机读,其中一台挂时,尝试读其它节点,可以配置权重 - 'port' => 6379, //默认的端口号 - 'password' => '', //AUTH认证密码,当redis服务直接暴露在外网时推荐 - 'timeout' => 10, //连接超时时间 - 'expire' => false, //默认过期时间,默认为永不过期 - 'prefix' => '', //缓存前缀,不宜过长 - 'persistent' => false, //是否长连接 false=短连接,推荐长连接 +'type' => 'Redisd' +'host' => 'A:6379,B:6379', //redis服务器ip,多台用逗号隔开;读写分离开启时,默认写A,当A主挂时,再尝试写B +'slave' => 'B:6379,C:6379', //redis服务器ip,多台用逗号隔开;读写分离开启时,所有IP随机读,其中一台挂时,尝试读其它节点,可以配置权重 +'port' => 6379, //默认的端口号 +'password' => '', //AUTH认证密码,当redis服务直接暴露在外网时推荐 +'timeout' => 10, //连接超时时间 +'expire' => false, //默认过期时间,默认为永不过期 +'prefix' => '', //缓存前缀,不宜过长 +'persistent' => false, //是否长连接 false=短连接,推荐长连接 ], 单例获取: - $redis = \think\Cache::connect(Config::get('cache')); - $redis->master(true)->setnx('key'); - $redis->master(false)->get('key'); +$redis = \think\Cache::connect(Config::get('cache')); +$redis->master(true)->setnx('key'); +$redis->master(false)->get('key'); */ /** @@ -70,7 +71,6 @@ class Redisd 'timeout' => 10, 'expire' => false, 'persistent' => false, - 'length' => 0, 'prefix' => '', 'serialize' => \Redis::SERIALIZER_PHP, ]; @@ -84,7 +84,7 @@ class Redisd public function __construct($options = []) { if (!extension_loaded('redis')) { - throw new Exception('_NOT_SUPPERT_:redis'); + throw new \BadFunctionCallException('not support: redis'); } $this->options = $options = array_merge($this->options, $options); @@ -136,7 +136,7 @@ class Redisd //发生错误则摘掉当前节点 try { $result = $this->handler->$func($host, $port, $this->options['timeout']); - if($result === false) { + if (false === $result) { $this->handler->getLastError(); } @@ -145,11 +145,11 @@ class Redisd } $this->handler->setOption(\Redis::OPT_SERIALIZER, $this->options['serialize']); - if(strlen($this->options['prefix'])) { + if (strlen($this->options['prefix'])) { $this->handler->setOption(\Redis::OPT_PREFIX, $this->options['prefix']); } - APP_DEBUG && Log::record("[ CACHE ] INIT Redisd : {$host}:{$port} master->" . var_export($master, true), Log::ALERT); + App::$debug && Log::record("[ CACHE ] INIT Redisd : {$host}:{$port} master->" . var_export($master, true), Log::ALERT); } catch (\RedisException $e) { //phpredis throws a RedisException object if it can't reach the Redis server. //That can happen in case of connectivity issues, if the Redis service is down, or if the redis host is overloaded. @@ -288,7 +288,7 @@ class Redisd $this->master(true); return $this->handler->flushDB(); } - + /** * 返回句柄对象,可执行其它高级方法 * 需要先执行 $redis->master() 连接到 DB diff --git a/library/think/cache/driver/Sae.php b/library/think/cache/driver/Sae.php deleted file mode 100644 index 4bead286..00000000 --- a/library/think/cache/driver/Sae.php +++ /dev/null @@ -1,123 +0,0 @@ - -// +---------------------------------------------------------------------- - -namespace think\cache\driver; - -use think\Cache; -use think\Exception; - -/** - * SAE Memcache缓存驱动 - * @author liu21st - */ -class Sae -{ - protected $handler = null; - protected $options = [ - 'host' => '127.0.0.1', - 'port' => 11211, - 'expire' => 0, - 'timeout' => false, - 'persistent' => false, - 'length' => 0, - 'prefix' => '', - ]; - - /** - * 架构函数 - * @param array $options 缓存参数 - * @access public - */ - public function __construct($options = []) - { - if (!function_exists('memcache_init')) { - throw new Exception('请在SAE平台上运行代码。'); - } - $this->handler = memcache_init(); - if (!$this->handler) { - throw new Exception('您未开通Memcache服务,请在SAE管理平台初始化Memcache服务'); - } - if (!empty($options)) { - $this->options = array_merge($this->options, $options); - } - } - - /** - * 读取缓存 - * @access public - * @param string $name 缓存变量名 - * @return mixed - */ - public function get($name) - { - return $this->handler->get($_SERVER['HTTP_APPVERSION'] . '/' . $this->options['prefix'] . $name); - } - - /** - * 写入缓存 - * @access public - * @param string $name 缓存变量名 - * @param mixed $value 存储数据 - * @param integer $expire 有效时间(秒) - * @return bool - */ - public function set($name, $value, $expire = null) - { - if (is_null($expire)) { - $expire = $this->options['expire']; - } - $name = $this->options['prefix'] . $name; - if ($this->handler->set($_SERVER['HTTP_APPVERSION'] . '/' . $name, $value, 0, $expire)) { - return true; - } - return false; - } - - /** - * 删除缓存 - * @param string $name 缓存变量名 - * @param bool|false $ttl - * @return bool - */ - public function rm($name, $ttl = false) - { - $name = $_SERVER['HTTP_APPVERSION'] . '/' . $this->options['prefix'] . $name; - return false === $ttl ? - $this->handler->delete($name) : - $this->handler->delete($name, $ttl); - } - - /** - * 清除缓存 - * @access public - * @return bool - */ - public function clear() - { - return $this->handler->flush(); - } - - /** - * 获得SaeKv对象 - */ - private function getKv() - { - static $kv; - if (!$kv) { - $kv = new \SaeKV(); - if (!$kv->init()) { - throw new Exception('您没有初始化KVDB,请在SAE管理平台初始化KVDB服务'); - } - } - return $kv; - } - -} diff --git a/library/think/cache/driver/Secache.php b/library/think/cache/driver/Secache.php index 13613bfe..e7c0ac1c 100644 --- a/library/think/cache/driver/Secache.php +++ b/library/think/cache/driver/Secache.php @@ -25,7 +25,6 @@ class Secache 'path' => '', 'expire' => 0, 'prefix' => '', - 'length' => 0, ]; /** @@ -63,9 +62,9 @@ class Secache /** * 写入缓存 * @access public - * @param string $name 缓存变量名 - * @param mixed $value 存储数据 - * @param integer $expire 有效时间(秒) + * @param string $name 缓存变量名 + * @param mixed $value 存储数据 + * @param integer $expire 有效时间(秒) * @return boolean */ public function set($name, $value) diff --git a/library/think/cache/driver/Sqlite.php b/library/think/cache/driver/Sqlite.php index 89a24e96..e4689d61 100644 --- a/library/think/cache/driver/Sqlite.php +++ b/library/think/cache/driver/Sqlite.php @@ -18,7 +18,7 @@ use think\Exception; * Sqlite缓存驱动 * @author liu21st */ -class Sqlite implements CacheInterface +class Sqlite { protected $options = [ @@ -26,20 +26,19 @@ class Sqlite implements CacheInterface 'table' => 'sharedmemory', 'prefix' => '', 'expire' => 0, - 'length' => 0, 'persistent' => false, ]; /** * 架构函数 * @param array $options 缓存参数 - * @throws Exception + * @throws \BadFunctionCallException * @access public */ public function __construct($options = []) { if (!extension_loaded('sqlite')) { - throw new Exception('_NOT_SUPPERT_:sqlite'); + throw new \BadFunctionCallException('not support: sqlite'); } if (!empty($options)) { $this->options = array_merge($this->options, $options); @@ -57,7 +56,7 @@ class Sqlite implements CacheInterface public function get($name) { $name = $this->options['prefix'] . sqlite_escape_string($name); - $sql = 'SELECT value FROM ' . $this->options['table'] . ' WHERE var=\'' . $name . '\' AND (expire=0 OR expire >' . time() . ') LIMIT 1'; + $sql = 'SELECT value FROM ' . $this->options['table'] . ' WHERE var=\'' . $name . '\' AND (expire=0 OR expire >' . $_SERVER['REQUEST_TIME'] . ') LIMIT 1'; $result = sqlite_query($this->handler, $sql); if (sqlite_num_rows($result)) { $content = sqlite_fetch_single($result); @@ -73,9 +72,9 @@ class Sqlite implements CacheInterface /** * 写入缓存 * @access public - * @param string $name 缓存变量名 - * @param mixed $value 存储数据 - * @param integer $expire 有效时间(秒) + * @param string $name 缓存变量名 + * @param mixed $value 存储数据 + * @param integer $expire 有效时间(秒) * @return boolean */ public function set($name, $value, $expire = null) @@ -85,7 +84,7 @@ class Sqlite implements CacheInterface if (is_null($expire)) { $expire = $this->options['expire']; } - $expire = (0 == $expire) ? 0 : (time() + $expire); //缓存有效期为0表示永久缓存 + $expire = (0 == $expire) ? 0 : ($_SERVER['REQUEST_TIME'] + $expire); //缓存有效期为0表示永久缓存 if (function_exists('gzcompress')) { //数据压缩 $value = gzcompress($value, 3); diff --git a/library/think/cache/driver/Test.php b/library/think/cache/driver/Test.php index 95ea31e1..45cfbf7f 100644 --- a/library/think/cache/driver/Test.php +++ b/library/think/cache/driver/Test.php @@ -34,9 +34,9 @@ class Test /** * 写入缓存 * @access public - * @param string $name 缓存变量名 - * @param mixed $value 存储数据 - * @param int $expire 有效时间 0为永久 + * @param string $name 缓存变量名 + * @param mixed $value 存储数据 + * @param int $expire 有效时间 0为永久 * @return boolean */ public function set($name, $value, $expire = null) diff --git a/library/think/cache/driver/Wincache.php b/library/think/cache/driver/Wincache.php index e77b53a2..bc1b35de 100644 --- a/library/think/cache/driver/Wincache.php +++ b/library/think/cache/driver/Wincache.php @@ -23,7 +23,6 @@ class Wincache protected $options = [ 'prefix' => '', 'expire' => 0, - 'length' => 0, ]; /** @@ -35,7 +34,7 @@ class Wincache public function __construct($options = []) { if (!function_exists('wincache_ucache_info')) { - throw new Exception('_NOT_SUPPERT_:WinCache'); + throw new \BadFunctionCallException('not support: WinCache'); } if (!empty($options)) { $this->options = array_merge($this->options, $options); @@ -57,9 +56,9 @@ class Wincache /** * 写入缓存 * @access public - * @param string $name 缓存变量名 - * @param mixed $value 存储数据 - * @param integer $expire 有效时间(秒) + * @param string $name 缓存变量名 + * @param mixed $value 存储数据 + * @param integer $expire 有效时间(秒) * @return boolean */ public function set($name, $value, $expire = null) diff --git a/library/think/cache/driver/Xcache.php b/library/think/cache/driver/Xcache.php index a529bed3..bb6f277e 100644 --- a/library/think/cache/driver/Xcache.php +++ b/library/think/cache/driver/Xcache.php @@ -23,18 +23,18 @@ class Xcache protected $options = [ 'prefix' => '', 'expire' => 0, - 'length' => 0, ]; /** * 架构函数 * @param array $options 缓存参数 * @access public + * @throws \BadFunctionCallException */ public function __construct($options = []) { if (!function_exists('xcache_info')) { - throw new Exception('_NOT_SUPPERT_:Xcache'); + throw new \BadFunctionCallException('not support: Xcache'); } if (!empty($options)) { $this->options = array_merge($this->options, $options); @@ -59,9 +59,9 @@ class Xcache /** * 写入缓存 * @access public - * @param string $name 缓存变量名 - * @param mixed $value 存储数据 - * @param integer $expire 有效时间(秒) + * @param string $name 缓存变量名 + * @param mixed $value 存储数据 + * @param integer $expire 有效时间(秒) * @return boolean */ public function set($name, $value, $expire = null) diff --git a/library/think/exception/NotFoundException.php b/library/think/config/driver/Json.php similarity index 63% rename from library/think/exception/NotFoundException.php rename to library/think/config/driver/Json.php index 06ae2e5d..ec2419f9 100644 --- a/library/think/exception/NotFoundException.php +++ b/library/think/config/driver/Json.php @@ -6,22 +6,19 @@ // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- -// | Author: 麦当苗儿 +// | Author: liu21st // +---------------------------------------------------------------------- -namespace think\exception; +namespace think\config\driver; -use think\Exception; - -/** - * Database相关异常处理类 - */ -class NotFoundException extends Exception +class Json { - /** - * 系统异常后发送给客户端的HTTP Status - * @var integer - */ - protected $httpStatus = 404; - + public function parse($config) + { + if (is_file($config)) { + $config = file_get_contents($config); + } + $result = json_decode($config, true); + return $result; + } } diff --git a/library/think/console/command/Make.php b/library/think/console/command/Make.php new file mode 100644 index 00000000..2829ee3c --- /dev/null +++ b/library/think/console/command/Make.php @@ -0,0 +1,120 @@ + +// +---------------------------------------------------------------------- + +namespace think\console\command; + +use think\Config; +use think\console\Input; +use think\console\input\Argument; +use think\console\Output; + +abstract class Make extends Command +{ + /** @var Input */ + protected $input; + + /** @var Output */ + protected $output; + + protected $type; + + abstract protected function getStub(); + + protected function configure() + { + $this->addArgument('name', Argument::REQUIRED, "The name of the class"); + } + + public function run(Input $input, Output $output) + { + $this->input = $input; + $this->output = $output; + return parent::run($input, $output); + } + + protected function execute(Input $input, Output $output) + { + + $name = trim($input->getArgument('name')); + + $classname = $this->getClassName($name); + + $pathname = $this->getPathName($classname); + + if (is_file($pathname)) { + $output->writeln('' . $this->type . ' already exists!'); + return false; + } + + if (!is_dir(dirname($pathname))) { + mkdir(strtolower(dirname($pathname)), 0755, true); + } + + file_put_contents($pathname, $this->buildClass($classname)); + + $output->writeln('' . $this->type . ' created successfully.'); + + } + + protected function buildClass($name) + { + $stub = file_get_contents($this->getStub()); + + $namespace = trim(implode('\\', array_slice(explode('\\', $name), 0, -1)), '\\'); + + $class = str_replace($namespace . '\\', '', $name); + + return str_replace(['{%className%}', '{%namespace%}', '{%app_namespace%}'], [ + $class, + $namespace, + Config::get('app_namespace') + ], $stub); + + } + + protected function getPathName($name) + { + $name = str_replace(Config::get('app_namespace') . '\\', '', $name); + + return APP_PATH . str_replace('\\', '/', $name) . '.php'; + } + + protected function getClassName($name) + { + $appNamespace = Config::get('app_namespace'); + + if (strpos($name, $appNamespace . '\\') === 0) { + return $name; + } + + if (Config::get('app_multi_module')) { + if (strpos($name, '/')) { + list($module, $name) = explode('/', $name, 2); + } else { + $module = 'common'; + } + } else { + $module = null; + } + + if (strpos($name, '/') !== false) { + $name = str_replace('/', '\\', $name); + } + + return $this->getNamespace($appNamespace, $module) . '\\' . $name; + } + + protected function getNamespace($appNamespace, $module) + { + return $module ? ($appNamespace . '\\' . $module) : $appNamespace; + } + +} diff --git a/library/think/console/command/make/Controller.php b/library/think/console/command/make/Controller.php index f5014fec..afa7be90 100644 --- a/library/think/console/command/make/Controller.php +++ b/library/think/console/command/make/Controller.php @@ -2,22 +2,49 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2015 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2016 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- -// | Author: yunwuxin <448901948@qq.com> +// | Author: 刘志淳 // +---------------------------------------------------------------------- namespace think\console\command\make; -use think\console\command\Command; +use think\Config; +use think\console\command\Make; +use think\console\input\Option; -class Controller extends Command +class Controller extends Make { - public function __construct() + protected $type = "Controller"; + + protected function configure() { - parent::__construct("make:controller"); + parent::configure(); + $this->setName('make:controller') + ->addOption('plain', null, Option::VALUE_NONE, 'Generate an empty controller class.') + ->setDescription('Create a new resource controller class'); } -} \ No newline at end of file + + protected function getStub() + { + if ($this->input->getOption('plain')) { + return __DIR__ . '/stubs/controller.plain.stub'; + } + + return __DIR__ . '/stubs/controller.stub'; + } + + protected function getClassName($name) + { + return parent::getClassName($name) . (Config::get('controller_suffix') ? ucfirst(Config::get('url_controller_layer')) : ''); + } + + protected function getNamespace($appNamespace, $module) + { + return parent::getNamespace($appNamespace, $module) . '\controller'; + } + +} diff --git a/library/think/console/command/make/Model.php b/library/think/console/command/make/Model.php index ef36c18f..d4e9b5dd 100644 --- a/library/think/console/command/make/Model.php +++ b/library/think/console/command/make/Model.php @@ -2,25 +2,35 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2015 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2016 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- -// | Author: yunwuxin <448901948@qq.com> +// | Author: 刘志淳 // +---------------------------------------------------------------------- namespace think\console\command\make; +use think\console\command\Make; -use think\console\command\Command; - - -class Model extends Command +class Model extends Make { + protected $type = "Model"; - public function __construct() + protected function configure() { - parent::__construct("make:model"); + parent::configure(); + $this->setName('make:model') + ->setDescription('Create a new model class'); } -} \ No newline at end of file + protected function getStub() + { + return __DIR__ . '/stubs/model.stub'; + } + + protected function getNamespace($appNamespace, $module) + { + return parent::getNamespace($appNamespace, $module) . '\model'; + } +} diff --git a/library/think/console/command/make/stubs/controller.plain.stub b/library/think/console/command/make/stubs/controller.plain.stub new file mode 100644 index 00000000..b7539dcf --- /dev/null +++ b/library/think/console/command/make/stubs/controller.plain.stub @@ -0,0 +1,10 @@ + +// +---------------------------------------------------------------------- +namespace think\console\command\optimize; + +use think\App; +use think\console\command\Command; +use think\console\Input; +use think\console\Output; + +class Autoload extends Command +{ + /** @var Output */ + protected $output; + + protected function configure() + { + $this->setName('optimize:autoload') + ->setDescription('Optimizes PSR0 and PSR4 packages to be loaded with classmaps too, good for production.'); + } + + protected function execute(Input $input, Output $output) + { + $this->output = $output; + + $classmapFile = << realpath(rtrim(APP_PATH)), + 'think\\' => LIB_PATH . 'think', + 'behavior\\' => LIB_PATH . 'behavior', + 'traits\\' => LIB_PATH . 'traits', + ]; + + krsort($namespacesToScan); + $classMap = []; + foreach ($namespacesToScan as $namespace => $dir) { + + if (!is_dir($dir)) { + continue; + } + + $namespaceFilter = $namespace === '' ? null : $namespace; + $classMap = $this->addClassMapCode($dir, $namespaceFilter, $classMap); + } + + ksort($classMap); + foreach ($classMap as $class => $code) { + $classmapFile .= ' ' . var_export($class, true) . ' => ' . $code; + } + $classmapFile .= "];\n"; + + if (!is_dir(RUNTIME_PATH)) { + @mkdir(RUNTIME_PATH, 0755, true); + } + + file_put_contents(RUNTIME_PATH . 'classmap' . EXT, $classmapFile); + + $output->writeln('Succeed!'); + } + + protected function addClassMapCode($dir, $namespace, $classMap) + { + foreach ($this->createMap($dir, $namespace) as $class => $path) { + + $pathCode = $this->getPathCode($path) . ",\n"; + + if (!isset($classMap[$class])) { + $classMap[$class] = $pathCode; + } elseif ($classMap[$class] !== $pathCode && !preg_match('{/(test|fixture|example|stub)s?/}i', strtr($classMap[$class] . ' ' . $path, '\\', '/'))) { + $this->output->writeln( + 'Warning: Ambiguous class resolution, "' . $class . '"' . + ' was found in both "' . str_replace(["',\n"], [ + '' + ], $classMap[$class]) . '" and "' . $path . '", the first will be used.' + ); + } + } + return $classMap; + } + + protected function getPathCode($path) + { + + $baseDir = ''; + $appPath = $this->normalizePath(realpath(APP_PATH)); + $libPath = $this->normalizePath(realpath(LIB_PATH)); + $path = $this->normalizePath($path); + + if (strpos($path, $libPath . '/') === 0) { + $path = substr($path, strlen(LIB_PATH)); + $baseDir = 'LIB_PATH'; + } elseif (strpos($path, $appPath . '/') === 0) { + $path = substr($path, strlen($appPath) + 1); + $baseDir = 'APP_PATH'; + } + + if ($path !== false) { + $baseDir .= " . "; + } + + return $baseDir . (($path !== false) ? var_export($path, true) : ""); + } + + + protected function normalizePath($path) + { + $parts = []; + $path = strtr($path, '\\', '/'); + $prefix = ''; + $absolute = false; + + if (preg_match('{^([0-9a-z]+:(?://(?:[a-z]:)?)?)}i', $path, $match)) { + $prefix = $match[1]; + $path = substr($path, strlen($prefix)); + } + + if (substr($path, 0, 1) === '/') { + $absolute = true; + $path = substr($path, 1); + } + + $up = false; + foreach (explode('/', $path) as $chunk) { + if ('..' === $chunk && ($absolute || $up)) { + array_pop($parts); + $up = !(empty($parts) || '..' === end($parts)); + } elseif ('.' !== $chunk && '' !== $chunk) { + $parts[] = $chunk; + $up = '..' !== $chunk; + } + } + + return $prefix . ($absolute ? '/' : '') . implode('/', $parts); + } + + protected function createMap($path, $namespace = null) + { + if (is_string($path)) { + if (is_file($path)) { + $path = [new \SplFileInfo($path)]; + } elseif (is_dir($path)) { + + $objects = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($path), \RecursiveIteratorIterator::SELF_FIRST); + + $path = []; + + /** @var \SplFileInfo $object */ + foreach ($objects as $object) { + if ($object->isFile() && $object->getExtension() == 'php') { + $path[] = $object; + } + } + } else { + throw new \RuntimeException( + 'Could not scan for classes inside "' . $path . + '" which does not appear to be a file nor a folder' + ); + } + } + + $map = []; + + /** @var \SplFileInfo $file */ + foreach ($path as $file) { + $filePath = $file->getRealPath(); + + if (pathinfo($filePath, PATHINFO_EXTENSION) != 'php') { + continue; + } + + $classes = $this->findClasses($filePath); + + foreach ($classes as $class) { + if (null !== $namespace && 0 !== strpos($class, $namespace)) { + continue; + } + + if (!isset($map[$class])) { + $map[$class] = $filePath; + } elseif ($map[$class] !== $filePath && !preg_match('{/(test|fixture|example|stub)s?/}i', strtr($map[$class] . ' ' . $filePath, '\\', '/'))) { + $this->output->writeln( + 'Warning: Ambiguous class resolution, "' . $class . '"' . + ' was found in both "' . $map[$class] . '" and "' . $filePath . '", the first will be used.' + ); + } + } + } + + return $map; + } + + + protected function findClasses($path) + { + $extraTypes = '|trait'; + + $contents = @php_strip_whitespace($path); + if (!$contents) { + if (!file_exists($path)) { + $message = 'File at "%s" does not exist, check your classmap definitions'; + } elseif (!is_readable($path)) { + $message = 'File at "%s" is not readable, check its permissions'; + } elseif ('' === trim(file_get_contents($path))) { + return []; + } else { + $message = 'File at "%s" could not be parsed as PHP, it may be binary or corrupted'; + } + $error = error_get_last(); + if (isset($error['message'])) { + $message .= PHP_EOL . 'The following message may be helpful:' . PHP_EOL . $error['message']; + } + throw new \RuntimeException(sprintf($message, $path)); + } + + if (!preg_match('{\b(?:class|interface' . $extraTypes . ')\s}i', $contents)) { + return []; + } + + // strip heredocs/nowdocs + $contents = preg_replace('{<<<\s*(\'?)(\w+)\\1(?:\r\n|\n|\r)(?:.*?)(?:\r\n|\n|\r)\\2(?=\r\n|\n|\r|;)}s', 'null', $contents); + // strip strings + $contents = preg_replace('{"[^"\\\\]*+(\\\\.[^"\\\\]*+)*+"|\'[^\'\\\\]*+(\\\\.[^\'\\\\]*+)*+\'}s', 'null', $contents); + // strip leading non-php code if needed + if (substr($contents, 0, 2) !== '.+<\?}s', '?>'); + if (false !== $pos && false === strpos(substr($contents, $pos), '])(?Pclass|interface' . $extraTypes . ') \s++ (?P[a-zA-Z_\x7f-\xff:][a-zA-Z0-9_\x7f-\xff:\-]*+) + | \b(?])(?Pnamespace) (?P\s++[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*+(?:\s*+\\\\\s*+[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*+)*+)? \s*+ [\{;] + ) + }ix', $contents, $matches); + + $classes = []; + $namespace = ''; + + for ($i = 0, $len = count($matches['type']); $i < $len; $i++) { + if (!empty($matches['ns'][$i])) { + $namespace = str_replace([' ', "\t", "\r", "\n"], '', $matches['nsname'][$i]) . '\\'; + } else { + $name = $matches['name'][$i]; + if ($name[0] === ':') { + $name = 'xhp' . substr(str_replace(['-', ':'], ['_', '__'], $name), 1); + } elseif ($matches['type'][$i] === 'enum') { + $name = rtrim($name, ':'); + } + $classes[] = ltrim($namespace . $name, '\\'); + } + } + + return $classes; + } + +} \ No newline at end of file diff --git a/library/think/console/helper/Helper.php b/library/think/console/helper/Helper.php index 66be1891..7b327bdd 100644 --- a/library/think/console/helper/Helper.php +++ b/library/think/console/helper/Helper.php @@ -118,4 +118,4 @@ abstract class Helper return self::strlen($string); } -} \ No newline at end of file +} diff --git a/library/think/console/helper/Process.php b/library/think/console/helper/Process.php index 5aaeeff4..4252bfe3 100644 --- a/library/think/console/helper/Process.php +++ b/library/think/console/helper/Process.php @@ -12,8 +12,8 @@ namespace think\console\helper; use think\console\Output; +use think\Process as ThinkProcess; use think\process\Builder as ProcessBuilder; -use think\process as ThinkProcess; use think\process\exception\Failed as ProcessFailedException; class Process extends Helper @@ -115,4 +115,4 @@ class Process extends Helper { return 'process'; } -} \ No newline at end of file +} diff --git a/library/think/console/helper/Question.php b/library/think/console/helper/Question.php index 78416fbd..e1820834 100644 --- a/library/think/console/helper/Question.php +++ b/library/think/console/helper/Question.php @@ -11,16 +11,16 @@ namespace think\console\helper; +use think\console\helper\question\Choice as ChoiceQuestion; +use think\console\helper\question\Question as OutputQuestion; use think\console\Input; use think\console\Output; -use think\console\helper\question\Question as OutputQuestion; -use think\console\helper\question\Choice as ChoiceQuestion; use think\console\output\formatter\Style as OutputFormatterStyle; class Question extends Helper { - private $inputStream; + private $inputStream; private static $shell; private static $stty; @@ -138,7 +138,7 @@ class Question extends Helper if ($question instanceof ChoiceQuestion) { $width = max(array_map('strlen', array_keys($question->getChoices()))); - $messages = (array)$question->getQuestion(); + $messages = (array) $question->getQuestion(); foreach ($question->getChoices() as $key => $value) { $messages[] = sprintf(" [%-${width}s] %s", $key, $value); } @@ -199,7 +199,7 @@ class Question extends Helper $output->write("\033[1D"); } - if ($i === 0) { + if (0 === $i) { $ofs = -1; $matches = $autocomplete; $numMatches = count($matches); @@ -249,7 +249,7 @@ class Question extends Helper $ofs = 0; foreach ($autocomplete as $value) { - if (0 === strpos($value, $ret) && $i !== strlen($value)) { + if (0 === strpos($value, $ret) && strlen($value) !== $i) { $matches[$numMatches++] = $value; } } @@ -314,7 +314,7 @@ class Question extends Helper } if (false !== $shell = $this->getShell()) { - $readCmd = $shell === 'csh' ? 'set mypassword = $<' : 'read -r mypassword'; + $readCmd = 'csh' === $shell ? 'set mypassword = $<' : 'read -r mypassword'; $command = sprintf("/usr/bin/env %s -c 'stty -echo; %s; stty echo; echo \$mypassword'", $shell, $readCmd); $value = rtrim(shell_exec($command)); $output->writeln(''); @@ -389,6 +389,6 @@ class Question extends Helper exec('stty 2>&1', $output, $exitcode); - return self::$stty = $exitcode === 0; + return self::$stty = 0 === $exitcode; } -} \ No newline at end of file +} diff --git a/library/think/console/output/Formatter.php b/library/think/console/output/Formatter.php index 4179680e..d3ffc659 100644 --- a/library/think/console/output/Formatter.php +++ b/library/think/console/output/Formatter.php @@ -39,6 +39,8 @@ class Formatter $this->setStyle('info', new Style('green')); $this->setStyle('comment', new Style('yellow')); $this->setStyle('question', new Style('black', 'cyan')); + $this->setStyle('highlight', new Style('red')); + $this->setStyle('warning', new Style('black', 'yellow')); $this->styleStack = new StyleStack(); } diff --git a/library/think/console/output/Stream.php b/library/think/console/output/Stream.php index b97b582e..fec94bdf 100644 --- a/library/think/console/output/Stream.php +++ b/library/think/console/output/Stream.php @@ -32,6 +32,8 @@ class Stream /** * 构造方法 + * @param $stream + * @param Formatter $formatter */ public function __construct($stream, Formatter $formatter = null) { @@ -180,8 +182,12 @@ class Stream */ protected function hasColorSupport() { - if (DIRECTORY_SEPARATOR == '\\') { - return false !== getenv('ANSICON') || 'ON' === getenv('ConEmuANSI'); + if (DIRECTORY_SEPARATOR === '\\') { + return + 0 >= version_compare('10.0.10586', PHP_WINDOWS_VERSION_MAJOR . '.' . PHP_WINDOWS_VERSION_MINOR . '.' . PHP_WINDOWS_VERSION_BUILD) + || false !== getenv('ANSICON') + || 'ON' === getenv('ConEmuANSI') + || 'xterm' === getenv('TERM'); } return function_exists('posix_isatty') && @posix_isatty($this->stream); diff --git a/library/think/controller/Hprose.php b/library/think/controller/Hprose.php index d6ab39d2..73993296 100644 --- a/library/think/controller/Hprose.php +++ b/library/think/controller/Hprose.php @@ -11,6 +11,8 @@ namespace think\controller; +use think\App; +use think\Loader; /** * ThinkPHP Hprose控制器类 */ @@ -35,17 +37,17 @@ abstract class Hprose } //导入类库 - \think\Loader::import('vendor.Hprose.HproseHttpServer'); + Loader::import('vendor.Hprose.HproseHttpServer'); //实例化HproseHttpServer $server = new \HproseHttpServer(); if ($this->allowMethodList) { $methods = $this->allowMethodList; } else { $methods = get_class_methods($this); - $methods = array_diff($methods, array('__construct', '__call', '_initialize')); + $methods = array_diff($methods, ['__construct', '__call', '_initialize']); } $server->addMethods($methods, $this); - if (APP_DEBUG || $this->debug) { + if (App::$debug || $this->debug) { $server->setDebugEnabled(true); } // Hprose设置 diff --git a/library/think/controller/Jsonrpc.php b/library/think/controller/Jsonrpc.php index 0e000d49..9236f549 100644 --- a/library/think/controller/Jsonrpc.php +++ b/library/think/controller/Jsonrpc.php @@ -11,6 +11,7 @@ namespace think\controller; +use think\Loader; /** * ThinkPHP JsonRPC控制器类 */ @@ -29,7 +30,7 @@ abstract class Jsonrpc } //导入类库 - \think\Loader::import('vendor.jsonrpc.jsonRPCServer'); + Loader::import('vendor.jsonrpc.jsonRPCServer'); // 启动server \jsonRPCServer::handle($this); } diff --git a/library/think/controller/Rest.php b/library/think/controller/Rest.php index 33372e1b..a58743b3 100644 --- a/library/think/controller/Rest.php +++ b/library/think/controller/Rest.php @@ -12,12 +12,13 @@ namespace think\controller; use think\Response; +use think\Request; abstract class Rest { - protected $_method = ''; // 当前请求类型 - protected $_type = ''; // 当前资源类型 + protected $method; // 当前请求类型 + protected $type; // 当前资源类型 // 输出类型 protected $restMethodList = 'get|post|put|delete'; protected $restDefaultMethod = 'get'; @@ -36,105 +37,63 @@ abstract class Rest public function __construct() { // 资源类型检测 - if ('' == __EXT__) { + $request = Request::instance(); + $ext = $request->ext(); + if ('' == $ext) { // 自动检测资源类型 - $this->_type = $this->getAcceptType(); - } elseif (!preg_match('/\(' . $this->restTypeList . '\)$/i', __EXT__)) { + $this->type = $request->type(); + } elseif (!preg_match('/\(' . $this->restTypeList . '\)$/i', $ext)) { // 资源类型非法 则用默认资源类型访问 - $this->_type = $this->restDefaultType; + $this->type = $this->restDefaultType; } else { - $this->_type = __EXT__; + $this->type = $ext; } // 请求方式检测 - $method = strtolower($_SERVER['REQUEST_METHOD']); + $method = strtolower($request->method()); if (false === stripos($this->restMethodList, $method)) { // 请求方式非法 则用默认请求方法 $method = $this->restDefaultMethod; } - $this->_method = $method; + $this->method = $method; } /** * REST 调用 * @access public - * * @param string $method 方法名 * @param array $args 参数 - * * @return mixed - * @throws \think\Exception + * @throws \Exception */ public function _empty($method, $args) { - if (method_exists($this, $method . '_' . $this->_method . '_' . $this->_type)) { + if (method_exists($this, $method . '_' . $this->method . '_' . $this->type)) { // RESTFul方法支持 - $fun = $method . '_' . $this->_method . '_' . $this->_type; - } elseif ($this->_method == $this->restDefaultMethod && method_exists($this, $method . '_' . $this->_type)) { - $fun = $method . '_' . $this->_type; - } elseif ($this->_type == $this->restDefaultType && method_exists($this, $method . '_' . $this->_method)) { - $fun = $method . '_' . $this->_method; + $fun = $method . '_' . $this->method . '_' . $this->type; + } elseif ($this->method == $this->restDefaultMethod && method_exists($this, $method . '_' . $this->type)) { + $fun = $method . '_' . $this->type; + } elseif ($this->type == $this->restDefaultType && method_exists($this, $method . '_' . $this->method)) { + $fun = $method . '_' . $this->method; } if (isset($fun)) { return $this->$fun(); } else { // 抛出异常 - throw new \Exception('error action :' . ACTION_NAME); + throw new \Exception('error action :' . $method); } } /** * 输出返回数据 * @access protected - * @param mixed $data 要返回的数据 - * @param String $type 返回类型 JSON XML - * @param integer $code HTTP状态 - * @return void + * @param mixed $data 要返回的数据 + * @param String $type 返回类型 JSON XML + * @param integer $code HTTP状态码 + * @return Response */ - protected function response($data, $type = '', $code = 200) + protected function response($data, $type = 'json', $code = 200) { - http_response_code($code); - Response::data($data); - if ($type) { - Response::type($type); - } + return Response::create($data, $type)->code($code); } - /** - * 获取当前请求的Accept头信息 - * @return string - */ - public static function getAcceptType() - { - if (!isset($_SERVER['HTTP_ACCEPT'])) { - return false; - } - - $type = [ - 'html' => 'text/html,application/xhtml+xml,*/*', - 'xml' => 'application/xml,text/xml,application/x-xml', - 'json' => 'application/json,text/x-json,application/jsonrequest,text/json', - 'js' => 'text/javascript,application/javascript,application/x-javascript', - 'css' => 'text/css', - 'rss' => 'application/rss+xml', - 'yaml' => 'application/x-yaml,text/yaml', - 'atom' => 'application/atom+xml', - 'pdf' => 'application/pdf', - 'text' => 'text/plain', - 'png' => 'image/png', - 'jpg' => 'image/jpg,image/jpeg,image/pjpeg', - 'gif' => 'image/gif', - 'csv' => 'text/csv', - ]; - - foreach ($type as $key => $val) { - $array = explode(',', $val); - foreach ($array as $k => $v) { - if (stristr($_SERVER['HTTP_ACCEPT'], $v)) { - return $key; - } - } - } - - return false; - } } diff --git a/library/think/controller/Rpc.php b/library/think/controller/Rpc.php index 2a014c16..e034588d 100644 --- a/library/think/controller/Rpc.php +++ b/library/think/controller/Rpc.php @@ -11,6 +11,8 @@ namespace think\controller; +use think\App; +use think\Loader; /** * ThinkPHP RPC控制器类 */ @@ -32,18 +34,18 @@ abstract class Rpc } //导入类库 - \think\Loader::import('vendor.phprpc.phprpc_server'); + Loader::import('vendor.phprpc.phprpc_server'); //实例化phprpc $server = new \PHPRPC_Server(); if ($this->allowMethodList) { $methods = $this->allowMethodList; } else { $methods = get_class_methods($this); - $methods = array_diff($methods, array('__construct', '__call', '_initialize')); + $methods = array_diff($methods, ['__construct', '__call', '_initialize']); } $server->add($methods, $this); - if (APP_DEBUG || $this->debug) { + if (App::$debug || $this->debug) { $server->setDebugMode(true); } $server->setEnableGZIP(true); diff --git a/library/think/controller/Yar.php b/library/think/controller/Yar.php index 0a876326..fcf5ced1 100644 --- a/library/think/controller/Yar.php +++ b/library/think/controller/Yar.php @@ -30,7 +30,7 @@ abstract class Yar //判断扩展是否存在 if (!extension_loaded('yar')) { - throw new Exception('not support yar'); + throw new \Exception('not support yar'); } //实例化Yar_Server diff --git a/library/think/db/Builder.php b/library/think/db/Builder.php index 23a5c8f4..3f9f989a 100644 --- a/library/think/db/Builder.php +++ b/library/think/db/Builder.php @@ -13,6 +13,8 @@ namespace think\db; use PDO; use think\Db; +use think\db\Connection; +use think\db\Query; use think\Exception; abstract class Builder @@ -26,7 +28,7 @@ abstract class Builder protected $options = []; // 数据库表达式 - protected $exp = ['eq' => '=', 'neq' => '<>', 'gt' => '>', 'egt' => '>=', 'lt' => '<', 'elt' => '<=', 'notlike' => 'NOT LIKE', 'like' => 'LIKE', 'in' => 'IN', 'exp' => 'EXP', 'notin' => 'NOT IN', 'not in' => 'NOT IN', 'between' => 'BETWEEN', 'not between' => 'NOT BETWEEN', 'notbetween' => 'NOT BETWEEN', 'exists' => 'EXISTS', 'notexists' => 'NOT EXISTS', 'not exists' => 'NOT EXISTS', 'null' => 'NULL', 'notnull' => 'NOT NULL', 'not null' => 'NOT NULL']; + protected $exp = ['eq' => '=', 'neq' => '<>', 'gt' => '>', 'egt' => '>=', 'lt' => '<', 'elt' => '<=', 'notlike' => 'NOT LIKE', 'like' => 'LIKE', 'in' => 'IN', 'exp' => 'EXP', 'notin' => 'NOT IN', 'not in' => 'NOT IN', 'between' => 'BETWEEN', 'not between' => 'NOT BETWEEN', 'notbetween' => 'NOT BETWEEN', 'exists' => 'EXISTS', 'notexists' => 'NOT EXISTS', 'not exists' => 'NOT EXISTS', 'null' => 'NULL', 'notnull' => 'NOT NULL', 'not null' => 'NOT NULL', '> time' => '> TIME', '< time' => '< TIME', '>= time' => '>= TIME', '<= time' => '<= TIME', 'between time' => 'BETWEEN TIME', 'not between time' => 'NOT BETWEEN TIME', 'notbetween time' => 'NOT BETWEEN TIME']; // SQL表达式 protected $selectSql = 'SELECT%DISTINCT% %FIELD% FROM %TABLE%%FORCE%%JOIN%%WHERE%%GROUP%%HAVING%%ORDER%%LIMIT% %UNION%%LOCK%%COMMENT%'; @@ -38,9 +40,9 @@ abstract class Builder /** * 架构函数 * @access public - * @param \think\db\Connection $connection 数据库连接对象实例 + * @param Connection $connection 数据库连接对象实例 */ - public function __construct($connection) + public function __construct(Connection $connection) { $this->connection = $connection; } @@ -48,10 +50,10 @@ abstract class Builder /** * 设置当前的Query对象实例 * @access protected - * @param \think\db\Query $query 当前查询对象实例 + * @param Query $query 当前查询对象实例 * @return void */ - public function setQuery($query) + public function setQuery(Query $query) { $this->query = $query; } @@ -64,14 +66,14 @@ abstract class Builder */ protected function parseSqlTable($sql) { - return $this->connection->parseSqlTable($sql); + return $this->query->parseSqlTable($sql); } /** * 数据分析 * @access protected - * @param array $data 数据 - * @param array $options 查询参数 + * @param array $data 数据 + * @param array $options 查询参数 * @return array */ protected function parseData($data, $options) @@ -85,14 +87,14 @@ abstract class Builder if ('*' == $options['field']) { $fields = array_keys($bind); } else { - $fields = is_array($options['field']) ? $options['field'] : explode(',', $options['field']); + $fields = $options['field']; } $result = []; foreach ($data as $key => $val) { if (!in_array($key, $fields, true)) { if ($options['strict']) { - throw new Exception(' fields not exists :[' . $key . ']'); + throw new Exception('fields not exists:[' . $key . ']'); } } else { $item = $this->parseKey($key); @@ -128,15 +130,14 @@ abstract class Builder /** * value分析 * @access protected - * @param mixed $value + * @param mixed $value + * @param string $field * @return string|array */ - protected function parseValue($value) + protected function parseValue($value, $field = '') { if (is_string($value)) { $value = strpos($value, ':') === 0 && $this->query->isBind(substr($value, 1)) ? $value : $this->connection->quote($value); - } elseif (is_array($value) && is_string($value[0]) && strtolower($value[0]) == 'exp') { - $value = $value[1]; } elseif (is_array($value)) { $value = array_map([$this, 'parseValue'], $value); } elseif (is_bool($value)) { @@ -198,35 +199,37 @@ abstract class Builder /** * where分析 * @access protected - * @param mixed $where + * @param mixed $where 查询条件 + * @param array $options 查询参数 * @return string */ - protected function parseWhere($where, $table) + protected function parseWhere($where, $options) { - $whereStr = $this->buildWhere($where, $table); + $whereStr = $this->buildWhere($where, $options); return empty($whereStr) ? '' : ' WHERE ' . $whereStr; } /** * 生成查询条件SQL * @access public - * @param mixed $where + * @param mixed $where + * @param array $options * @return string */ - public function buildWhere($where, $table) + public function buildWhere($where, $options) { if (empty($where)) { $where = []; } if ($where instanceof Query) { - return $this->buildWhere($where->getOptions('where'), $table); + return $this->buildWhere($where->getOptions('where'), $options); } $whereStr = ''; // 获取字段信息 - $fields = $this->query->getTableInfo($table, 'fields'); - $binds = $this->query->getTableInfo($table, 'bind'); + $fields = $this->query->getTableInfo($options['table'], 'fields'); + $binds = $this->query->getTableInfo($options['table'], 'bind'); foreach ($where as $key => $val) { $str = []; foreach ($val as $field => $value) { @@ -238,14 +241,14 @@ abstract class Builder // 使用闭包查询 $query = new Query($this->connection); call_user_func_array($value, [ & $query]); - $str[] = ' ' . $key . ' ( ' . $this->buildWhere($query->getOptions('where'), $table) . ' )'; + $str[] = ' ' . $key . ' ( ' . $this->buildWhere($query->getOptions('where'), $options) . ' )'; } else { if (strpos($field, '|')) { // 不同字段使用相同查询条件(OR) $array = explode('|', $field); $item = []; foreach ($array as $k) { - $item[] = $this->parseWhereItem($k, $value); + $item[] = $this->parseWhereItem($k, $value, '', $options); } $str[] = ' ' . $key . ' ( ' . implode(' OR ', $item) . ' )'; } elseif (strpos($field, '&')) { @@ -253,28 +256,27 @@ abstract class Builder $array = explode('&', $field); $item = []; foreach ($array as $k) { - $item[] = $this->parseWhereItem($k, $value); + $item[] = $this->parseWhereItem($k, $value, '', $options); } $str[] = ' ' . $key . ' ( ' . implode(' AND ', $item) . ' )'; } else { // 对字段使用表达式查询 $field = is_string($field) ? $field : ''; - $str[] = ' ' . $key . ' ' . $this->parseWhereItem($field, $value, $key); + $str[] = ' ' . $key . ' ' . $this->parseWhereItem($field, $value, $key, $options); } } } + $whereStr .= empty($whereStr) ? substr(implode(' ', $str), strlen($key) + 1) : implode(' ', $str); } return $whereStr; } // where子单元分析 - protected function parseWhereItem($key, $val, $rule = '') + protected function parseWhereItem($field, $val, $rule = '', $options = []) { - if ($key) { - // 字段分析 - $key = $this->parseKey($key); - } + // 字段分析 + $key = $field ? $this->parseKey($field) : ''; // 查询规则和条件 if (!is_array($val)) { @@ -292,7 +294,7 @@ abstract class Builder array_push($val, $item); } foreach ($val as $item) { - $str[] = $this->parseWhereItem($key, $item, $rule); + $str[] = $this->parseWhereItem($key, $item, $rule, $options); } return '( ' . implode(' ' . $rule . ' ', $str) . ' )'; } @@ -310,7 +312,7 @@ abstract class Builder $whereStr = ''; if (in_array($exp, ['=', '<>', '>', '>=', '<', '<=', 'LIKE', 'NOT LIKE'])) { // 比较运算 及 模糊匹配 - $whereStr .= $key . ' ' . $exp . ' ' . $this->parseValue($value); + $whereStr .= $key . ' ' . $exp . ' ' . $this->parseValue($value, $field); } elseif ('EXP' == $exp) { // 表达式查询 $whereStr .= '( ' . $key . ' ' . $value . ' )'; @@ -323,16 +325,27 @@ abstract class Builder $whereStr .= $key . ' ' . $exp . ' ' . $this->parseClosure($value); } else { $value = is_array($value) ? $value : explode(',', $value); - $zone = implode(',', $this->parseValue($value)); + $zone = implode(',', $this->parseValue($value, $field)); $whereStr .= $key . ' ' . $exp . ' (' . $zone . ')'; } } elseif (in_array($exp, ['NOT BETWEEN', 'BETWEEN'])) { // BETWEEN 查询 $data = is_array($value) ? $value : explode(',', $value); - $whereStr .= $key . ' ' . $exp . ' ' . $this->parseValue($data[0]) . ' AND ' . $this->parseValue($data[1]); + $whereStr .= $key . ' ' . $exp . ' ' . $this->parseValue($data[0], $field) . ' AND ' . $this->parseValue($data[1], $field); } elseif (in_array($exp, ['NOT EXISTS', 'EXISTS'])) { // EXISTS 查询 - $whereStr .= $exp . ' ' . $this->parseClosure($value); + if ($value instanceof \Closure) { + $whereStr .= $exp . ' ' . $this->parseClosure($value); + } else { + $whereStr .= $exp . ' (' . $value . ')'; + } + } elseif (in_array($exp, ['< TIME', '> TIME', '<= TIME', '>= TIME'])) { + $whereStr .= $key . ' ' . substr($exp, 0, 2) . ' ' . $this->parseDateTime($value, $field, $options); + } elseif (in_array($exp, ['BETWEEN TIME', 'NOT BETWEEN TIME'])) { + if (is_string($value)) { + $value = explode(',', $value); + } + $whereStr .= $key . ' ' . substr($exp, 0, -4) . $this->parseDateTime($value[0], $field, $options) . ' AND ' . $this->parseDateTime($value[1], $field, $options); } return $whereStr; } @@ -345,6 +358,36 @@ abstract class Builder return $query->buildSql($show); } + /** + * 日期时间条件解析 + * @access protected + * @param string $value + * @param string $key + * @param array $options + * @return string + */ + protected function parseDateTime($value, $key, $options = []) + { + // 获取时间字段类型 + $type = $this->query->getTableInfo('', 'type'); + if (isset($options['field_type'][$key])) { + $info = $options['field_type'][$key]; + } elseif (isset($type[$key])) { + $info = $type[$key]; + } + if (isset($info)) { + $value = strtotime($value) ?: $value; + if (preg_match('/(datetime|timestamp)/is', $info)) { + // 日期及时间戳类型 + $value = date('Y-m-d H:i:s', $value); + } elseif (preg_match('/(date)/is', $info)) { + // 日期及时间戳类型 + $value = date('Y-m-d', $value); + } + } + return is_int($value) ? $value : $this->connection->quote($value); + } + /** * limit分析 * @access protected @@ -510,7 +553,7 @@ abstract class Builder $this->parseDistinct($options['distinct']), $this->parseField($options['field']), $this->parseJoin($options['join']), - $this->parseWhere($options['where'], $options['table']), + $this->parseWhere($options['where'], $options), $this->parseGroup($options['group']), $this->parseHaving($options['having']), $this->parseOrder($options['order']), @@ -526,9 +569,9 @@ abstract class Builder /** * 生成insert SQL * @access public - * @param array $data 数据 - * @param array $options 表达式 - * @param bool $replace 是否replace + * @param array $data 数据 + * @param array $options 表达式 + * @param bool $replace 是否replace * @return string */ public function insert(array $data, $options = [], $replace = false) @@ -557,8 +600,8 @@ abstract class Builder /** * 生成insertall SQL * @access public - * @param array $dataSet 数据集 - * @param array $options 表达式 + * @param array $dataSet 数据集 + * @param array $options 表达式 * @return string */ public function insertAll($dataSet, $options) @@ -567,24 +610,27 @@ abstract class Builder if ('*' == $options['field']) { $fields = $this->query->getTableInfo($options['table'], 'fields'); } else { - $fields = is_array($options['field']) ? $options['field'] : explode(',', $options['field']); + $fields = $options['field']; } foreach ($dataSet as &$data) { foreach ($data as $key => $val) { if (!in_array($key, $fields, true)) { if ($options['strict']) { - throw new Exception(' fields not exists :[' . $key . ']'); + throw new Exception('fields not exists:[' . $key . ']'); } unset($data[$key]); + } elseif (is_scalar($val)) { + $data[$key] = $this->parseValue($val, $key); } else { - $data[$key] = $this->parseValue($val); + // 过滤掉非标量数据 + unset($data[$key]); } } $value = array_values($data); $values[] = 'SELECT ' . implode(',', $value); } - $fields = array_map([$this, 'parseKey'], array_keys($dataSet[0])); + $fields = array_map([$this, 'parseKey'], array_keys(reset($dataSet))); $sql = str_replace( ['%TABLE%', '%FIELD%', '%DATA%', '%COMMENT%'], [ @@ -600,9 +646,9 @@ abstract class Builder /** * 生成slectinsert SQL * @access public - * @param array $fields 数据 - * @param string $table 数据表 - * @param array $options 表达式 + * @param array $fields 数据 + * @param string $table 数据表 + * @param array $options 表达式 * @return string */ public function selectInsert($fields, $table, $options) @@ -612,16 +658,15 @@ abstract class Builder } $fields = array_map([$this, 'parseKey'], $fields); - $sql = 'INSERT INTO ' . $this->parseTable($table) . ' (' . implode(',', $fields) . ') '; - $sql .= $this->buildSelectSql($options); + $sql = 'INSERT INTO ' . $this->parseTable($table) . ' (' . implode(',', $fields) . ') ' . $this->select($options); return $sql; } /** * 生成update SQL * @access public - * @param array $fields 数据 - * @param array $options 表达式 + * @param array $fields 数据 + * @param array $options 表达式 * @return string */ public function update($data, $options) @@ -641,7 +686,7 @@ abstract class Builder $this->parseTable($options['table']), implode(',', $set), $this->parseJoin($options['join']), - $this->parseWhere($options['where'], $options['table']), + $this->parseWhere($options['where'], $options), $this->parseOrder($options['order']), $this->parseLimit($options['limit']), $this->parseLimit($options['lock']), @@ -665,7 +710,7 @@ abstract class Builder $this->parseTable($options['table']), !empty($options['using']) ? ' USING ' . $this->parseTable($options['using']) . ' ' : '', $this->parseJoin($options['join']), - $this->parseWhere($options['where'], $options['table']), + $this->parseWhere($options['where'], $options), $this->parseOrder($options['order']), $this->parseLimit($options['limit']), $this->parseLimit($options['lock']), diff --git a/library/think/db/Connection.php b/library/think/db/Connection.php index 14ce32bc..a45d8d3e 100644 --- a/library/think/db/Connection.php +++ b/library/think/db/Connection.php @@ -13,11 +13,13 @@ namespace think\db; use PDO; use PDOStatement; +use think\App; use think\Collection; use think\Db; +use think\db\exception\BindParamException; +use think\db\Query; use think\Debug; use think\Exception; -use think\exception\DbBindParamException; use think\exception\PDOException; use think\Log; @@ -27,23 +29,14 @@ abstract class Connection /** @var PDOStatement PDO操作实例 */ protected $PDOStatement; - // 当前操作的数据表名 - protected $table = ''; - // 当前操作的数据对象名 - protected $name = ''; - /** @var string 当前SQL指令 */ protected $queryStr = ''; // 最后插入ID protected $lastInsID; // 返回或者影响记录数 protected $numRows = 0; - // 事务的数据库连接 - protected $transPDO; // 事务指令数 protected $transTimes = 0; - // 事务标识 - protected $transLabel = ''; // 错误信息 protected $error = ''; @@ -52,16 +45,19 @@ abstract class Connection /** @var PDO 当前连接ID */ protected $linkID; + protected $linkRead; + protected $linkWrite; // 查询结果类型 - protected $resultSetType = Db::RESULTSET_ARRAY; + protected $resultSetType = 'array'; // 查询结果类型 protected $fetchType = PDO::FETCH_ASSOC; // 字段属性大小写 protected $attrCase = PDO::CASE_LOWER; // 监听回调 protected static $event = []; - + // 查询对象 + protected $query = []; // 数据库连接参数配置 protected $config = [ // 数据库类型 @@ -97,12 +93,16 @@ abstract class Connection // 是否严格检查字段是否存在 'fields_strict' => true, // 数据集返回类型 - 'resultset_type' => Db::RESULTSET_ARRAY, + 'resultset_type' => 'array', + // 自动写入时间戳字段 + 'auto_timestamp' => false, + // 是否需要进行SQL性能分析 + 'sql_explain' => false, ]; // PDO连接参数 protected $params = [ - PDO::ATTR_CASE => PDO::CASE_LOWER, + PDO::ATTR_CASE => PDO::CASE_NATURAL, PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_ORACLE_NULLS => PDO::NULL_NATURAL, PDO::ATTR_STRINGIFY_FETCHES => false, @@ -119,19 +119,35 @@ abstract class Connection if (!empty($config)) { $this->config = array_merge($this->config, $config); } - $this->query = new Query($this); + } + + /** + * 创建指定模型的查询对象 + * @access public + * @param string $model 模型类名称 + * @return Query + */ + public function model($model) + { + if (!isset($this->query[$model])) { + $this->query[$model] = new Query($this, $model); + } + return $this->query[$model]; } /** * 调用Query类的查询方法 * @access public - * @param string $method 方法名称 - * @param array $args 调用参数 + * @param string $method 方法名称 + * @param array $args 调用参数 * @return mixed */ public function __call($method, $args) { - return call_user_func_array([$this->query, $method], $args); + if (!isset($this->query['database'])) { + $this->query['database'] = new Query($this); + } + return call_user_func_array([$this->query['database'], $method], $args); } /** @@ -172,7 +188,7 @@ abstract class Connection * @param array $info 字段信息 * @return array */ - protected function fieldCase($info) + public function fieldCase($info) { // 字段大小写转换 switch ($this->attrCase) { @@ -195,16 +211,16 @@ abstract class Connection * @param string $config 配置名称 * @return mixed */ - public function getConfig($config) + public function getConfig($config = '') { - return $this->config[$config]; + return $config ? $this->config[$config] : $this->config; } /** * 设置数据库的配置参数 * @access public - * @param string $config 配置名称 - * @param mixed $value 配置值 + * @param string $config 配置名称 + * @param mixed $value 配置值 * @return void */ public function setConfig($config, $value) @@ -215,9 +231,9 @@ abstract class Connection /** * 连接数据库方法 * @access public - * @param array $config 连接参数 - * @param integer $linkNum 连接序号 - * @param array|bool $autoConnection 是否自动连接主数据库(用于分布式) + * @param array $config 连接参数 + * @param integer $linkNum 连接序号 + * @param array|bool $autoConnection 是否自动连接主数据库(用于分布式) * @return PDO * @throws Exception */ @@ -245,7 +261,7 @@ abstract class Connection } $this->links[$linkNum] = new PDO($config['dsn'], $config['username'], $config['password'], $params); // 记录数据库连接信息 - APP_DEBUG && Log::record('[ DB ] CONNECT: ' . $config['dsn'], 'info'); + App::$debug && Log::record('[ DB ] CONNECT: ' . $config['dsn'], 'info'); } catch (\PDOException $e) { if ($autoConnection) { Log::record($e->getMessage(), 'error'); @@ -268,7 +284,7 @@ abstract class Connection if ($this->linkID) { return $this->linkID->getAttribute(PDO::ATTR_DRIVER_NAME); } else { - return $this->config['type']; + return basename(str_replace('\\', '/', $this->config['type'])); } } @@ -298,28 +314,23 @@ abstract class Connection /** * 执行查询 返回数据集 * @access public - * @param string $sql sql指令 - * @param array $bind 参数绑定 - * @param boolean $fetch 不执行只是获取SQL - * @param boolean $master 是否在主服务器读操作 - * @param bool|string $class 指定返回的数据集对象 + * @param string $sql sql指令 + * @param array $bind 参数绑定 + * @param boolean $master 是否在主服务器读操作 + * @param bool|string $class 指定返回的数据集对象 * @return mixed - * @throws DbBindParamException + * @throws BindParamException * @throws PDOException */ - public function query($sql, $bind = [], $fetch = false, $master = false, $class = false) + public function query($sql, $bind = [], $master = false, $class = false) { $this->initConnect($master); if (!$this->linkID) { return false; } - // 根据参数绑定组装最终的SQL语句 - $this->queryStr = $this->getBindSql($sql, $bind); + $this->queryStr = $this->getRealSql($sql, $bind); - if ($fetch) { - return $this->queryStr; - } //释放前次的查询结果 if (!empty($this->PDOStatement)) { $this->free(); @@ -337,7 +348,8 @@ abstract class Connection $result = $this->PDOStatement->execute(); // 调试结束 $this->debug(false); - return $this->getResult($class); + $procedure = 0 === strpos(strtolower(substr(trim($sql), 0, 4)), 'call'); + return $this->getResult($class, $procedure); } catch (\PDOException $e) { throw new PDOException($e, $this->config, $this->queryStr); } @@ -346,26 +358,23 @@ abstract class Connection /** * 执行语句 * @access public - * @param string $sql sql指令 - * @param array $bind 参数绑定 - * @param boolean $fetch 不执行只是获取SQL - * @param boolean $getLastInsID 是否获取自增ID + * @param string $sql sql指令 + * @param array $bind 参数绑定 + * @param boolean $getLastInsID 是否获取自增ID + * @param string $sequence 自增序列名 * @return int - * @throws DbBindParamException + * @throws BindParamException * @throws PDOException */ - public function execute($sql, $bind = [], $fetch = false, $getLastInsID = false) + public function execute($sql, $bind = [], $getLastInsID = false, $sequence = null) { $this->initConnect(true); if (!$this->linkID) { return false; } // 根据参数绑定组装最终的SQL语句 - $this->queryStr = $this->getBindSql($sql, $bind); + $this->queryStr = $this->getRealSql($sql, $bind); - if ($fetch) { - return $this->queryStr; - } //释放前次的查询结果 if (!empty($this->PDOStatement)) { $this->free(); @@ -386,7 +395,7 @@ abstract class Connection $this->numRows = $this->PDOStatement->rowCount(); if (preg_match("/^\s*(INSERT\s+INTO|REPLACE\s+INTO)\s+/i", $sql)) { - $this->lastInsID = $this->linkID->lastInsertId(); + $this->lastInsID = $this->linkID->lastInsertId($sequence); if ($getLastInsID) { return $this->lastInsID; } @@ -400,11 +409,11 @@ abstract class Connection /** * 根据参数绑定组装最终的SQL语句 便于调试 * @access public - * @param string $sql 带参数绑定的sql语句 - * @param array $bind 参数绑定列表 + * @param string $sql 带参数绑定的sql语句 + * @param array $bind 参数绑定列表 * @return string */ - protected function getBindSql($sql, array $bind = []) + public function getRealSql($sql, array $bind = []) { if ($bind) { foreach ($bind as $key => $val) { @@ -412,7 +421,10 @@ abstract class Connection // 判断占位符 $sql = is_numeric($key) ? substr_replace($sql, $val, strpos($sql, '?'), 1) : - str_replace([':' . $key . ')', ':' . $key . ' '], [$val . ')', $val . ' '], $sql . ' '); + str_replace( + [':' . $key . ')', ':' . $key . ',', ':' . $key . ' '], + [$val . ')', $val . ',', $val . ' '], + $sql . ' '); } } return $sql; @@ -438,7 +450,7 @@ abstract class Connection $result = $this->PDOStatement->bindValue($param, $val); } if (!$result) { - throw new DbBindParamException( + throw new BindParamException( "Error occurred when binding parameters '{$param}'", $this->config, $this->queryStr, @@ -451,104 +463,118 @@ abstract class Connection /** * 获得数据集 * @access protected - * @param bool|string $class true 返回PDOStatement 字符串用于指定返回的类名 + * @param bool|string $class true 返回PDOStatement 字符串用于指定返回的类名 + * @param bool $procedure 是否存储过程 * @return mixed */ - protected function getResult($class = '') + protected function getResult($class = '', $procedure = false) { if (true === $class) { // 返回PDOStatement对象处理 return $this->PDOStatement; } + if ($procedure) { + // 存储过程返回结果 + return $this->procedure($class); + } $result = $this->PDOStatement->fetchAll($this->fetchType); $this->numRows = count($result); if (!empty($class)) { // 返回指定数据集对象类 - return new $class($result); - } - switch ($this->resultSetType) { - case Db::RESULTSET_COLLECTION: - // 返回数据集Collection对象 - $result = new Collection($result); - break; - case Db::RESULTSET_CLASS: - break; - case Db::RESULTSET_ARRAY: - default: - // 返回二维数组 + $result = new $class($result); + } elseif ('collection' == $this->resultSetType) { + // 返回数据集Collection对象 + $result = new Collection($result); } return $result; } + /** + * 获得存储过程数据集 + * @access protected + * @param bool|string $class true 返回PDOStatement 字符串用于指定返回的类名 + * @return array + */ + protected function procedure($class) + { + $item = []; + do { + $result = $this->getResult($class); + if ($result) { + $item[] = $result; + } + } while ($this->PDOStatement->nextRowset()); + $this->numRows = count($item); + return $item; + } + /** * 执行数据库事务 * @access public * @param callable $callback 数据操作方法回调 * @return mixed + * @throws PDOException + * @throws \Exception + * @throws \Throwable */ public function transaction($callback) { - $this->startTrans(NOW_TIME); + $this->startTrans(); try { $result = null; if (is_callable($callback)) { - $result = call_user_func_array($callback, []); + $result = call_user_func_array($callback, [$this]); } - $this->commit(NOW_TIME); + $this->commit(); return $result; - } catch (\PDOException $e) { + } catch (\Exception $e) { $this->rollback(); - return false; + throw $e; + } catch (\Throwable $e) { + $this->rollback(); + throw $e; } } /** * 启动事务 * @access public - * @param string $label 事务标识 * @return bool|null */ - public function startTrans($label = '') + public function startTrans() { $this->initConnect(true); if (!$this->linkID) { return false; } - //数据rollback 支持 - if (0 == $this->transTimes) { - $this->transLabel = $label; + ++$this->transTimes; + + if (1 == $this->transTimes) { $this->linkID->beginTransaction(); - if (1 == $this->config['deploy']) { - $this->transPDO = $this->linkID; - } + } elseif ($this->transTimes > 1 && $this->supportSavepoint()) { + $this->linkID->exec( + $this->parseSavepoint('trans' . $this->transTimes) + ); } - $this->transTimes++; - return null; } /** * 用于非自动提交状态下面的查询提交 * @access public - * @param string $label 事务标识 * @return boolean * @throws PDOException */ - public function commit($label = '') + public function commit() { - if ($this->transTimes > 0 && $label == $this->transLabel) { - try { - $this->linkID->commit(); - $this->transTimes = 0; - if (1 == $this->config['deploy']) { - $this->transPDO = null; - } - } catch (\PDOException $e) { - throw new PDOException($e, $this->config, $this->queryStr); - } + $this->initConnect(true); + + if (1 == $this->transTimes) { + $this->linkID->commit(); } - return true; + + --$this->transTimes; } /** @@ -559,64 +585,75 @@ abstract class Connection */ public function rollback() { - if ($this->transTimes > 0) { - try { - $this->linkID->rollback(); - $this->transTimes = 0; - if (1 == $this->config['deploy']) { - $this->transPDO = null; - } - } catch (\PDOException $e) { - throw new PDOException($e, $this->config, $this->queryStr); - } + $this->initConnect(true); + + if (1 == $this->transTimes) { + $this->linkID->rollBack(); + } elseif ($this->transTimes > 1 && $this->supportSavepoint()) { + $this->linkID->exec( + $this->parseSavepointRollBack('trans' . $this->transTimes) + ); } - return true; + + $this->transTimes = max(0, $this->transTimes - 1); + } + + /** + * 是否支持事务嵌套 + * @return bool + */ + protected function supportSavepoint() + { + return false; + } + + /** + * 生成定义保存点的SQL + * @param $name + * @return string + */ + protected function parseSavepoint($name) + { + return 'SAVEPOINT ' . $name; + } + + /** + * 生成回滚到保存点的SQL + * @param $name + * @return string + */ + protected function parseSavepointRollBack($name) + { + return 'ROLLBACK TO SAVEPOINT ' . $name; } /** * 批处理执行SQL语句 * 批处理的指令都认为是execute操作 * @access public - * @param array $sql SQL批处理指令 + * @param array $sqlArray SQL批处理指令 * @return boolean */ - public function batchQuery($sql = []) + public function batchQuery($sqlArray = []) { - if (!is_array($sql)) { + if (!is_array($sqlArray)) { return false; } // 自动启动事务支持 - $this->startTrans(NOW_TIME); + $this->startTrans(); try { - foreach ($sql as $_sql) { - $result = $this->execute($_sql); + foreach ($sqlArray as $sql) { + $this->execute($sql); } // 提交事务 - $this->commit(NOW_TIME); - } catch (\PDOException $e) { + $this->commit(); + } catch (\Exception $e) { $this->rollback(); - return false; + throw $e; } return true; } - /** - * 将SQL语句中的__TABLE_NAME__字符串替换成带前缀的表名(小写) - * @access public - * @param string $sql sql语句 - * @return string - */ - public function parseSqlTable($sql) - { - if (false !== strpos($sql, '__')) { - $prefix = $this->config['prefix']; - $sql = preg_replace_callback("/__([A-Z0-9_-]+)__/sU", function ($match) use ($prefix) { - return $prefix . strtolower($match[1]); - }, $sql); - } - return $sql; - } - /** * 获得查询次数 * @access public @@ -699,9 +736,10 @@ abstract class Connection } /** - * 数据库调试 记录当前SQL + * 数据库调试 记录当前SQL及分析性能 * @access protected * @param boolean $start 调试开始标记 true 开始 false 结束 + * @return void */ protected function debug($start) { @@ -716,7 +754,7 @@ abstract class Connection $log = $this->queryStr . ' [ RunTime:' . $runtime . 's ]'; $result = []; // SQL性能分析 - if (0 === stripos(trim($this->queryStr), 'select')) { + if ($this->config['sql_explain'] && 0 === stripos(trim($this->queryStr), 'select')) { $result = $this->getExplain($this->queryStr); } // SQL监听 @@ -739,9 +777,9 @@ abstract class Connection /** * 触发SQL事件 * @access protected - * @param string $sql SQL语句 - * @param float $runtime SQL运行时间 - * @param mixed $explain SQL分析 + * @param string $sql SQL语句 + * @param float $runtime SQL运行时间 + * @param mixed $explain SQL分析 * @return bool */ protected function trigger($sql, $runtime, $explain = []) @@ -764,18 +802,23 @@ abstract class Connection /** * 初始化数据库连接 * @access protected - * @param boolean $master 主服务器 + * @param boolean $master 是否主服务器 * @return void */ protected function initConnect($master = true) { if (!empty($this->config['deploy'])) { - if ($this->transPDO) { - // 使用事务连接 - $this->linkID = $this->transPDO; + // 采用分布式数据库 + if ($master) { + if (!$this->linkWrite) { + $this->linkWrite = $this->multiConnect(true); + } + $this->linkID = $this->linkWrite; } else { - // 采用分布式数据库 - $this->linkID = $this->multiConnect($master); + if (!$this->linkRead) { + $this->linkRead = $this->multiConnect(false); + } + $this->linkID = $this->linkRead; } } elseif (!$this->linkID) { // 默认单数据库 diff --git a/library/think/db/Query.php b/library/think/db/Query.php index 71646d7b..b1451de8 100644 --- a/library/think/db/Query.php +++ b/library/think/db/Query.php @@ -16,8 +16,14 @@ use think\Cache; use think\Collection; use think\Config; use think\Db; +use think\db\Builder; +use think\db\Connection; +use think\db\exception\BindParamException; +use think\db\exception\DataNotFoundException; +use think\db\exception\ModelNotFoundException; use think\Exception; use think\exception\DbException; +use think\exception\PDOException; use think\Loader; use think\Model; use think\model\Relation; @@ -29,29 +35,40 @@ class Query protected $connection; // 数据库驱动类型 protected $driver; - + // 当前模型类名称 + protected $model; + // 当前数据表名称(含前缀) + protected $table = ''; + // 当前数据表名称(不含前缀) + protected $name = ''; + // 当前数据表前缀 + protected $prefix = ''; // 查询参数 protected $options = []; // 参数绑定 protected $bind = []; + // 数据表信息 + protected $info = []; /** * 架构函数 * @access public - * @param object|string $connection 数据库对象实例 - * @throws Exception + * @param Connection $connection 数据库对象实例 + * @param string $model 模型名 */ - public function __construct($connection = '') + public function __construct(Connection $connection = null, $model = '') { $this->connection = $connection ?: Db::connect([], true); $this->driver = $this->connection->getDriverName(); + $this->prefix = $this->connection->getConfig('prefix'); + $this->model = $model; } /** * 利用__call方法实现一些特殊的Model方法 * @access public * @param string $method 方法名称 - * @param array $args 调用参数 + * @param array $args 调用参数 * @return mixed * @throws DbException * @throws Exception @@ -69,19 +86,278 @@ class Query $where[$name] = $args[0]; return $this->where($where)->value($args[1]); } else { - throw new Exception(__CLASS__ . ':' . $method . ' method not exist'); + throw new Exception('method not exist:' . __CLASS__ . '->' . $method); + } + } + + /** + * 获取当前的数据库Connection对象 + * @access public + * @return Connection + */ + public function getConnection() + { + return $this->connection; + } + + /** + * 切换当前的数据库连接 + * @access public + * @param mixed $config + * @return $this + */ + public function connect($config) + { + $this->connection = Db::connect($config); + return $this; + } + + /** + * 指定默认的数据表名(不含前缀) + * @access public + * @param string $name + * @return $this + */ + public function name($name) + { + $this->name = $name; + return $this; + } + + /** + * 指定默认数据表名(含前缀) + * @access public + * @param string $table 表名 + * @return $this + */ + public function setTable($table) + { + $this->table = $table; + return $this; + } + + /** + * 得到当前或者指定名称的数据表 + * @access public + * @param string $name + * @return string + */ + public function getTable($name = '') + { + if ($name || empty($this->table)) { + $name = $name ?: $this->name; + $tableName = $this->prefix; + if ($name) { + $tableName .= Loader::parseName($name); + } + } else { + $tableName = $this->table; + } + return $tableName; + } + + /** + * 将SQL语句中的__TABLE_NAME__字符串替换成带前缀的表名(小写) + * @access public + * @param string $sql sql语句 + * @return string + */ + public function parseSqlTable($sql) + { + if (false !== strpos($sql, '__')) { + $prefix = $this->prefix; + $sql = preg_replace_callback("/__([A-Z0-9_-]+)__/sU", function ($match) use ($prefix) { + return $prefix . strtolower($match[1]); + }, $sql); + } + return $sql; + } + + /** + * 执行查询 返回数据集 + * @access public + * @param string $sql sql指令 + * @param array $bind 参数绑定 + * @param boolean $master 是否在主服务器读操作 + * @param bool|string $class 指定返回的数据集对象 + * @return mixed + * @throws BindParamException + * @throws PDOException + */ + public function query($sql, $bind = [], $master = false, $class = false) + { + return $this->connection->query($sql, $bind, $master, $class); + } + + /** + * 执行语句 + * @access public + * @param string $sql sql指令 + * @param array $bind 参数绑定 + * @param boolean $getLastInsID 是否获取自增ID + * @param boolean $sequence 自增序列名 + * @return int + * @throws BindParamException + * @throws PDOException + */ + public function execute($sql, $bind = [], $getLastInsID = false, $sequence = null) + { + return $this->connection->execute($sql, $bind, $getLastInsID, $sequence); + } + + /** + * 获取最近插入的ID + * @access public + * @return string + */ + public function getLastInsID() + { + return $this->connection->getLastInsID(); + } + + /** + * 获取最近一次查询的sql语句 + * @access public + * @return string + */ + public function getLastSql() + { + return $this->connection->getLastSql(); + } + + /** + * 执行数据库事务 + * @access public + * @param callable $callback 数据操作方法回调 + * @return mixed + */ + public function transaction($callback) + { + return $this->connection->transaction($callback); + } + + /** + * 启动事务 + * @access public + * @return bool|null + */ + public function startTrans() + { + return $this->connection->startTrans(); + } + + /** + * 用于非自动提交状态下面的查询提交 + * @access public + * @return boolean + * @throws PDOException + */ + public function commit() + { + return $this->connection->commit(); + } + + /** + * 事务回滚 + * @access public + * @return boolean + * @throws PDOException + */ + public function rollback() + { + return $this->connection->rollback(); + } + + /** + * 批处理执行SQL语句 + * 批处理的指令都认为是execute操作 + * @access public + * @param array $sql SQL批处理指令 + * @return boolean + */ + public function batchQuery($sql = []) + { + return $this->connection->batchQuery($sql); + } + + /** + * 获取数据库的配置参数 + * @access public + * @param string $name 参数名称 + * @return boolean + */ + public function getConfig($name = '') + { + return $this->connection->getConfig($name); + } + + /** + * 得到分表的的数据表名 + * @access public + * @param array $data 操作的数据 + * @param string $field 分表依据的字段 + * @param array $rule 分表规则 + * @return string + */ + public function getPartitionTableName($data, $field, $rule = []) + { + // 对数据表进行分区 + if ($field && isset($data[$field])) { + $value = $data[$field]; + $type = $rule['type']; + switch ($type) { + case 'id': + // 按照id范围分表 + $step = $rule['expr']; + $seq = floor($value / $step) + 1; + break; + case 'year': + // 按照年份分表 + if (!is_numeric($value)) { + $value = strtotime($value); + } + $seq = date('Y', $value) - $rule['expr'] + 1; + break; + case 'mod': + // 按照id的模数分表 + $seq = ($value % $rule['num']) + 1; + break; + case 'md5': + // 按照md5的序列分表 + $seq = (ord(substr(md5($value), 0, 1)) % $rule['num']) + 1; + break; + default: + if (function_exists($type)) { + // 支持指定函数哈希 + $seq = (ord(substr($type($value), 0, 1)) % $rule['num']) + 1; + } else { + // 按照字段的首字母的值分表 + $seq = (ord($value{0}) % $rule['num']) + 1; + } + } + return $this->getTable() . '_' . $seq; + } else { + // 当设置的分表字段不在查询条件或者数据中 + // 进行联合查询,必须设定 partition['num'] + $tableName = []; + for ($i = 0; $i < $rule['num']; $i++) { + $tableName[] = 'SELECT * FROM ' . $this->getTable() . '_' . ($i + 1); + } + + $tableName = '( ' . implode(" UNION ", $tableName) . ') AS ' . $this->name; + return $tableName; } } /** * 获取当前的builder实例对象 * @access protected - * @return \think\db\Builder + * @return Builder */ protected function builder() { static $builder = []; - $driver = $this->driver; + $driver = $this->driver; if (!isset($builder[$driver])) { $class = '\\think\\db\\builder\\' . ucfirst($driver); $builder[$driver] = new $class($this->connection); @@ -94,15 +370,19 @@ class Query /** * 得到某个字段的值 * @access public - * @param string $field 字段名 + * @param string $field 字段名 + * @param mixed $default 默认值 * @return mixed */ - public function value($field) + public function value($field, $default = null) { - $result = false; + $result = null; if (!empty($this->options['cache'])) { // 判断查询缓存 - $cache = $this->options['cache']; + $cache = $this->options['cache']; + if (empty($this->options['table'])) { + $this->options['table'] = $this->getTable(); + } $key = is_string($cache['key']) ? $cache['key'] : md5($field . serialize($this->options)); $result = Cache::get($key); } @@ -110,7 +390,11 @@ class Query if (isset($this->options['field'])) { unset($this->options['field']); } - $pdo = $this->field($field)->fetchPdo(true)->find(); + $pdo = $this->field($field)->fetchPdo(true)->find(); + if (is_string($pdo)) { + // 返回SQL语句 + return $pdo; + } $result = $pdo->fetchColumn(); if (isset($cache)) { // 缓存数据 @@ -120,14 +404,14 @@ class Query // 清空查询条件 $this->options = []; } - return $result; + return !is_null($result) ? $result : $default; } /** * 得到某个列的数组 * @access public * @param string $field 字段名 多个字段用逗号分隔 - * @param string $key 索引 + * @param string $key 索引 * @return array */ public function column($field, $key = '') @@ -135,7 +419,10 @@ class Query $result = false; if (!empty($this->options['cache'])) { // 判断查询缓存 - $cache = $this->options['cache']; + $cache = $this->options['cache']; + if (empty($this->options['table'])) { + $this->options['table'] = $this->getTable(); + } $guid = is_string($cache['key']) ? $cache['key'] : md5($field . serialize($this->options)); $result = Cache::get($guid); } @@ -147,6 +434,10 @@ class Query $field = $key . ',' . $field; } $pdo = $this->field($field)->fetchPdo(true)->select(); + if (is_string($pdo)) { + // 返回SQL语句 + return $pdo; + } if (1 == $pdo->columnCount()) { $result = $pdo->fetchAll(PDO::FETCH_COLUMN); } else { @@ -162,6 +453,8 @@ class Query $result[$val[$key]] = $val; } elseif (2 == $count) { $result[$val[$key]] = $val[$key2]; + } elseif (1 == $count) { + $result[$val[$key]] = $val[$key1]; } } } else { @@ -187,7 +480,7 @@ class Query */ public function count($field = '*') { - return $this->value('COUNT(' . $field . ') AS tp_count'); + return $this->value('COUNT(' . $field . ') AS tp_count', 0); } /** @@ -198,8 +491,7 @@ class Query */ public function sum($field = '*') { - $result = $this->value('SUM(' . $field . ') AS tp_sum'); - return is_null($result) ? 0 : $result; + return $this->value('SUM(' . $field . ') AS tp_sum', 0); } /** @@ -210,8 +502,7 @@ class Query */ public function min($field = '*') { - $result = $this->value('MIN(' . $field . ') AS tp_min'); - return is_null($result) ? 0 : $result; + return $this->value('MIN(' . $field . ') AS tp_min', 0); } /** @@ -222,8 +513,7 @@ class Query */ public function max($field = '*') { - $result = $this->value('MAX(' . $field . ') AS tp_max'); - return is_null($result) ? 0 : $result; + return $this->value('MAX(' . $field . ') AS tp_max', 0); } /** @@ -234,8 +524,7 @@ class Query */ public function avg($field = '*') { - $result = $this->value('AVG(' . $field . ') AS tp_avg'); - return is_null($result) ? 0 : $result; + return $this->value('AVG(' . $field . ') AS tp_avg', 0); } /** @@ -243,7 +532,7 @@ class Query * 支持使用数据库字段和方法 * @access public * @param string|array $field 字段名 - * @param string $value 字段值 + * @param mixed $value 字段值 * @return integer */ public function setField($field, $value = '') @@ -259,11 +548,11 @@ class Query /** * 字段值(延迟)增长 * @access public - * @param string $field 字段名 - * @param integer $step 增长值 + * @param string $field 字段名 + * @param integer $step 增长值 * @param integer $lazyTime 延时时间(s) * @return integer|true - * @throws \think\Exception + * @throws Exception */ public function setInc($field, $step = 1, $lazyTime = 0) { @@ -286,11 +575,11 @@ class Query /** * 字段值(延迟)减少 * @access public - * @param string $field 字段名 - * @param integer $step 减少值 + * @param string $field 字段名 + * @param integer $step 减少值 * @param integer $lazyTime 延时时间(s) * @return integer|true - * @throws \think\Exception + * @throws Exception */ public function setDec($field, $step = 1, $lazyTime = 0) { @@ -314,8 +603,8 @@ class Query * 延时更新检查 返回false表示需要延时 * 否则返回实际写入的数值 * @access public - * @param string $guid 写入标识 - * @param integer $step 写入步进值 + * @param string $guid 写入标识 + * @param integer $step 写入步进值 * @param integer $lazyTime 延时时间(s) * @return false|integer */ @@ -323,7 +612,7 @@ class Query { if (false !== ($value = Cache::get($guid))) { // 存在缓存写入数据 - if (NOW_TIME > Cache::get($guid . '_time') + $lazyTime) { + if ($_SERVER['REQUEST_TIME'] > Cache::get($guid . '_time') + $lazyTime) { // 延时更新时间到了,删除缓存数据 并实际写入数据库 Cache::rm($guid); Cache::rm($guid . '_time'); @@ -337,7 +626,7 @@ class Query // 没有缓存数据 Cache::set($guid, $step, 0); // 计时开始 - Cache::set($guid . '_time', NOW_TIME, 0); + Cache::set($guid . '_time', $_SERVER['REQUEST_TIME'], 0); return false; } } @@ -345,9 +634,9 @@ class Query /** * 查询SQL组装 join * @access public - * @param mixed $join 关联的表名 - * @param mixed $condition 条件 - * @param string $type JOIN类型 + * @param mixed $join 关联的表名 + * @param mixed $condition 条件 + * @param string $type JOIN类型 * @return $this */ public function join($join, $condition = null, $type = 'INNER') @@ -360,7 +649,7 @@ class Query } } } else { - $prefix = $this->connection->getConfig('prefix'); + $prefix = $this->prefix; // 传入的表名为数组 if (is_array($join)) { if (0 !== $key = key($join)) { @@ -371,7 +660,7 @@ class Query } if (count($join)) { // 有设置第二个元素则把第二元素作为表前缀 - $table = (string) current($join) . $table; + $table = (string)current($join) . $table; } elseif (false === strpos($table, '.')) { // 加上默认的表前缀 $table = $prefix . $table; @@ -379,7 +668,7 @@ class Query } else { $join = trim($join); if (0 === strpos($join, '__')) { - $table = $this->connection->parseSqlTable($join); + $table = $this->parseSqlTable($join); } elseif (false === strpos($join, '(') && false === strpos($join, '.') && !empty($prefix) && 0 !== strpos($join, $prefix)) { // 传入的表名中不带有'('并且不以默认的表前缀开头时加上默认的表前缀 $table = $prefix . $join; @@ -398,7 +687,7 @@ class Query /** * 查询SQL组装 union * @access public - * @param mixed $union + * @param mixed $union * @param boolean $all * @return $this */ @@ -417,11 +706,11 @@ class Query /** * 指定查询字段 支持字段排除和指定数据表 * @access public - * @param mixed $field - * @param boolean $except 是否排除 - * @param string $tableName 数据表名 - * @param string $prefix 字段前缀 - * @param string $alias 别名前缀 + * @param mixed $field + * @param boolean $except 是否排除 + * @param string $tableName 数据表名 + * @param string $prefix 字段前缀 + * @param string $alias 别名前缀 * @return $this */ public function field($field, $except = false, $tableName = '', $prefix = '', $alias = '') @@ -430,12 +719,12 @@ class Query return $this; } if (is_string($field)) { - $field = explode(',', $field); + $field = array_map('trim', explode(',', $field)); } if (true === $field) { // 获取全部字段 $fields = $this->getTableInfo($tableName, 'fields'); - $field = $fields ?: '*'; + $field = $fields ?: ['*']; } elseif ($except) { // 字段排除 $fields = $this->getTableInfo($tableName, 'fields'); @@ -460,10 +749,80 @@ class Query } /** - * 指定查询条件 + * 指定JOIN查询字段 * @access public - * @param mixed $field 查询字段 - * @param mixed $op 查询表达式 + * @param string|array $table 数据表 + * @param string|array $field 查询字段 + * @param string|array $on JOIN条件 + * @param string $type JOIN类型 + * @return $this + */ + public function view($join, $field = null, $on = null, $type = 'INNER') + { + $this->options['view'] = true; + if (is_array($join) && is_null($field)) { + foreach ($join as $key => $val) { + $this->view($key, $val[0], isset($val[1]) ? $val[1] : null, isset($val[2]) ? $val[2] : 'INNER'); + } + } else { + $fields = []; + if (is_array($join)) { + // 支持数据表别名 + list($join, $alias, $table) = array_pad($join, 3, ''); + } else { + $alias = $join; + } + $table = !empty($table) ? $table : $this->getTable($join); + if (true === $field) { + $fields = $alias . '.*'; + } else { + if (is_string($field)) { + $field = explode(',', $field); + } + foreach ($field as $key => $val) { + if (is_numeric($key)) { + $fields[] = $alias . '.' . $val; + $this->options['map'][$val] = $alias . '.' . $val; + } else { + if (preg_match('/[,=\.\'\"\(\s]/', $key)) { + $name = $key; + } else { + $name = $alias . '.' . $key; + } + $fields[] = $name . ' AS ' . $val; + $this->options['map'][$val] = $name; + } + } + } + $this->field($fields); + if ($on) { + $this->join($table . ' ' . $alias, $on, $type); + } else { + $this->table($table . ' ' . $alias); + } + } + return $this; + } + + /** + * 设置分表规则 + * @access public + * @param array $data 操作的数据 + * @param string $field 分表依据的字段 + * @param array $rule 分表规则 + * @return $this + */ + public function partition($data, $field, $rule = []) + { + $this->options['table'] = $this->getPartitionTableName($data, $field, $rule); + return $this; + } + + /** + * 指定AND查询条件 + * @access public + * @param mixed $field 查询字段 + * @param mixed $op 查询表达式 * @param mixed $condition 查询条件 * @return $this */ @@ -476,10 +835,10 @@ class Query } /** - * 指定查询条件 + * 指定OR查询条件 * @access public - * @param mixed $field 查询字段 - * @param mixed $op 查询表达式 + * @param mixed $field 查询字段 + * @param mixed $op 查询表达式 * @param mixed $condition 查询条件 * @return $this */ @@ -491,23 +850,40 @@ class Query return $this; } + /** + * 指定XOR查询条件 + * @access public + * @param mixed $field 查询字段 + * @param mixed $op 查询表达式 + * @param mixed $condition 查询条件 + * @return $this + */ + public function whereXor($field, $op = null, $condition = null) + { + $param = func_get_args(); + array_shift($param); + $this->parseWhereExp('XOR', $field, $op, $condition, $param); + return $this; + } + /** * 分析查询表达式 * @access public - * @param string|array|\Closure $field 查询字段 - * @param mixed $op 查询表达式 - * @param mixed $condition 查询条件 - * @param string $operator and or + * @param string $logic 查询逻辑 and or xor + * @param string|array|\Closure $field 查询字段 + * @param mixed $op 查询表达式 + * @param mixed $condition 查询条件 + * @param array $param 查询参数 * @return void */ - protected function parseWhereExp($operator, $field, $op, $condition, $param = []) + protected function parseWhereExp($logic, $field, $op, $condition, $param = []) { if ($field instanceof \Closure) { - call_user_func_array($field, [ & $this]); + $this->options['where'][$logic][] = is_string($op) ? [$op, $field] : $field; return; } - if (is_string($field) && !empty($this->options['via'])) { + if (is_string($field) && !empty($this->options['via']) && !strpos($field, '.')) { $field = $this->options['via'] . '.' . $field; } if (is_string($field) && preg_match('/[,=\>\<\'\"\(\s]/', $field)) { @@ -538,61 +914,13 @@ class Query $where[$field] = [$op, $condition]; } if (!empty($where)) { - if (!isset($this->options['where'][$operator])) { - $this->options['where'][$operator] = []; + if (!isset($this->options['where'][$logic])) { + $this->options['where'][$logic] = []; } - $this->options['where'][$operator] = array_merge($this->options['where'][$operator], $where); + $this->options['where'][$logic] = array_merge($this->options['where'][$logic], $where); } } - /** - * 指定查询条件 - * @access public - * @param mixed $where 条件表达式 - * @return $this - */ - public function whereExist($where) - { - $this->options['where']['AND'][] = ['EXISTS', $where]; - return $this; - } - - /** - * 指定查询条件 - * @access public - * @param mixed $where 条件表达式 - * @return $this - */ - public function whereOrExist($where) - { - $this->options['where']['OR'][] = ['EXISTS', $where]; - return $this; - } - - /** - * 指定查询条件 - * @access public - * @param mixed $where 条件表达式 - * @return $this - */ - public function whereNotExist($where) - { - $this->options['where']['AND'][] = ['NOT EXISTS', $where]; - return $this; - } - - /** - * 指定查询条件 - * @access public - * @param mixed $where 条件表达式 - * @return $this - */ - public function whereOrNotExist($where) - { - $this->options['where']['OR'][] = ['NOT EXISTS', $where]; - return $this; - } - /** * 指定查询数量 * @access public @@ -612,7 +940,7 @@ class Query /** * 指定分页 * @access public - * @param mixed $page 页数 + * @param mixed $page 页数 * @param mixed $listRows 每页数量 * @return $this */ @@ -627,26 +955,27 @@ class Query /** * 分页查询 - * @param int $listRows 每页数量 - * @param bool $simple 简洁模式 - * @param array $config 配置参数 - * page:当前页, - * path:url路径, - * query:url额外参数, - * fragment:url锚点, - * var_page:分页变量, - * type:分页类名, - * namespace:分页类命名空间 + * @param int|null $listRows 每页数量 + * @param bool $simple 简洁模式 + * @param array $config 配置参数 + * page:当前页, + * path:url路径, + * query:url额外参数, + * fragment:url锚点, + * var_page:分页变量, + * list_rows:每页数量 + * type:分页类名 * @return \think\paginator\Collection * @throws DbException */ - public function paginate($listRows = 15, $simple = false, $config = []) + public function paginate($listRows = null, $simple = false, $config = []) { - $config = array_merge(Config::get('paginate'), $config); + $config = array_merge(Config::get('paginate'), $config); + $listRows = $listRows ?: $config['list_rows']; - $class = (!empty($config['namespace']) ? $config['namespace'] : '\\think\\paginator\\driver\\') . ucwords($config['type']); - - $page = isset($config['page']) ? (int) $config['page'] : call_user_func([ + /** @var Paginator $class */ + $class = false !== strpos($config['type'], '\\') ? $config['type'] : '\\think\\paginator\\driver\\' . ucwords($config['type']); + $page = isset($config['page']) ? (int)$config['page'] : call_user_func([ $class, 'getCurrentPage', ], $config['var_page']); @@ -654,23 +983,21 @@ class Query $page = $page < 1 ? 1 : $page; $config['path'] = isset($config['path']) ? $config['path'] : call_user_func([$class, 'getCurrentPath']); - - /** @var Paginator $paginator */ + if (!$simple) { - $options = $this->getOptions(); - $total = $this->count(); - $results = $this->options($options)->page($page, $listRows)->select(); - $paginator = new $class($results, $listRows, $page, $simple, $total, $config); + $options = $this->getOptions(); + $total = $this->count(); + $results = $this->options($options)->page($page, $listRows)->select(); } else { - $results = $this->limit(($page - 1) * $listRows, $listRows + 1)->select(); - $paginator = new $class($results, $listRows, $page, $simple, null, $config); + $results = $this->limit(($page - 1) * $listRows, $listRows + 1)->select(); + $total = null; } - return $paginator->items(); + return $class::make($results, $listRows, $page, $total, $simple, $config); } /** - * 指定数据表 + * 指定当前操作的数据表 * @access public * @param string $table 表名 * @return $this @@ -697,7 +1024,7 @@ class Query * 指定排序 order('id','desc') 或者 order(['id'=>'desc','create_time'=>'desc']) * @access public * @param string|array $field 排序字段 - * @param string $order 排序 + * @param string $order 排序 * @return $this */ public function order($field, $order = null) @@ -726,8 +1053,8 @@ class Query /** * 查询缓存 * @access public - * @param mixed $key - * @param integer $expire + * @param mixed $key 缓存key + * @param integer $expire 缓存有效期 * @return $this */ public function cache($key = true, $expire = null) @@ -775,7 +1102,8 @@ class Query */ public function lock($lock = false) { - $this->options['lock'] = $lock; + $this->options['lock'] = $lock; + $this->options['master'] = true; return $this; } @@ -899,72 +1227,106 @@ class Query } /** - * 指定当前模型 + * 设置自增序列名 * @access public - * @param string $model 模型类名称 + * @param string $sequence 自增序列名 * @return $this */ - public function model($model) + public function sequence($sequence = null) { - $this->options['model'] = $model; + $this->options['sequence'] = $sequence; return $this; } /** - * 设置当前name + * 查询日期或者时间 * @access public - * @param string $name + * @param string $field 日期字段名 + * @param string $op 比较运算符或者表达式 + * @param string|array $range 比较范围 * @return $this */ - public function name($name) + public function whereTime($field, $op, $range = null) { - $this->options['name'] = $name; - return $this; - } - - /** - * 得到当前的数据表 - * @access public - * @return string - */ - public function getTable() - { - if (empty($this->options['table'])) { - $tableName = $this->connection->getConfig('prefix'); - if (isset($this->options['name'])) { - $tableName .= Loader::parseName($this->options['name']); + if (is_null($range)) { + // 使用日期表达式 + $date = getdate(); + switch (strtolower($op)) { + case 'today': + case 'd': + $range = 'today'; + break; + case 'week': + case 'w': + $range = 'this week 00:00:00'; + break; + case 'month': + case 'm': + $range = mktime(0, 0, 0, $date['mon'], 1, $date['year']); + break; + case 'year': + case 'y': + $range = mktime(0, 0, 0, 1, 1, $date['year']); + break; + case 'yesterday': + $range = ['yesterday', 'today']; + break; + case 'last week': + $range = ['last week 00:00:00', 'this week 00:00:00']; + break; + case 'last month': + $range = [date('y-m-01', strtotime('-1 month')), mktime(0, 0, 0, $date['mon'], 1, $date['year'])]; + break; + case 'last year': + $range = [mktime(0, 0, 0, 1, 1, $date['year'] - 1), mktime(0, 0, 0, 1, 1, $date['year'])]; + break; } - } else { - $tableName = $this->options['table']; + $op = is_array($range) ? 'between' : '>'; } - return $tableName; + $this->where($field, strtolower($op) . ' time', $range); + return $this; + } + + /** + * 设置字段类型 + * @access public + * @param array $fieldType 字段类型信息 + * @return $this + */ + public function setFieldType($fieldType = []) + { + $this->options['field_type'] = $fieldType; + return $this; } /** * 获取数据表信息 * @access public - * @param string $fetch 获取信息类型 包括 fields type bind pk * @param string $tableName 数据表名 留空自动获取 + * @param string $fetch 获取信息类型 包括 fields type bind pk * @return mixed */ public function getTableInfo($tableName = '', $fetch = '') { - static $_info = []; if (!$tableName) { $tableName = $this->getTable(); } if (is_array($tableName)) { $tableName = key($tableName) ?: current($tableName); } + if (strpos($tableName, ',')) { // 多表不获取字段信息 return false; + } else { + $tableName = $this->parseSqlTable($tableName); } - $guid = md5($tableName); - if (!isset($_info[$guid])) { + + $guid = $tableName; + if (!isset($this->info[$guid])) { $info = $this->connection->getFields($tableName); $fields = array_keys($info); - $bind = $type = []; + $bind = $type = []; foreach ($info as $key => $val) { // 记录字段类型 $type[$key] = $val['type']; @@ -985,17 +1347,16 @@ class Query } else { $pk = null; } - $result = ['fields' => $fields, 'type' => $type, 'bind' => $bind, 'pk' => $pk]; - $_info[$guid] = $result; + $this->info[$guid] = ['fields' => $fields, 'type' => $type, 'bind' => $bind, 'pk' => $pk]; } - return $fetch ? $_info[$guid][$fetch] : $_info[$guid]; + return $fetch ? $this->info[$guid][$fetch] : $this->info[$guid]; } /** - * 获取当前模型对象的主键 + * 获取当前数据表的主键 * @access public * @param string $table 数据表名 - * @return mixed + * @return string|array */ public function getPk($table = '') { @@ -1005,9 +1366,9 @@ class Query /** * 参数绑定 * @access public - * @param mixed $key 参数名 - * @param mixed $value 绑定变量值 - * @param integer $type 绑定类型 + * @param mixed $key 参数名 + * @param mixed $value 绑定变量值 + * @param integer $type 绑定类型 * @return $this */ public function bind($key, $value = false, $type = PDO::PARAM_STR) @@ -1020,6 +1381,12 @@ class Query return $this; } + /** + * 检测参数是否已经绑定 + * @access public + * @param string $key 参数名 + * @return bool + */ public function isBind($key) { return isset($this->bind[$key]); @@ -1037,6 +1404,12 @@ class Query return $this; } + /** + * 获取当前的查询参数 + * @access public + * @param string $name 参数名 + * @return mixed + */ public function getOptions($name = '') { return isset($this->options[$name]) ? $this->options[$name] : $this->options; @@ -1046,7 +1419,7 @@ class Query * 设置关联查询JOIN预查询 * @access public * @param string|array $with 关联方法名称 - * @return Db + * @return $this */ public function with($with) { @@ -1059,7 +1432,7 @@ class Query } $i = 0; - $currentModel = $this->options['model']; + $currentModel = $this->model; /** @var Model $class */ $class = new $currentModel; @@ -1071,28 +1444,51 @@ class Query $relation = $key; $with[$key] = $key; } elseif (is_string($relation) && strpos($relation, '.')) { - $with[$key] = $relation; + $with[$key] = $relation; list($relation, $subRelation) = explode('.', $relation, 2); } /** @var Relation $model */ $model = $class->$relation(); - $info = $class->getRelationInfo(); + $info = $model->getRelationInfo(); if (in_array($info['type'], [Relation::HAS_ONE, Relation::BELONGS_TO])) { if (0 == $i) { $name = Loader::parseName(basename(str_replace('\\', '/', $currentModel))); $table = $this->getTable(); - $this->table($table)->alias($name)->field(true, false, $table, $name); + $alias = isset($info['alias'][$name]) ? $info['alias'][$name] : $name; + $this->table($table)->alias($alias); + if (isset($this->options['field'])) { + $field = $this->options['field']; + unset($this->options['field']); + } else { + $field = true; + } + $this->field($field, false, $table, $alias); } // 预载入封装 $joinTable = $model->getTable(); $joinName = Loader::parseName(basename(str_replace('\\', '/', $info['model']))); - $this->via($joinName); - $this->join($joinTable . ' ' . $joinName, $name . '.' . $info['localKey'] . '=' . $joinName . '.' . $info['foreignKey'])->field(true, false, $joinTable, $joinName, $joinName . '__'); + $joinAlias = isset($info['alias'][$joinName]) ? $info['alias'][$joinName] : $joinName; + $this->via($joinAlias); + + if (Relation::HAS_ONE == $info['type']) { + $this->join($joinTable . ' ' . $joinAlias, $alias . '.' . $info['localKey'] . '=' . $joinAlias . '.' . $info['foreignKey'], $info['joinType']); + } else { + $this->join($joinTable . ' ' . $joinAlias, $alias . '.' . $info['foreignKey'] . '=' . $joinAlias . '.' . $info['localKey'], $info['joinType']); + } + if ($closure) { // 执行闭包查询 - call_user_func_array($closure, [ & $this]); + call_user_func_array($closure, [& $this]); + //指定获取关联的字段 + //需要在 回调中 调方法 withField 方法,如 + // $query->where(['id'=>1])->withField('id,name'); + if (!empty($this->options['with_field'])) { + $field = $this->options['with_field']; + unset($this->options['with_field']); + } } + $this->field($field, false, $joinTable, $joinAlias, $relation . '__'); $i++; } elseif ($closure) { $with[$key] = $closure; @@ -1103,11 +1499,27 @@ class Query return $this; } + /** + * 关联预加载中 获取关联指定字段值 + * example: + * Model::with(['relation' => function($query){ + * $query->withField("id,name"); + * }]) + * + * @param string | array $field 指定获取的字段 + * @return $this + */ + public function withField($field) + { + $this->options['with_field'] = $field; + return $this; + } + /** * 设置当前字段添加的表别名 * @access public * @param string $via - * @return Db + * @return $this */ public function via($via = '') { @@ -1119,7 +1531,7 @@ class Query * 设置关联查询 * @access public * @param string $relation 关联名称 - * @return Db + * @return $this */ public function relation($relation) { @@ -1130,10 +1542,10 @@ class Query /** * 把主键值转换为查询条件 支持复合主键 * @access public - * @param array $data 主键数据 - * @param mixed $options 表达式参数 + * @param array|string $data 主键数据 + * @param mixed $options 表达式参数 * @return void - * @throws \think\Exception + * @throws Exception */ protected function parsePkWhere($data, &$options) { @@ -1150,7 +1562,6 @@ class Query } else { $where[$key] = strpos($data, ',') ? ['IN', $data] : $data; } - $options['where']['AND'] = $where; } elseif (is_array($pk) && is_array($data) && !empty($data)) { // 根据复合主键查询 foreach ($pk as $key) { @@ -1161,7 +1572,14 @@ class Query throw new Exception('miss complex primary data'); } } - $options['where']['AND'] = $where; + } + + if (!empty($where)) { + if (isset($options['where']['AND'])) { + $options['where']['AND'] = array_merge($options['where']['AND'], $where); + } else { + $options['where']['AND'] = $where; + } } return; } @@ -1169,77 +1587,95 @@ class Query /** * 插入记录 * @access public - * @param mixed $data 数据 - * @param boolean $replace 是否replace + * @param mixed $data 数据 + * @param boolean $replace 是否replace * @param boolean $getLastInsID 是否获取自增ID - * @return integer + * @param string $sequence 自增序列名 + * @return integer|string */ - public function insert(array $data, $replace = false, $getLastInsID = false) + public function insert(array $data, $replace = false, $getLastInsID = false, $sequence = null) { // 分析查询表达式 $options = $this->parseExpress(); // 生成SQL语句 $sql = $this->builder()->insert($data, $options, $replace); + if ($options['fetch_sql']) { + // 获取实际执行的SQL语句 + return $this->connection->getRealSql($sql, $this->bind); + } + $sequence = $sequence ?: (isset($options['sequence']) ? $options['sequence'] : null); // 执行操作 - return $this->connection->execute($sql, $this->getBind(), $options['fetch_sql'], $getLastInsID); + return $this->execute($sql, $this->getBind(), $getLastInsID, $sequence); } /** * 插入记录并获取自增ID * @access public - * @param mixed $data 数据 - * @param boolean $replace 是否replace - * @return integer + * @param mixed $data 数据 + * @param boolean $replace 是否replace + * @param string $sequence 自增序列名 + * @return integer|string */ - public function insertGetId(array $data, $replace = false) + public function insertGetId(array $data, $replace = false, $sequence = null) { - return $this->insert($data, $replace, true); + return $this->insert($data, $replace, true, $sequence); } /** * 批量插入记录 * @access public * @param mixed $dataSet 数据集 - * @return integer + * @return integer|string */ public function insertAll(array $dataSet) { // 分析查询表达式 $options = $this->parseExpress(); - if (!is_array($dataSet[0])) { + if (!is_array(reset($dataSet))) { return false; } // 生成SQL语句 $sql = $this->builder()->insertAll($dataSet, $options); - // 执行操作 - return $this->connection->execute($sql, $this->getBind(), $options['fetch_sql']); + if ($options['fetch_sql']) { + // 获取实际执行的SQL语句 + return $this->connection->getRealSql($sql, $this->bind); + } else { + // 执行操作 + return $this->execute($sql, $this->getBind()); + } } /** * 通过Select方式插入记录 * @access public * @param string $fields 要插入的数据表字段名 - * @param string $table 要插入的数据表名 - * @return int - * @throws \think\exception\PDOException + * @param string $table 要插入的数据表名 + * @return integer|string + * @throws PDOException */ public function selectInsert($fields, $table) { // 分析查询表达式 $options = $this->parseExpress(); // 生成SQL语句 - $sql = $this->builder()->selectInsert($fields, $table, $options); - // 执行操作 - return $this->connection->execute($sql, $this->getBind(), $options['fetch_sql']); + $table = $this->parseSqlTable($table); + $sql = $this->builder()->selectInsert($fields, $table, $options); + if ($options['fetch_sql']) { + // 获取实际执行的SQL语句 + return $this->connection->getRealSql($sql, $this->bind); + } else { + // 执行操作 + return $this->execute($sql, $this->getBind()); + } } /** * 更新记录 * @access public * @param mixed $data 数据 - * @return int + * @return integer|string * @throws Exception - * @throws \think\exception\PDOException + * @throws PDOException */ public function update(array $data) { @@ -1249,6 +1685,7 @@ class Query // 如果存在主键数据 则自动作为更新条件 if (is_string($pk) && isset($data[$pk])) { $where[$pk] = $data[$pk]; + $key = 'think:' . $options['table'] . '|' . $data[$pk]; unset($data[$pk]); } elseif (is_array($pk)) { // 增加复合主键支持 @@ -1257,7 +1694,7 @@ class Query $where[$field] = $data[$field]; } else { // 如果缺少复合主键数据则不执行 - throw new Exception('miss pk data'); + throw new Exception('miss complex primary data'); } unset($data[$field]); } @@ -1271,28 +1708,36 @@ class Query } // 生成UPDATE SQL语句 $sql = $this->builder()->update($data, $options); - if ('' == $sql) { - return 0; + if ($options['fetch_sql']) { + // 获取实际执行的SQL语句 + return $this->connection->getRealSql($sql, $this->bind); + } else { + // 检测缓存 + if (isset($key) && Cache::get($key)) { + // 删除缓存 + Cache::rm($key); + } + // 执行操作 + return '' == $sql ? 0 : $this->execute($sql, $this->getBind()); } - // 执行操作 - return $this->connection->execute($sql, $this->getBind(), $options['fetch_sql']); } /** * 查找记录 * @access public - * @param array $data + * @param array|string|Query|\Closure $data * @return Collection|false|\PDOStatement|string * @throws DbException - * @throws Exception - * @throws \think\exception\PDOException + * @throws ModelNotFoundException + * @throws DataNotFoundException */ - public function select($data = []) + public function select($data = null) { if ($data instanceof Query) { return $data->select(); } elseif ($data instanceof \Closure) { - call_user_func_array($data, [ & $this]); + call_user_func_array($data, [& $this]); + $data = null; } // 分析查询表达式 $options = $this->parseExpress(); @@ -1300,13 +1745,13 @@ class Query if (false === $data) { // 用于子查询 不查询只返回SQL $options['fetch_sql'] = true; - } elseif (empty($options['where']) && !empty($data)) { + } elseif (!is_null($data)) { // 主键条件分析 $this->parsePkWhere($data, $options); } $resultSet = false; - if (!empty($options['cache'])) { + if (empty($options['fetch_sql']) && !empty($options['cache'])) { // 判断查询缓存 $cache = $options['cache']; $key = is_string($cache['key']) ? $cache['key'] : md5(serialize($options)); @@ -1315,13 +1760,13 @@ class Query if (!$resultSet) { // 生成查询SQL $sql = $this->builder()->select($options); - // 执行查询操作 - $resultSet = $this->connection->query($sql, $this->getBind(), $options['fetch_sql'], $options['master'], $options['fetch_class']); - - if (is_string($resultSet)) { - // 返回SQL - return $resultSet; + if ($options['fetch_sql']) { + // 获取实际执行的SQL语句 + return $this->connection->getRealSql($sql, $this->bind); } + // 执行查询操作 + $resultSet = $this->query($sql, $this->getBind(), $options['master'], $options['fetch_class']); + if ($resultSet instanceof \PDOStatement) { // 返回PDOStatement对象 return $resultSet; @@ -1335,11 +1780,10 @@ class Query // 返回结果处理 if ($resultSet) { - // 数据列表读取后的处理 - if (!empty($options['model'])) { + if (!empty($this->model)) { // 生成模型对象 - $model = $options['model']; + $model = $this->model; foreach ($resultSet as $key => $result) { /** @var Model $result */ $result = new $model($result); @@ -1352,11 +1796,11 @@ class Query } if (!empty($options['with'])) { // 预载入 - $resultSet = $result->eagerlyResultSet($resultSet, $options['with'], is_class($resultSet) ? get_class($resultSet) : ''); + $resultSet = $result->eagerlyResultSet($resultSet, $options['with'], is_object($resultSet) ? get_class($resultSet) : ''); } } } elseif (!empty($options['fail'])) { - throw new DbException('Data not Found', $options, $sql); + $this->throwNotFound($options); } return $resultSet; } @@ -1364,45 +1808,49 @@ class Query /** * 查找单条记录 * @access public - * @param array $data 表达式 + * @param array|string|Query|\Closure $data * @return array|false|\PDOStatement|string|Model * @throws DbException - * @throws Exception - * @throws \think\exception\PDOException + * @throws ModelNotFoundException + * @throws DataNotFoundException */ - public function find($data = []) + public function find($data = null) { if ($data instanceof Query) { return $data->find(); } elseif ($data instanceof \Closure) { - call_user_func_array($data, [ & $this]); + call_user_func_array($data, [& $this]); + $data = null; } // 分析查询表达式 $options = $this->parseExpress(); - if (empty($options['where']) && (!empty($data) || 0 == $data)) { + if (!is_null($data)) { // AR模式分析主键条件 $this->parsePkWhere($data, $options); } $options['limit'] = 1; $result = false; - if (!empty($options['cache'])) { + if (empty($options['fetch_sql']) && !empty($options['cache'])) { // 判断查询缓存 - $cache = $options['cache']; - $key = is_string($cache['key']) ? $cache['key'] : md5(serialize($options)); + $cache = $options['cache']; + if (true === $cache['key'] && !is_null($data) && !is_array($data)) { + $key = 'think:' . $options['table'] . '|' . $data; + } else { + $key = is_string($cache['key']) ? $cache['key'] : md5(serialize($options)); + } $result = Cache::get($key); } if (!$result) { // 生成查询SQL $sql = $this->builder()->select($options); - // 执行查询 - $result = $this->connection->query($sql, $this->getBind(), $options['fetch_sql'], $options['master'], $options['fetch_class']); - - if (is_string($result)) { - // 返回SQL - return $result; + if ($options['fetch_sql']) { + // 获取实际执行的SQL语句 + return $this->connection->getRealSql($sql, $this->bind); } + // 执行查询 + $result = $this->query($sql, $this->getBind(), $options['master'], $options['fetch_class']); if ($result instanceof \PDOStatement) { // 返回PDOStatement对象 @@ -1418,9 +1866,10 @@ class Query // 数据处理 if (!empty($result[0])) { $data = $result[0]; - if (!empty($options['model'])) { + if (!empty($this->model)) { // 返回模型对象 - $data = new $options['model']($data); + $model = $this->model; + $data = new $model($data); $data->isUpdate(true, isset($options['where']['AND']) ? $options['where']['AND'] : null); // 关联查询 if (!empty($options['relation'])) { @@ -1432,25 +1881,70 @@ class Query } } } elseif (!empty($options['fail'])) { - throw new DbException('Data not Found', $options, $sql); + $this->throwNotFound($options); } else { - $data = false; + $data = null; } return $data; } + /** + * 查询失败 抛出异常 + * @access public + * @param array $options 查询参数 + * @throws ModelNotFoundException + * @throws DataNotFoundException + */ + protected function throwNotFound($options = []) + { + if (!empty($this->model)) { + throw new ModelNotFoundException('model data Not Found:' . $this->model, $this->model, $options); + } else { + throw new DataNotFoundException('table data not Found:' . $options['table'], $options['table'], $options); + } + } + + /** + * 查找多条记录 如果不存在则抛出异常 + * @access public + * @param array|string|Query|\Closure $data + * @return array|\PDOStatement|string|Model + * @throws DbException + * @throws ModelNotFoundException + * @throws DataNotFoundException + */ + public function selectOrFail($data = null) + { + return $this->failException(true)->select($data); + } + + /** + * 查找单条记录 如果不存在则抛出异常 + * @access public + * @param array|string|Query|\Closure $data + * @return array|\PDOStatement|string|Model + * @throws DbException + * @throws ModelNotFoundException + * @throws DataNotFoundException + */ + public function findOrFail($data = null) + { + return $this->failException(true)->find($data); + } + /** * 分批数据返回处理 * @access public - * @param integer $count 每次处理的数据数量 + * @param integer $count 每次处理的数据数量 * @param callable $callback 处理回调方法 - * @param string $column 分批处理的字段名 - * @return array + * @param string $column 分批处理的字段名 + * @return boolean */ public function chunk($count, $callback, $column = null) { $column = $column ?: $this->getPk(); $options = $this->getOptions(); + $bind = $this->bind; $resultSet = $this->limit($count)->order($column, 'asc')->select(); while (!empty($resultSet)) { @@ -1461,6 +1955,7 @@ class Query $lastId = is_array($end) ? $end[$column] : $end->$column; $resultSet = $this->options($options) ->limit($count) + ->bind($bind) ->where($column, '>', $lastId) ->order($column, 'asc') ->select(); @@ -1495,37 +1990,52 @@ class Query /** * 删除记录 * @access public - * @param array $data 表达式 + * @param mixed $data 表达式 true 表示强制删除 * @return int * @throws Exception - * @throws \think\exception\PDOException + * @throws PDOException */ - public function delete($data = []) + public function delete($data = null) { // 分析查询表达式 $options = $this->parseExpress(); - if (empty($options['where']) && !empty($data)) { + if (!is_null($data) && true !== $data) { + if (!is_array($data)) { + // 缓存标识 + $key = 'think:' . $options['table'] . '|' . $data; + } // AR模式分析主键条件 $this->parsePkWhere($data, $options); } - if (empty($options['where'])) { + if (true !== $data && empty($options['where'])) { // 如果条件为空 不进行删除操作 除非设置 1=1 - throw new Exception('no data to delete without where'); + throw new Exception('delete without condition'); } // 生成删除SQL语句 $sql = $this->builder()->delete($options); + + if ($options['fetch_sql']) { + // 获取实际执行的SQL语句 + return $this->connection->getRealSql($sql, $this->bind); + } + + // 检测缓存 + if (isset($key) && Cache::get($key)) { + // 删除缓存 + Cache::rm($key); + } // 执行操作 - return $this->connection->execute($sql, $this->getBind(), $options['fetch_sql']); + return $this->execute($sql, $this->getBind()); } /** * 分析表达式(可用于查询或者写入操作) - * @access public + * @access protected * @return array */ - public function parseExpress() + protected function parseExpress() { $options = $this->options; @@ -1536,6 +2046,42 @@ class Query if (!isset($options['where'])) { $options['where'] = []; + } elseif (isset($options['view'])) { + // 视图查询条件处理 + foreach (['AND', 'OR'] as $logic) { + if (isset($options['where'][$logic])) { + foreach ($options['where'][$logic] as $key => $val) { + if (array_key_exists($key, $options['map'])) { + $options['where'][$logic][$options['map'][$key]] = $val; + unset($options['where'][$logic][$key]); + } + } + } + } + + if (isset($options['order'])) { + // 视图查询排序处理 + if (is_string($options['order'])) { + $options['order'] = explode(',', $options['order']); + } + foreach ($options['order'] as $key => $val) { + if (is_numeric($key)) { + if (strpos($val, ' ')) { + list($field, $sort) = explode(' ', $val); + if (array_key_exists($field, $options['map'])) { + $options['order'][$options['map'][$field]] = $sort; + unset($options['order'][$key]); + } + } elseif (array_key_exists($val, $options['map'])) { + $options['order'][$options['map'][$val]] = 'asc'; + unset($options['order'][$key]); + } + } elseif (array_key_exists($key, $options['map'])) { + $options['order'][$options['map'][$key]] = $val; + unset($options['order'][$key]); + } + } + } } // 表别名 @@ -1548,7 +2094,7 @@ class Query } if (!isset($options['strict'])) { - $options['strict'] = $this->connection->getConfig('fields_strict'); + $options['strict'] = $this->getConfig('fields_strict'); } foreach (['master', 'lock', 'fetch_class', 'fetch_sql', 'distinct'] as $name) { @@ -1566,10 +2112,10 @@ class Query if (isset($options['page'])) { // 根据页数计算limit list($page, $listRows) = $options['page']; - $page = $page > 0 ? $page : 1; - $listRows = $listRows > 0 ? $listRows : (is_numeric($options['limit']) ? $options['limit'] : 20); - $offset = $listRows * ($page - 1); - $options['limit'] = $offset . ',' . $listRows; + $page = $page > 0 ? $page : 1; + $listRows = $listRows > 0 ? $listRows : (is_numeric($options['limit']) ? $options['limit'] : 20); + $offset = $listRows * ($page - 1); + $options['limit'] = $offset . ',' . $listRows; } $this->options = []; diff --git a/library/think/db/builder/Mysql.php b/library/think/db/builder/Mysql.php index 3f98adbe..84853307 100644 --- a/library/think/db/builder/Mysql.php +++ b/library/think/db/builder/Mysql.php @@ -18,6 +18,7 @@ use think\db\Builder; */ class Mysql extends Builder { + protected $updateSql = 'UPDATE %TABLE% %JOIN% SET %SET% %WHERE% %ORDER%%LIMIT% %LOCK%%COMMENT%'; /** * 字段和表名处理 @@ -30,7 +31,7 @@ class Mysql extends Builder $key = trim($key); if (strpos($key, '$.') && false === strpos($key, '(')) { // JSON字段支持 - list($field, $name) = explode($key, '$.'); + list($field, $name) = explode('$.', $key); $key = 'jsn_extract(' . $field . ', \'$.\'.' . $name . ')'; } if (!preg_match('/[,\'\"\*\(\)`.\s]/', $key)) { diff --git a/library/think/db/builder/Sqlsrv.php b/library/think/db/builder/Sqlsrv.php index ef4a2e85..f54bd139 100644 --- a/library/think/db/builder/Sqlsrv.php +++ b/library/think/db/builder/Sqlsrv.php @@ -18,7 +18,10 @@ use think\db\Builder; */ class Sqlsrv extends Builder { - protected $selectSql = 'SELECT T1.* FROM (SELECT thinkphp.*, ROW_NUMBER() OVER (%ORDER%) AS ROW_NUMBER FROM (SELECT %DISTINCT% %FIELD% FROM %TABLE%%JOIN%%WHERE%%GROUP%%HAVING%) AS thinkphp) AS T1 %LIMIT%%COMMENT%'; + protected $selectSql = 'SELECT T1.* FROM (SELECT thinkphp.*, ROW_NUMBER() OVER (%ORDER%) AS ROW_NUMBER FROM (SELECT %DISTINCT% %FIELD% FROM %TABLE%%JOIN%%WHERE%%GROUP%%HAVING%) AS thinkphp) AS T1 %LIMIT%%COMMENT%'; + protected $selectInsertSql = 'SELECT %DISTINCT% %FIELD% FROM %TABLE%%JOIN%%WHERE%%GROUP%%HAVING%'; + protected $updateSql = 'UPDATE %TABLE% SET %SET% FROM %TABLE% %JOIN% %WHERE% %LIMIT% %LOCK%%COMMENT%'; + protected $deleteSql = 'DELETE FROM %TABLE% %USING% %JOIN% %WHERE% %LIMIT% %LOCK%%COMMENT%'; /** * order分析 @@ -28,7 +31,7 @@ class Sqlsrv extends Builder */ protected function parseOrder($order) { - return !empty($order) ? ' ORDER BY ' . $order[0] : ' ORDER BY rand()'; + return !empty($order) ? ' ORDER BY ' . $order : ' ORDER BY rand()'; } /** @@ -77,4 +80,10 @@ class Sqlsrv extends Builder return 'WHERE ' . $limitStr; } + public function selectInsert($fields, $table, $options) + { + $this->selectSql = $this->selectInsertSql; + return parent::selectInsert($fields, $table, $options); + } + } diff --git a/library/think/db/connector/Firebird.php b/library/think/db/connector/Firebird.php new file mode 100644 index 00000000..ffd82e50 --- /dev/null +++ b/library/think/db/connector/Firebird.php @@ -0,0 +1,174 @@ + +// +---------------------------------------------------------------------- + +namespace think\db\connector; + +use PDO; +use think\db\Connection; +use think\Log; +use think\Db; +use think\Exception; +use think\exception\PDOException; + +/** + * firebird数据库驱动 + */ +class Firebird extends Connection +{ + + /** + * 解析pdo连接的dsn信息 + * @access public + * @param array $config 连接信息 + * @return string + */ + protected function parseDsn($config) + { + $dsn = 'firebird:dbname=' . $config['hostname'].'/'.$config['hostport'].':'.$config['database']; + return $dsn; + } + + /** + * 取得数据表的字段信息 + * @access public + * @param string $tableName + * @return array + */ + public function getFields($tableName) + { + $this->initConnect(true); + list($tableName) = explode(' ', $tableName); + $sql= 'SELECT TRIM(RF.RDB$FIELD_NAME) AS FIELD,RF.RDB$DEFAULT_VALUE AS DEFAULT1,RF.RDB$NULL_FLAG AS NULL1,TRIM(T.RDB$TYPE_NAME) || \'(\' || F.RDB$FIELD_LENGTH || \')\' as TYPE FROM RDB$RELATION_FIELDS RF LEFT JOIN RDB$FIELDS F ON (F.RDB$FIELD_NAME = RF.RDB$FIELD_SOURCE) LEFT JOIN RDB$TYPES T ON (T.RDB$TYPE = F.RDB$FIELD_TYPE) WHERE RDB$RELATION_NAME=UPPER(\'' . $tableName . '\') AND T.RDB$FIELD_NAME = \'RDB$FIELD_TYPE\' ORDER By RDB$FIELD_POSITION'; + $result = $this->linkID->query($sql); + $info = []; + if ($result) { + foreach ($result as $key => $val) { + $info[$val[0]] = array( + 'name' => $val[0], + 'type' => $val[3], + 'notnull' => ($val[2]==1), + 'default' => $val[1], + 'primary' => false, + 'autoinc' => false, + ); + } + } + //获取主键 + $sql = 'select TRIM(b.rdb$field_name) as field_name from rdb$relation_constraints a join rdb$index_segments b on a.rdb$index_name=b.rdb$index_name where a.rdb$constraint_type=\'PRIMARY KEY\' and a.rdb$relation_name=UPPER(\'' . $tableName . '\')'; + $rs_temp = $this->linkID->query($sql); + foreach ($rs_temp as $row) { + $info[$row[0]]['primary'] = true; + } + return $this->fieldCase($info); + } + + /** + * 取得数据库的表信息 + * @access public + * @param string $dbName + * @return array + */ + public function getTables($dbName = '') + { + $sql = 'SELECT DISTINCT RDB$RELATION_NAME FROM RDB$RELATION_FIELDS WHERE RDB$SYSTEM_FLAG=0'; + $result = $this->query($sql); + $info = []; + foreach ($result as $key => $val) { + $info[$key] = trim(current($val)); + } + return $info; + } + + /** + * 执行语句 + * @access public + * @param string $sql sql指令 + * @param array $bind 参数绑定 + * @param boolean $getLastInsID 是否获取自增ID + * @param string $sequence 自增序列名 + * @return int + * @throws BindParamException + * @throws PDOException + */ + public function execute($sql, $bind = [], $getLastInsID = false, $sequence = null) + { + $this->initConnect(true); + if (!$this->linkID) { + return false; + } + // 根据参数绑定组装最终的SQL语句 + $this->queryStr = $this->getRealSql($sql, $bind); + + //释放前次的查询结果 + if (!empty($this->PDOStatement)) { + $this->free(); + } + + $bind=array_map(function($v){ + return array_map(function($v2){ + return mb_convert_encoding($v2,'gbk','utf-8');},$v); + },$bind); + + Db::$executeTimes++; + try { + // 调试开始 + $this->debug(true); + // 预处理 + $this->PDOStatement = $this->linkID->prepare(mb_convert_encoding($sql,'gbk','utf-8')); + // 参数绑定操作 + $this->bindValue($bind); + // 执行语句 + $result = $this->PDOStatement->execute(); + // 调试结束 + $this->debug(false); + + $this->numRows = $this->PDOStatement->rowCount(); + return $this->numRows; + } catch (\PDOException $e) { + throw new PDOException($e, $this->config, $this->queryStr); + } + } + + /** + * 启动事务 + * @access public + * @return bool|null + */ + public function startTrans() + { + $this->initConnect(true); + if (!$this->linkID) { + return false; + } + + ++$this->transTimes; + + if (1 == $this->transTimes) { + $this->linkID->setAttribute(\PDO::ATTR_AUTOCOMMIT,false); + $this->linkID->beginTransaction(); + } elseif ($this->transTimes > 1 && $this->supportSavepoint()) { + $this->linkID->exec( + $this->parseSavepoint('trans' . $this->transTimes) + ); + } + } + + /** + * SQL性能分析 + * @access protected + * @param string $sql + * @return array + */ + protected function getExplain($sql) + { + return []; + } +} diff --git a/library/think/db/connector/Mysql.php b/library/think/db/connector/Mysql.php index b9d910d3..5f83f496 100644 --- a/library/think/db/connector/Mysql.php +++ b/library/think/db/connector/Mysql.php @@ -110,4 +110,8 @@ class Mysql extends Connection } return $result; } + + protected function supportSavepoint(){ + return true; + } } diff --git a/library/think/db/connector/Oracle.php b/library/think/db/connector/Oracle.php index cc4f6496..840ed44d 100644 --- a/library/think/db/connector/Oracle.php +++ b/library/think/db/connector/Oracle.php @@ -21,8 +21,6 @@ use think\db\Connection; class Oracle extends Connection { - private $table = ''; - /** * 解析pdo连接的dsn信息 * @access protected @@ -48,12 +46,13 @@ class Oracle extends Connection * @access public * @param string $sql sql指令 * @param array $bind 参数绑定 - * @param boolean $fetch 不执行只是获取SQL + * @param boolean $getLastInsID 是否获取自增ID + * @param string $sequence 序列名 * @return integer * @throws \Exception * @throws \think\Exception */ - public function execute($sql, $bind = [], $fetch = false) + public function execute($sql, $bind = [], $getLastInsID = false, $sequence = null) { $this->initConnect(true); if (!$this->linkID) { @@ -61,15 +60,16 @@ class Oracle extends Connection } // 根据参数绑定组装最终的SQL语句 - $this->queryStr = $this->getBindSql($sql, $bind); - if ($fetch) { - return $this->queryStr; - } + $this->queryStr = $this->getRealSql($sql, $bind); + $flag = false; if (preg_match("/^\s*(INSERT\s+INTO)\s+(\w+)\s+/i", $sql, $match)) { - $this->table = Config::get("db_sequence_prefix") . str_ireplace(Config::get("database.prefix"), "", $match[2]); - $flag = (boolean) $this->query("SELECT * FROM all_sequences WHERE sequence_name='" . strtoupper($this->table) . "'"); + if (is_null($sequence)) { + $sequence = Config::get("db_sequence_prefix") . str_ireplace(Config::get("database.prefix"), "", $match[2]); + } + $flag = (boolean) $this->query("SELECT * FROM all_sequences WHERE sequence_name='" . strtoupper($sequence) . "'"); } + //释放前次的查询结果 if (!empty($this->PDOStatement)) { $this->free(); @@ -87,6 +87,9 @@ class Oracle extends Connection $this->numRows = $this->PDOStatement->rowCount(); if ($flag || preg_match("/^\s*(INSERT\s+INTO|REPLACE\s+INTO)\s+/i", $sql)) { $this->lastInsID = $this->linkID->lastInsertId(); + if ($getLastInsID) { + return $this->lastInsID; + } } return $this->numRows; } catch (\PDOException $e) { @@ -102,6 +105,7 @@ class Oracle extends Connection */ public function getFields($tableName) { + $this->initConnect(true); list($tableName) = explode(' ', $tableName); $sql = "select a.column_name,data_type,DECODE (nullable, 'Y', 0, 1) notnull,data_default, DECODE (A .column_name,b.column_name,1,0) pk from all_tab_columns a,(select column_name from all_constraints c, all_cons_columns col where c.constraint_name = col.constraint_name and c.constraint_type = 'P' and c.table_name = '" . strtoupper($tableName) . "' ) b where table_name = '" . strtoupper($tableName) . "' and a.column_name = b.column_name (+)"; $pdo = $this->linkID->query($sql); @@ -126,10 +130,10 @@ class Oracle extends Connection /** * 取得数据库的表信息(暂时实现取得用户表信息) * @access public + * @param string $dbName * @return array - * @internal param string $dbName */ - public function getTables() + public function getTables($dbName = '') { $pdo = $this->linkID->query("select table_name from all_tables"); $result = $pdo->fetchAll(PDO::FETCH_ASSOC); @@ -150,4 +154,9 @@ class Oracle extends Connection { return []; } + + protected function supportSavepoint() + { + return true; + } } diff --git a/library/think/db/connector/Pgsql.php b/library/think/db/connector/Pgsql.php index 58a33c34..161165ac 100644 --- a/library/think/db/connector/Pgsql.php +++ b/library/think/db/connector/Pgsql.php @@ -43,8 +43,9 @@ class Pgsql extends Connection */ public function getFields($tableName) { + $this->initConnect(true); list($tableName) = explode(' ', $tableName); - $sql = 'select fields_name as "field",fields_type as "type",fields_not_null as "null",fields_key_name as "key",fields_default as "default",fields_default as "extra" from table_msg(' . $tableName . ');'; + $sql = 'select fields_name as "field",fields_type as "type",fields_not_null as "null",fields_key_name as "key",fields_default as "default",fields_default as "extra" from table_msg(\'' . $tableName . '\');'; $pdo = $this->linkID->query($sql); $result = $pdo->fetchAll(PDO::FETCH_ASSOC); $info = []; @@ -92,4 +93,8 @@ class Pgsql extends Connection { return []; } + + protected function supportSavepoint(){ + return true; + } } diff --git a/library/think/db/connector/Sqlite.php b/library/think/db/connector/Sqlite.php index e3b03afb..d8f63f81 100644 --- a/library/think/db/connector/Sqlite.php +++ b/library/think/db/connector/Sqlite.php @@ -40,6 +40,7 @@ class Sqlite extends Connection */ public function getFields($tableName) { + $this->initConnect(true); list($tableName) = explode(' ', $tableName); $sql = 'PRAGMA table_info( ' . $tableName . ' )'; $pdo = $this->linkID->query($sql); @@ -91,4 +92,8 @@ class Sqlite extends Connection { return []; } + + protected function supportSavepoint(){ + return true; + } } diff --git a/library/think/db/connector/Sqlsrv.php b/library/think/db/connector/Sqlsrv.php index 2eead773..31cf9045 100644 --- a/library/think/db/connector/Sqlsrv.php +++ b/library/think/db/connector/Sqlsrv.php @@ -24,7 +24,6 @@ class Sqlsrv extends Connection PDO::ATTR_CASE => PDO::CASE_LOWER, PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_STRINGIFY_FETCHES => false, - PDO::SQLSRV_ATTR_ENCODING => PDO::SQLSRV_ENCODING_UTF8, ]; /** @@ -50,6 +49,7 @@ class Sqlsrv extends Connection */ public function getFields($tableName) { + $this->initConnect(true); list($tableName) = explode(' ', $tableName); $sql = "SELECT column_name, data_type, column_default, is_nullable FROM information_schema.tables AS t diff --git a/library/think/db/connector/pgsql.sql b/library/think/db/connector/pgsql.sql new file mode 100644 index 00000000..e1a09a30 --- /dev/null +++ b/library/think/db/connector/pgsql.sql @@ -0,0 +1,117 @@ +CREATE OR REPLACE FUNCTION pgsql_type(a_type varchar) RETURNS varchar AS +$BODY$ +DECLARE + v_type varchar; +BEGIN + IF a_type='int8' THEN + v_type:='bigint'; + ELSIF a_type='int4' THEN + v_type:='integer'; + ELSIF a_type='int2' THEN + v_type:='smallint'; + ELSIF a_type='bpchar' THEN + v_type:='char'; + ELSE + v_type:=a_type; + END IF; + RETURN v_type; +END; +$BODY$ +LANGUAGE PLPGSQL; + +CREATE TYPE "public"."tablestruct" AS ( + "fields_key_name" varchar(100), + "fields_name" VARCHAR(200), + "fields_type" VARCHAR(20), + "fields_length" BIGINT, + "fields_not_null" VARCHAR(10), + "fields_default" VARCHAR(500), + "fields_comment" VARCHAR(1000) +); + +CREATE OR REPLACE FUNCTION "public"."table_msg" (a_schema_name varchar, a_table_name varchar) RETURNS SETOF "public"."tablestruct" AS +$body$ +DECLARE + v_ret tablestruct; + v_oid oid; + v_sql varchar; + v_rec RECORD; + v_key varchar; +BEGIN + SELECT + pg_class.oid INTO v_oid + FROM + pg_class + INNER JOIN pg_namespace ON (pg_class.relnamespace = pg_namespace.oid AND lower(pg_namespace.nspname) = a_schema_name) + WHERE + pg_class.relname=a_table_name; + IF NOT FOUND THEN + RETURN; + END IF; + + v_sql=' + SELECT + pg_attribute.attname AS fields_name, + pg_attribute.attnum AS fields_index, + pgsql_type(pg_type.typname::varchar) AS fields_type, + pg_attribute.atttypmod-4 as fields_length, + CASE WHEN pg_attribute.attnotnull THEN ''not null'' + ELSE '''' + END AS fields_not_null, + pg_attrdef.adsrc AS fields_default, + pg_description.description AS fields_comment + FROM + pg_attribute + INNER JOIN pg_class ON pg_attribute.attrelid = pg_class.oid + INNER JOIN pg_type ON pg_attribute.atttypid = pg_type.oid + LEFT OUTER JOIN pg_attrdef ON pg_attrdef.adrelid = pg_class.oid AND pg_attrdef.adnum = pg_attribute.attnum + LEFT OUTER JOIN pg_description ON pg_description.objoid = pg_class.oid AND pg_description.objsubid = pg_attribute.attnum + WHERE + pg_attribute.attnum > 0 + AND attisdropped <> ''t'' + AND pg_class.oid = ' || v_oid || ' + ORDER BY pg_attribute.attnum' ; + + FOR v_rec IN EXECUTE v_sql LOOP + v_ret.fields_name=v_rec.fields_name; + v_ret.fields_type=v_rec.fields_type; + IF v_rec.fields_length > 0 THEN + v_ret.fields_length:=v_rec.fields_length; + ELSE + v_ret.fields_length:=NULL; + END IF; + v_ret.fields_not_null=v_rec.fields_not_null; + v_ret.fields_default=v_rec.fields_default; + v_ret.fields_comment=v_rec.fields_comment; + SELECT constraint_name INTO v_key FROM information_schema.key_column_usage WHERE table_schema=a_schema_name AND table_name=a_table_name AND column_name=v_rec.fields_name; + IF FOUND THEN + v_ret.fields_key_name=v_key; + ELSE + v_ret.fields_key_name=''; + END IF; + RETURN NEXT v_ret; + END LOOP; + RETURN ; +END; +$body$ +LANGUAGE 'plpgsql' VOLATILE CALLED ON NULL INPUT SECURITY INVOKER; + +COMMENT ON FUNCTION "public"."table_msg"(a_schema_name varchar, a_table_name varchar) +IS '获得表信息'; + +---重载一个函数 +CREATE OR REPLACE FUNCTION "public"."table_msg" (a_table_name varchar) RETURNS SETOF "public"."tablestruct" AS +$body$ +DECLARE + v_ret tablestruct; +BEGIN + FOR v_ret IN SELECT * FROM table_msg('public',a_table_name) LOOP + RETURN NEXT v_ret; + END LOOP; + RETURN; +END; +$body$ +LANGUAGE 'plpgsql' VOLATILE CALLED ON NULL INPUT SECURITY INVOKER; + +COMMENT ON FUNCTION "public"."table_msg"(a_table_name varchar) +IS '获得表信息'; \ No newline at end of file diff --git a/library/think/exception/DbBindParamException.php b/library/think/db/exception/BindParamException.php similarity index 76% rename from library/think/exception/DbBindParamException.php rename to library/think/db/exception/BindParamException.php index 21ddcd58..585a0d73 100644 --- a/library/think/exception/DbBindParamException.php +++ b/library/think/db/exception/BindParamException.php @@ -9,15 +9,24 @@ // | Author: 麦当苗儿 // +---------------------------------------------------------------------- -namespace think\exception; +namespace think\db\exception; use think\exception\DbException; /** * PDO参数绑定异常 */ -class DbBindParamException extends DbException +class BindParamException extends DbException { + + /** + * BindParamException constructor. + * @param string $message + * @param array $config + * @param string $sql + * @param array $bind + * @param int $code + */ public function __construct($message, $config, $sql, $bind, $code = 10502) { $this->setData('Bind Param', $bind); diff --git a/library/think/db/exception/DataNotFoundException.php b/library/think/db/exception/DataNotFoundException.php new file mode 100644 index 00000000..efd66d3b --- /dev/null +++ b/library/think/db/exception/DataNotFoundException.php @@ -0,0 +1,43 @@ + +// +---------------------------------------------------------------------- + +namespace think\db\exception; + +use think\exception\DbException; + +class DataNotFoundException extends DbException +{ + protected $table; + + /** + * DbException constructor. + * @param string $message + * @param string $table + * @param array $config + */ + public function __construct($message, $table = '', Array $config = []) + { + $this->message = $message; + $this->table = $table; + + $this->setData('Database Config', $config); + } + + /** + * 获取数据表名 + * @access public + * @return string + */ + public function getTable() + { + return $this->table; + } +} diff --git a/library/think/db/exception/ModelNotFoundException.php b/library/think/db/exception/ModelNotFoundException.php new file mode 100644 index 00000000..69b70965 --- /dev/null +++ b/library/think/db/exception/ModelNotFoundException.php @@ -0,0 +1,43 @@ + +// +---------------------------------------------------------------------- + +namespace think\db\exception; + +use think\exception\DbException; + +class ModelNotFoundException extends DbException +{ + protected $model; + + /** + * 构造方法 + * @param string $message + * @param string $model + */ + public function __construct($message, $model = '', Array $config = []) + { + $this->message = $message; + $this->model = $model; + + $this->setData('Database Config', $config); + } + + /** + * 获取模型类名 + * @access public + * @return string + */ + public function getModel() + { + return $this->model; + } + +} diff --git a/library/think/debug/Console.php b/library/think/debug/Console.php new file mode 100644 index 00000000..6933d5af --- /dev/null +++ b/library/think/debug/Console.php @@ -0,0 +1,160 @@ + +// +---------------------------------------------------------------------- + +namespace think\debug; + +use think\Cache; +use think\Config; +use think\Db; +use think\Debug; +use think\Request; +use think\Response; + +/** + * 浏览器调试输出 + */ +class Console +{ + protected $config = [ + 'trace_tabs' => ['base' => '基本', 'file' => '文件', 'info' => '流程', 'notice|error' => '错误', 'sql' => 'SQL', 'debug|log' => '调试'], + ]; + + // 实例化并传入参数 + public function __construct($config = []) + { + if (is_array($config)) { + $this->config = array_merge($this->config, $config); + } + } + + /** + * 调试输出接口 + * @access public + * @param Response $response Response对象 + * @param array $log 日志信息 + * @return bool + */ + public function output(Response $response, array $log = []) + { + $request = Request::instance(); + $contentType = $response->getHeader('Content-Type'); + $accept = $request->header('accept'); + if (strpos($accept, 'application/json') === 0 || $request->isAjax()) { + return false; + } elseif (!empty($contentType) && strpos($contentType, 'html') === false) { + return false; + } + // 获取基本信息 + $runtime = number_format(microtime(true), 8, '.', '') - THINK_START_TIME; + $reqs = number_format(1 / $runtime, 2); + $mem = number_format((memory_get_usage() - THINK_START_MEM) / 1024, 2); + + if (isset($_SERVER['HTTP_HOST'])) { + $uri = $_SERVER['SERVER_PROTOCOL'] . ' ' . $_SERVER['REQUEST_METHOD'] . ' : ' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI']; + } else { + $uri = 'cmd:' . implode(' ', $_SERVER['argv']); + } + + // 页面Trace信息 + $base = [ + '请求信息' => date('Y-m-d H:i:s', $_SERVER['REQUEST_TIME']) . ' ' . $uri, + '运行时间' => number_format($runtime, 6) . 's [ 吞吐率:' . $reqs . 'req/s ] 内存消耗:' . $mem . 'kb 文件加载:' . count(get_included_files()), + '查询信息' => Db::$queryTimes . ' queries ' . Db::$executeTimes . ' writes ', + '缓存信息' => Cache::$readTimes . ' reads,' . Cache::$writeTimes . ' writes', + '配置加载' => count(Config::get()), + ]; + + if (session_id()) { + $base['会话信息'] = 'SESSION_ID=' . session_id(); + } + + $info = Debug::getFile(true); + + // 页面Trace信息 + $trace = []; + foreach ($this->config['trace_tabs'] as $name => $title) { + $name = strtolower($name); + switch ($name) { + case 'base': // 基本信息 + $trace[$title] = $base; + break; + case 'file': // 文件信息 + $trace[$title] = $info; + break; + default: // 调试信息 + if (strpos($name, '|')) { + // 多组信息 + $names = explode('|', $name); + $result = []; + foreach ($names as $name) { + $result = array_merge($result, isset($log[$name]) ? $log[$name] : []); + } + $trace[$title] = $result; + } else { + $trace[$title] = isset($log[$name]) ? $log[$name] : ''; + } + } + } + + //输出到控制台 + $lines = ''; + foreach ($trace as $type => $msg) { + $lines .= $this->console($type, $msg); + } + $js = << +{$lines} + +JS; + return $js; + } + + protected function console($type, $msg) + { + $type = strtolower($type); + $trace_tabs = array_values($this->config['trace_tabs']); + $line[] = ($type == $trace_tabs[0] || '调试' == $type || '错误' == $type) + ? "console.group('{$type}');" + : "console.groupCollapsed('{$type}');"; + + foreach ((array) $msg as $key => $m) { + switch ($type) { + case '调试': + $var_type = gettype($m); + if (in_array($var_type, ['array', 'string'])) { + $line[] = "console.log(" . json_encode($m) . ");"; + } else { + $line[] = "console.log(" . json_encode(var_export($m, 1)) . ");"; + } + break; + case '错误': + $msg = str_replace("\n", '\n', $m); + $style = 'color:#F4006B;font-size:14px;'; + $line[] = "console.error(\"%c{$msg}\", \"{$style}\");"; + break; + case 'sql': + $msg = str_replace("\n", '\n', $m); + $style = "color:#009bb4;"; + $line[] = "console.log(\"%c{$msg}\", \"{$style}\");"; + break; + default: + $m = is_string($key) ? $key . ' ' . $m : $key + 1 . ' ' . $m; + $msg = json_encode($m); + $line[] = "console.log({$msg});"; + break; + } + } + $line[] = "console.groupEnd();"; + return implode(PHP_EOL, $line); + } + +} diff --git a/library/think/log/driver/Trace.php b/library/think/debug/Html.php similarity index 61% rename from library/think/log/driver/Trace.php rename to library/think/debug/Html.php index e4565c83..a1d42fb5 100644 --- a/library/think/log/driver/Trace.php +++ b/library/think/debug/Html.php @@ -9,15 +9,19 @@ // | Author: liu21st // +---------------------------------------------------------------------- -namespace think\log\driver; +namespace think\debug; +use think\Cache; use think\Config; +use think\Db; use think\Debug; +use think\Request; +use think\Response; /** * 页面Trace调试 */ -class Trace +class Html { protected $config = [ 'trace_file' => '', @@ -32,29 +36,38 @@ class Trace } /** - * 日志写入接口 + * 调试输出接口 * @access public - * @param array $log 日志信息 + * @param Response $response Response对象 + * @param array $log 日志信息 * @return bool */ - public function save(array $log = []) + public function output(Response $response, array $log = []) { - if (IS_AJAX || IS_CLI || IS_API || 'html' != Config::get('default_return_type')) { - // ajax cli api方式下不输出 + $request = Request::instance(); + $contentType = $response->getHeader('Content-Type'); + $accept = $request->header('accept'); + if (strpos($accept, 'application/json') === 0 || $request->isAjax()) { + return false; + } elseif (!empty($contentType) && strpos($contentType, 'html') === false) { return false; } // 获取基本信息 - $runtime = microtime(true) - START_TIME; - $reqs = number_format(1 / number_format($runtime, 8), 2); - $runtime = number_format($runtime, 6); - $mem = number_format((memory_get_usage() - START_MEM) / 1024, 2); + $runtime = number_format(microtime(true), 8, '.', '') - THINK_START_TIME; + $reqs = number_format(1 / $runtime, 2); + $mem = number_format((memory_get_usage() - THINK_START_MEM) / 1024, 2); // 页面Trace信息 + if (isset($_SERVER['HTTP_HOST'])) { + $uri = $_SERVER['SERVER_PROTOCOL'] . ' ' . $_SERVER['REQUEST_METHOD'] . ' : ' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI']; + } else { + $uri = 'cmd:' . implode(' ', $_SERVER['argv']); + } $base = [ - '请求信息' => date('Y-m-d H:i:s', $_SERVER['REQUEST_TIME']) . ' ' . $_SERVER['SERVER_PROTOCOL'] . ' ' . $_SERVER['REQUEST_METHOD'] . ' : ' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'], - '运行时间' => "{$runtime}s [ 吞吐率:{$reqs}req/s ] 内存消耗:{$mem}kb 文件加载:" . count(get_included_files()), - '查询信息' => \think\Db::$queryTimes . ' queries ' . \think\Db::$executeTimes . ' writes ', - '缓存信息' => \think\Cache::$readTimes . ' reads,' . \think\Cache::$writeTimes . ' writes', + '请求信息' => date('Y-m-d H:i:s', $_SERVER['REQUEST_TIME']) . ' ' . $uri, + '运行时间' => number_format($runtime, 6) . 's [ 吞吐率:' . $reqs . 'req/s ] 内存消耗:' . $mem . 'kb 文件加载:' . count(get_included_files()), + '查询信息' => Db::$queryTimes . ' queries ' . Db::$executeTimes . ' writes ', + '缓存信息' => Cache::$readTimes . ' reads,' . Cache::$writeTimes . ' writes', '配置加载' => count(Config::get()), ]; @@ -64,12 +77,6 @@ class Trace $info = Debug::getFile(true); - // 获取调试日志 - $debug = []; - foreach ($log as $line) { - $debug[$line['type']][] = $line['msg']; - } - // 页面Trace信息 $trace = []; foreach ($this->config['trace_tabs'] as $name => $title) { @@ -87,19 +94,18 @@ class Trace $names = explode('|', $name); $result = []; foreach ($names as $name) { - $result = array_merge($result, isset($debug[$name]) ? $debug[$name] : []); + $result = array_merge($result, isset($log[$name]) ? $log[$name] : []); } $trace[$title] = $result; } else { - $trace[$title] = isset($debug[$name]) ? $debug[$name] : ''; + $trace[$title] = isset($log[$name]) ? $log[$name] : ''; } } } // 调用Trace页面模板 ob_start(); include $this->config['trace_file']; - echo ob_get_clean(); - return true; + return ob_get_clean(); } } diff --git a/library/think/exception/ClassNotFoundException.php b/library/think/exception/ClassNotFoundException.php new file mode 100644 index 00000000..7160f86b --- /dev/null +++ b/library/think/exception/ClassNotFoundException.php @@ -0,0 +1,32 @@ + +// +---------------------------------------------------------------------- + +namespace think\exception; + +class ClassNotFoundException extends \RuntimeException +{ + protected $class; + public function __construct($message,$class='') + { + $this->message = $message; + $this->class = $class; + } + + /** + * 获取类名 + * @access public + * @return string + */ + public function getClass() + { + return $this->class; + } +} \ No newline at end of file diff --git a/library/think/exception/Handle.php b/library/think/exception/Handle.php new file mode 100644 index 00000000..a598df8c --- /dev/null +++ b/library/think/exception/Handle.php @@ -0,0 +1,267 @@ + +// +---------------------------------------------------------------------- + +namespace think\exception; + +use Exception; +use think\App; +use think\Config; +use think\Console; +use think\console\Output; +use think\Lang; +use think\Log; +use think\Response; + +class Handle +{ + + protected $ignoreReport = [ + '\\think\\exception\\HttpException', + ]; + + /** + * Report or log an exception. + * + * @param \Exception $exception + * @return void + */ + public function report(Exception $exception) + { + if (!$this->isIgnoreReport($exception)) { + // 收集异常数据 + if (App::$debug) { + $data = [ + 'file' => $exception->getFile(), + 'line' => $exception->getLine(), + 'message' => $this->getMessage($exception), + 'code' => $this->getCode($exception), + ]; + $log = "[{$data['code']}]{$data['message']}[{$data['file']}:{$data['line']}]"; + } else { + $data = [ + 'code' => $this->getCode($exception), + 'message' => $this->getMessage($exception), + ]; + $log = "[{$data['code']}]{$data['message']}"; + } + + Log::record($log, 'error'); + } + } + + protected function isIgnoreReport(Exception $exception) + { + foreach ($this->ignoreReport as $class) { + if ($exception instanceof $class) { + return true; + } + } + return false; + } + + /** + * Render an exception into an HTTP response. + * + * @param \Exception $e + * @return Response + */ + public function render(Exception $e) + { + if ($e instanceof HttpException) { + return $this->renderHttpException($e); + } else { + return $this->convertExceptionToResponse($e); + } + } + + /** + * @param Output $output + * @param Exception $e + */ + public function renderForConsole(Output $output, Exception $e) + { + if (App::$debug) { + $output->setVerbosity(Output::VERBOSITY_DEBUG); + } + (new Console)->renderException($e, $output); + } + + /** + * @param HttpException $e + * @return Response + */ + protected function renderHttpException(HttpException $e) + { + $status = $e->getStatusCode(); + $template = Config::get('http_exception_template'); + if (!App::$debug && !empty($template[$status])) { + return Response::create($template[$status], 'view')->vars(['e' => $e]); + } else { + return $this->convertExceptionToResponse($e); + } + } + + /** + * @param Exception $exception + * @return Response + */ + protected function convertExceptionToResponse(Exception $exception) + { + // 收集异常数据 + if (App::$debug) { + // 调试模式,获取详细的错误信息 + $data = [ + 'name' => get_class($exception), + 'file' => $exception->getFile(), + 'line' => $exception->getLine(), + 'message' => $this->getMessage($exception), + 'trace' => $exception->getTrace(), + 'code' => $this->getCode($exception), + 'source' => $this->getSourceCode($exception), + 'datas' => $this->getExtendData($exception), + 'tables' => [ + 'GET Data' => $_GET, + 'POST Data' => $_POST, + 'Files' => $_FILES, + 'Cookies' => $_COOKIE, + 'Session' => isset($_SESSION) ? $_SESSION : [], + 'Server/Request Data' => $_SERVER, + 'Environment Variables' => $_ENV, + 'ThinkPHP Constants' => $this->getConst(), + ], + ]; + } else { + // 部署模式仅显示 Code 和 Message + $data = [ + 'code' => $this->getCode($exception), + 'message' => $this->getMessage($exception), + ]; + + if (!Config::get('show_error_msg')) { + // 不显示详细错误信息 + $data['message'] = Config::get('error_message'); + } + } + + //保留一层 + while (ob_get_level() > 1) { + ob_end_clean(); + } + + $data['echo'] = ob_get_clean(); + + ob_start(); + extract($data); + include Config::get('exception_tmpl'); + // 获取并清空缓存 + $content = ob_get_clean(); + $response = new Response($content, 'html'); + + if ($exception instanceof HttpException) { + $statusCode = $exception->getStatusCode(); + $response->header($exception->getHeaders()); + } + + if (!isset($statusCode)) { + $statusCode = 500; + } + $response->code($statusCode); + return $response; + } + + /** + * 获取错误编码 + * ErrorException则使用错误级别作为错误编码 + * @param \Exception $exception + * @return integer 错误编码 + */ + protected function getCode(Exception $exception) + { + $code = $exception->getCode(); + if (!$code && $exception instanceof ErrorException) { + $code = $exception->getSeverity(); + } + return $code; + } + + /** + * 获取错误信息 + * ErrorException则使用错误级别作为错误编码 + * @param \Exception $exception + * @return string 错误信息 + */ + protected function getMessage(Exception $exception) + { + $message = $exception->getMessage(); + if (IS_CLI) { + return $message; + } + // 导入语言包 + if (!Config::get('lang_switch_on')) { + Lang::load(THINK_PATH . 'lang' . DS . Lang::detect() . EXT); + } + + if (strpos($message, ':')) { + $name = strstr($message, ':', true); + return Lang::has($name) ? Lang::get($name) . ' ' . strstr($message, ':') : $message; + } else { + return Lang::has($message) ? Lang::get($message) : $message; + } + } + + /** + * 获取出错文件内容 + * 获取错误的前9行和后9行 + * @param \Exception $exception + * @return array 错误文件内容 + */ + protected function getSourceCode(Exception $exception) + { + // 读取前9行和后9行 + $line = $exception->getLine(); + $first = ($line - 9 > 0) ? $line - 9 : 1; + + try { + $contents = file($exception->getFile()); + $source = [ + 'first' => $first, + 'source' => array_slice($contents, $first - 1, 19), + ]; + } catch (Exception $e) { + $source = []; + } + return $source; + } + + /** + * 获取异常扩展信息 + * 用于非调试模式html返回类型显示 + * @param \Exception $exception + * @return array 异常类定义的扩展数据 + */ + protected function getExtendData(Exception $exception) + { + $data = []; + if ($exception instanceof \think\Exception) { + $data = $exception->getData(); + } + return $data; + } + + /** + * 获取常量列表 + * @return array 常量列表 + */ + private static function getConst() + { + return get_defined_constants(true)['user']; + } +} diff --git a/library/think/exception/HttpException.php b/library/think/exception/HttpException.php new file mode 100644 index 00000000..99beb002 --- /dev/null +++ b/library/think/exception/HttpException.php @@ -0,0 +1,37 @@ + +// +---------------------------------------------------------------------- + +namespace think\exception; + + +class HttpException extends \RuntimeException +{ + private $statusCode; + private $headers; + + public function __construct($statusCode, $message = null, \Exception $previous = null, array $headers = [], $code = 0) + { + $this->statusCode = $statusCode; + $this->headers = $headers; + + parent::__construct($message, $code, $previous); + } + + public function getStatusCode() + { + return $this->statusCode; + } + + public function getHeaders() + { + return $this->headers; + } +} \ No newline at end of file diff --git a/library/think/exception/HttpResponseException.php b/library/think/exception/HttpResponseException.php new file mode 100644 index 00000000..249e3da3 --- /dev/null +++ b/library/think/exception/HttpResponseException.php @@ -0,0 +1,35 @@ + +// +---------------------------------------------------------------------- + +namespace think\exception; + + +use think\Response; + +class HttpResponseException extends \RuntimeException +{ + /** + * @var Response + */ + protected $response; + + public function __construct(Response $response) + { + $this->response = $response; + } + + public function getResponse() + { + return $this->response; + } + + +} \ No newline at end of file diff --git a/library/think/exception/TemplateNotFoundException.php b/library/think/exception/TemplateNotFoundException.php new file mode 100644 index 00000000..b9d5294e --- /dev/null +++ b/library/think/exception/TemplateNotFoundException.php @@ -0,0 +1,33 @@ + +// +---------------------------------------------------------------------- + +namespace think\exception; + +class TemplateNotFoundException extends \RuntimeException +{ + protected $template; + + public function __construct($message,$template='') + { + $this->message = $message; + $this->template = $template; + } + + /** + * 获取模板文件 + * @access public + * @return string + */ + public function getTemplate() + { + return $this->template; + } +} \ No newline at end of file diff --git a/library/think/exception/ThrowableError.php b/library/think/exception/ThrowableError.php new file mode 100644 index 00000000..8ba26ea8 --- /dev/null +++ b/library/think/exception/ThrowableError.php @@ -0,0 +1,49 @@ + +// +---------------------------------------------------------------------- + +namespace think\exception; + + +class ThrowableError extends \ErrorException +{ + public function __construct(\Throwable $e) + { + + if ($e instanceof \ParseError) { + $message = 'Parse error: ' . $e->getMessage(); + $severity = E_PARSE; + } elseif ($e instanceof \TypeError) { + $message = 'Type error: ' . $e->getMessage(); + $severity = E_RECOVERABLE_ERROR; + } else { + $message = 'Fatal error: ' . $e->getMessage(); + $severity = E_ERROR; + } + + parent::__construct( + $message, + $e->getCode(), + $severity, + $e->getFile(), + $e->getLine() + ); + + + $this->setTrace($e->getTrace()); + } + + protected function setTrace($trace) + { + $traceReflector = new \ReflectionProperty('Exception', 'trace'); + $traceReflector->setAccessible(true); + $traceReflector->setValue($this, $trace); + } +} \ No newline at end of file diff --git a/library/think/exception/ValidateException.php b/library/think/exception/ValidateException.php new file mode 100644 index 00000000..6f1cd4d1 --- /dev/null +++ b/library/think/exception/ValidateException.php @@ -0,0 +1,32 @@ + +// +---------------------------------------------------------------------- + +namespace think\exception; + +class ValidateException extends \RuntimeException +{ + protected $error; + + public function __construct($error) + { + $this->error = $error; + } + + /** + * 获取验证错误信息 + * @access public + * @return array|string + */ + public function getError() + { + return $this->error; + } +} \ No newline at end of file diff --git a/library/think/log/alarm/Email.php b/library/think/log/alarm/Email.php deleted file mode 100644 index 0a1ac7dd..00000000 --- a/library/think/log/alarm/Email.php +++ /dev/null @@ -1,41 +0,0 @@ - -// +---------------------------------------------------------------------- - -namespace think\log\alarm; - -/** - * 邮件通知驱动 - */ -class Email -{ - - protected $config = [ - 'address' => '', - ]; - - // 实例化并传入参数 - public function __construct($config = []) - { - $this->config = array_merge($this->config, $config); - } - - /** - * 通知发送接口 - * @access public - * @param string $msg 日志信息 - * @return bool - */ - public function send($msg = '') - { - return error_log($msg, 1, $this->config['address']); - } - -} diff --git a/library/think/log/driver/File.php b/library/think/log/driver/File.php index 0f781866..bbc9b76e 100644 --- a/library/think/log/driver/File.php +++ b/library/think/log/driver/File.php @@ -45,7 +45,7 @@ class File //检测日志文件大小,超过配置大小则备份日志文件重新生成 if (is_file($destination) && floor($this->config['file_size']) <= filesize($destination)) { - rename($destination, dirname($destination) . DS . time() . '-' . basename($destination)); + rename($destination, dirname($destination) . DS . $_SERVER['REQUEST_TIME'] . '-' . basename($destination)); } // 获取基本信息 @@ -54,27 +54,26 @@ class File } else { $current_uri = "cmd:" . implode(' ', $_SERVER['argv']); } - $runtime = microtime(true) - START_TIME; + $runtime = (number_format(microtime(true), 8, '.', '') - THINK_START_TIME) ?: 0.00000001; $reqs = number_format(1 / number_format($runtime, 8), 2); - $runtime = number_format($runtime, 6); - $time_str = " [运行时间:{$runtime}s] [吞吐率:{$reqs}req/s]"; - $memory_use = number_format((memory_get_usage() - START_MEM) / 1024, 2); - $memory_str = " [内存消耗:{$memory_use}kb]"; - $file_load = " [文件加载:" . count(get_included_files()) . "]"; + $time_str = ' [运行时间:' . number_format($runtime, 6) . 's][吞吐率:' . $reqs . 'req/s]'; + $memory_use = number_format((memory_get_usage() - THINK_START_MEM) / 1024, 2); + $memory_str = ' [内存消耗:' . $memory_use . 'kb]'; + $file_load = ' [文件加载:' . count(get_included_files()) . ']'; - array_unshift($log, [ - 'type' => 'log', - 'msg' => $current_uri . $time_str . $memory_str . $file_load, - ]); - - $info = ''; - foreach ($log as $line) { - $info .= '[' . $line['type'] . '] ' . $line['msg'] . "\r\n"; + $info = '[ log ] ' . $current_uri . $time_str . $memory_str . $file_load . "\r\n"; + foreach ($log as $type => $val) { + foreach ($val as $msg) { + if (!is_string($msg)) { + $msg = var_export($msg, true); + } + $info .= '[ ' . $type . ' ] ' . $msg . "\r\n"; + } } $server = isset($_SERVER['SERVER_ADDR']) ? $_SERVER['SERVER_ADDR'] : '0.0.0.0'; $remote = isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : '0.0.0.0'; - $method = REQUEST_METHOD; + $method = isset($_SERVER['REQUEST_METHOD']) ? $_SERVER['REQUEST_METHOD'] : 'CLI'; $uri = isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : ''; return error_log("[{$now}] {$server} {$remote} {$method} {$uri}\r\n{$info}\r\n", 3, $destination); } diff --git a/library/think/log/driver/Sae.php b/library/think/log/driver/Sae.php deleted file mode 100644 index d01bdabc..00000000 --- a/library/think/log/driver/Sae.php +++ /dev/null @@ -1,72 +0,0 @@ - ' c ', - ]; - - // 实例化并传入参数 - public function __construct(array $config = []) - { - $this->config = array_merge($this->config, $config); - } - - /** - * 日志写入接口 - * @access public - * @param array $log 日志信息 - * @return bool - */ - public function save(array $log = []) - { - static $is_debug = null; - $now = date($this->config['log_time_format']); - // 获取基本信息 - if (isset($_SERVER['HTTP_HOST'])) { - $current_uri = $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI']; - } else { - $current_uri = "cmd:" . implode(' ', $_SERVER['argv']); - } - $runtime = microtime(true) - START_TIME; - $reqs = number_format(1 / number_format($runtime, 8), 2); - $runtime = number_format($runtime, 6); - $time_str = " [运行时间:{$runtime}s] [吞吐率:{$reqs}req/s]"; - $memory_use = number_format((memory_get_usage() - START_MEM) / 1024, 2); - $memory_str = " [内存消耗:{$memory_use}kb]"; - $file_load = " [文件加载:" . count(get_included_files()) . "]"; - - array_unshift($log, [ - 'type' => 'log', - 'msg' => $current_uri . $time_str . $memory_str . $file_load, - ]); - - $info = ''; - foreach ($log as $line) { - $info .= '[' . $line['type'] . '] ' . $line['msg'] . "\r\n"; - } - - $logstr = "[{$now}] {$_SERVER['SERVER_ADDR']} {$_SERVER['REMOTE_ADDR']} {$_SERVER['REQUEST_URI']}\r\n{$info}\r\n"; - if (is_null($is_debug)) { - $appSettings = []; - preg_replace_callback('@(\w+)\=([^;]*)@', function ($match) use (&$appSettings) { - $appSettings[$match['1']] = $match['2']; - }, $_SERVER['HTTP_APPCOOKIE']); - $is_debug = in_array($_SERVER['HTTP_APPVERSION'], explode(',', $appSettings['debug'])) ? true : false; - } - if ($is_debug) { - sae_set_display_errors(false); //记录日志不将日志打印出来 - } - sae_debug($logstr); - if ($is_debug) { - sae_set_display_errors(true); - } - return true; - } - -} diff --git a/library/think/log/driver/Socket.php b/library/think/log/driver/Socket.php index 88c34033..facdf0e3 100644 --- a/library/think/log/driver/Socket.php +++ b/library/think/log/driver/Socket.php @@ -20,27 +20,25 @@ class Socket public $port = 1116; //SocketLog 服务的http的端口号 protected $config = [ - 'enable' => true, //是否记录日志的开关 + // socket服务器地址 'host' => 'localhost', - //是否显示利于优化的参数,如果允许时间,消耗内存等 - 'optimize' => false, + // 是否显示加载的文件列表 'show_included_files' => false, - 'error_handler' => false, - //日志强制记录到配置的client_id + // 日志强制记录到配置的client_id 'force_client_ids' => [], - //限制允许读取日志的client_id + // 限制允许读取日志的client_id 'allow_client_ids' => [], ]; protected $css = [ - 'sql' => 'color:#009bb4;', - 'sql_warn' => 'color:#009bb4;font-size:14px;', - 'error_handler' => 'color:#f4006b;font-size:14px;', - 'page' => 'color:#40e2ff;background:#171717;', - 'big' => 'font-size:20px;color:red;', + 'sql' => 'color:#009bb4;', + 'sql_warn' => 'color:#009bb4;font-size:14px;', + 'error' => 'color:#f4006b;font-size:14px;', + 'page' => 'color:#40e2ff;background:#171717;', + 'big' => 'font-size:20px;color:red;', ]; - protected $_allowForceClientIds = []; //配置强制推送且被授权的client_id + protected $allowForceClientIds = []; //配置强制推送且被授权的client_id /** * 架构函数 @@ -52,82 +50,98 @@ class Socket if (!empty($config)) { $this->config = array_merge($this->config, $config); } - if (isset($this->config['allow_client_id'])) { - //兼容旧配置 - $this->allow_client_ids = array_merge($this->allow_client_ids, [$this->config['allow_client_id']]); - } } /** - * 日志写入接口 + * 调试输出接口 * @access public - * @param array $logs 日志信息 + * @param array $log 日志信息 * @return bool */ - public function save(array $logs = []) + public function save(array $log = []) { if (!$this->check()) { return false; } - $runtime = microtime(true) - START_TIME; + $runtime = number_format(microtime(true), 8, '.', '') - THINK_START_TIME; $reqs = number_format(1 / number_format($runtime, 8), 2); - $runtime = number_format($runtime, 6); - $time_str = " [运行时间:{$runtime}s][吞吐率:{$reqs}req/s]"; - $memory_use = number_format((memory_get_usage() - START_MEM) / 1024, 2); - $memory_str = " [内存消耗:{$memory_use}kb]"; - $file_load = " [文件加载:" . count(get_included_files()) . "]"; + $time_str = ' [运行时间:' . number_format($runtime, 6) . 's][吞吐率:' . $reqs . 'req/s]'; + $memory_use = number_format((memory_get_usage() - THINK_START_MEM) / 1024, 2); + $memory_str = ' [内存消耗:' . $memory_use . 'kb]'; + $file_load = ' [文件加载:' . count(get_included_files()) . ']'; if (isset($_SERVER['HTTP_HOST'])) { $current_uri = $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI']; } else { - $current_uri = "cmd:" . implode(' ', $_SERVER['argv']); + $current_uri = 'cmd:' . implode(' ', $_SERVER['argv']); } - array_unshift($logs, [ + // 基本信息 + $trace[] = [ 'type' => 'group', 'msg' => $current_uri . $time_str . $memory_str . $file_load, 'css' => $this->css['page'], - ]); - - $logs[] = [ - 'type' => 'groupCollapsed', - 'msg' => 'included_files', - 'css' => '', - ]; - $logs[] = [ - 'type' => 'log', - 'msg' => implode("\n", get_included_files()), - 'css' => '', - ]; - $logs[] = [ - 'type' => 'groupEnd', - 'msg' => '', - 'css' => '', ]; - $logs[] = [ - 'type' => 'groupEnd', - 'msg' => '', - 'css' => '', - ]; - - foreach ($logs as &$log) { - if (in_array($log['type'], ['sql', 'notice', 'debug', 'info'])) { - $log['type'] = 'log'; + foreach ($log as $type => $val) { + $trace[] = [ + 'type' => 'groupCollapsed', + 'msg' => '[ ' . $type . ' ]', + 'css' => isset($this->css[$type]) ? $this->css[$type] : '', + ]; + foreach ($val as $msg) { + if (!is_string($msg)) { + $msg = var_export($msg, true); + } + $trace[] = [ + 'type' => 'log', + 'msg' => $msg, + 'css' => '', + ]; } + $trace[] = [ + 'type' => 'groupEnd', + 'msg' => '', + 'css' => '', + ]; } + + if ($this->config['show_included_files']) { + $trace[] = [ + 'type' => 'groupCollapsed', + 'msg' => '[ file ]', + 'css' => '', + ]; + $trace[] = [ + 'type' => 'log', + 'msg' => implode("\n", get_included_files()), + 'css' => '', + ]; + $trace[] = [ + 'type' => 'groupEnd', + 'msg' => '', + 'css' => '', + ]; + } + + $trace[] = [ + 'type' => 'groupEnd', + 'msg' => '', + 'css' => '', + ]; + $tabid = $this->getClientArg('tabid'); if (!$client_id = $this->getClientArg('client_id')) { $client_id = ''; } - if (!empty($this->_allowForceClientIds)) { + if (!empty($this->allowForceClientIds)) { //强制推送到多个client_id - foreach ($this->_allowForceClientIds as $force_client_id) { + foreach ($this->allowForceClientIds as $force_client_id) { $client_id = $force_client_id; - $this->sendToClient($tabid, $client_id, $logs, $force_client_id); + $this->sendToClient($tabid, $client_id, $trace, $force_client_id); } } else { - $this->sendToClient($tabid, $client_id, $logs, ''); + $this->sendToClient($tabid, $client_id, $trace, ''); } return true; } @@ -142,12 +156,12 @@ class Socket */ protected function sendToClient($tabid, $client_id, $logs, $force_client_id) { - $logs = array( + $logs = [ 'tabid' => $tabid, 'client_id' => $client_id, 'logs' => $logs, 'force_client_id' => $force_client_id, - ); + ]; $msg = @json_encode($logs); $address = '/' . $client_id; //将client_id作为地址, server端通过地址判断将日志发布给谁 $this->send($this->config['host'], $msg, $address); @@ -155,9 +169,6 @@ class Socket protected function check() { - if (!$this->config['enable']) { - return false; - } $tabid = $this->getClientArg('tabid'); //是否记录日志的检查 if (!$tabid && !$this->config['force_client_ids']) { @@ -167,8 +178,8 @@ class Socket $allow_client_ids = $this->config['allow_client_ids']; if (!empty($allow_client_ids)) { //通过数组交集得出授权强制推送的client_id - $this->_allowForceClientIds = array_intersect($allow_client_ids, $this->config['force_client_ids']); - if (!$tabid && count($this->_allowForceClientIds)) { + $this->allowForceClientIds = array_intersect($allow_client_ids, $this->config['force_client_ids']); + if (!$tabid && count($this->allowForceClientIds)) { return true; } @@ -177,7 +188,7 @@ class Socket return false; } } else { - $this->_allowForceClientIds = $this->config['force_client_ids']; + $this->allowForceClientIds = $this->config['force_client_ids']; } return true; } @@ -209,7 +220,7 @@ class Socket } /** - * @param null $host - $host of socket server + * @param string $host - $host of socket server * @param string $message - 发送的消息 * @param string $address - 地址 * @return bool diff --git a/library/think/model/Merge.php b/library/think/model/Merge.php index 5384bb64..1bf634f4 100644 --- a/library/think/model/Merge.php +++ b/library/think/model/Merge.php @@ -39,12 +39,12 @@ class Merge extends Model /** * 查找单条记录 * @access public - * @param mixed $data 主键值或者查询条件(闭包) - * @param string $with 关联预查询 - * @param bool $cache 是否缓存 + * @param mixed $data 主键值或者查询条件(闭包) + * @param string $with 关联预查询 + * @param bool $cache 是否缓存 * @return \think\Model */ - public static function get($data = '', $with = [], $cache = false) + public static function get($data = null, $with = [], $cache = false) { $query = self::parseQuery($data, $with, $cache); $query = self::attachQuery($query); @@ -59,16 +59,16 @@ class Merge extends Model */ protected static function attachQuery($query) { - $master = basename(str_replace('\\', '/', get_called_class())); $class = new static(); - $fields = self::getModelField($master, '', $class->mapFields); + $master = $class->name; + $fields = self::getModelField($query, $master, '', $class->mapFields); $query->alias($master)->field($fields); foreach (static::$relationModel as $key => $model) { $name = is_int($key) ? $model : $key; - $table = is_int($key) ? self::db()->name($name)->getTable() : $model; + $table = is_int($key) ? $query->getTable($name) : $model; $query->join($table . ' ' . $name, $name . '.' . $class->fk . '=' . $master . '.' . $class->getPk()); - $fields = self::getModelField($name, $table, $class->mapFields); + $fields = self::getModelField($query, $name, $table, $class->mapFields); $query->field($fields); } return $query; @@ -77,15 +77,16 @@ class Merge extends Model /** * 获取关联模型的字段 并解决混淆 * @access protected - * @param string $name 模型名称 - * @param string $table 关联表名称 - * @param array $map 字段映射 + * @param \think\db\Query $query 查询对象 + * @param string $name 模型名称 + * @param string $table 关联表名称 + * @param array $map 字段映射 * @return array */ - protected static function getModelField($name, $table = '', $map = []) + protected static function getModelField($query, $name, $table = '', $map = []) { // 获取模型的字段信息 - $fields = self::db()->getTableInfo($table, 'fields'); + $fields = $query->getTableInfo($table, 'fields'); $array = []; foreach ($fields as $field) { if ($key = array_search($name . '.' . $field, $map)) { @@ -101,11 +102,11 @@ class Merge extends Model /** * 查找所有记录 * @access public - * @param mixed $data 主键列表或者查询条件(闭包) - * @param string $with 关联预查询 + * @param mixed $data 主键列表或者查询条件(闭包) + * @param string $with 关联预查询 * @return array|false|string */ - public static function all($data = [], $with = [], $cache = false) + public static function all($data = null, $with = [], $cache = false) { $query = self::parseQuery($data, $with, $cache); $query = self::attachQuery($query); @@ -115,9 +116,9 @@ class Merge extends Model /** * 处理写入的模型数据 * @access public - * @param string $model 模型名称 - * @param array $data 数据 - * @param bool $insert 是否新增 + * @param string $model 模型名称 + * @param array $data 数据 + * @param bool $insert 是否新增 * @return void */ protected function parseData($model, $data, $insert = false) @@ -141,32 +142,38 @@ class Merge extends Model /** * 保存模型数据 以及关联数据 * @access public - * @param mixed $data 数据 - * @param array $where 更新条件 - * @param bool $getId 新增的时候是否获取id + * @param mixed $data 数据 + * @param array $where 更新条件 + * @param bool $getId 新增的时候是否获取id + * @param bool $replace 是否replace * @return mixed */ - public function save($data = [], $where = [], $getId = true) + public function save($data = [], $where = [], $getId = true, $replace = false) { if (!empty($data)) { + // 数据自动验证 + if (!$this->validateData($data)) { + return false; + } // 数据对象赋值 foreach ($data as $key => $value) { - $this->__set($key, $value); + $this->setAttr($key, $value); } if (!empty($where)) { $this->isUpdate = true; } } - // 数据自动验证 - if (!$this->validateData()) { - return false; - } + // 数据自动完成 $this->autoCompleteData($this->auto); - // 处理模型数据 - $data = $this->parseData($this->name, $this->data); - self::db()->startTrans(); + // 自动写入更新时间 + if ($this->autoWriteTimestamp && $this->updateTime) { + $this->setAttr($this->updateTime, null); + } + + $db = $this->db(); + $db->startTrans(); try { if ($this->isUpdate) { // 自动写入 @@ -176,16 +183,30 @@ class Merge extends Model return false; } + if (empty($where) && !empty($this->updateWhere)) { + $where = $this->updateWhere; + } + + if (!empty($where)) { + $pk = $this->getPk(); + if (is_string($pk) && isset($data[$pk])) { + unset($data[$pk]); + } + } + + // 处理模型数据 + $data = $this->parseData($this->name, $this->data); // 写入主表数据 - $result = self::db()->strict(false)->update($data); + $result = $db->strict(false)->where($where)->update($data); // 写入附表数据 foreach (static::$relationModel as $key => $model) { $name = is_int($key) ? $model : $key; - $table = is_int($key) ? self::db()->name($model)->getTable() : $model; + $table = is_int($key) ? $db->getTable($model) : $model; // 处理关联模型数据 - $data = $this->parseData($name, $this->data); - self::db()->table($table)->strict(false)->where($this->fk, $this->data[$this->getPk()])->update($data); + $data = $this->parseData($name, $this->data); + $query = clone $db; + $query->table($table)->strict(false)->where($this->fk, $this->data[$this->getPk()])->update($data); } // 新增回调 $this->trigger('after_update', $this); @@ -193,35 +214,43 @@ class Merge extends Model // 自动写入 $this->autoCompleteData($this->insert); + // 自动写入创建时间 + if ($this->autoWriteTimestamp && $this->createTime) { + $this->setAttr($this->createTime, null); + } + if (false === $this->trigger('before_insert', $this)) { return false; } + // 处理模型数据 + $data = $this->parseData($this->name, $this->data, true); // 写入主表数据 - $result = self::db()->name($this->name)->strict(false)->insert($this->data); + $result = $db->name($this->name)->strict(false)->insert($data, $replace); if ($result) { - $insertId = self::db()->getLastInsID(); + $insertId = $db->getLastInsID(); // 写入外键数据 $this->data[$this->fk] = $insertId; // 写入附表数据 foreach (static::$relationModel as $key => $model) { $name = is_int($key) ? $model : $key; - $table = is_int($key) ? self::db()->name($model)->getTable() : $model; + $table = is_int($key) ? $db->getTable($model) : $model; // 处理关联模型数据 - $data = $this->parseData($name, $this->data, true); - self::db()->table($table)->strict(false)->insert($data); + $data = $this->parseData($name, $this->data, true); + $query = clone $db; + $query->table($table)->strict(false)->insert($data); } $result = $insertId; } // 新增回调 $this->trigger('after_insert', $this); } - self::db()->commit(); + $db->commit(); return $result; - } catch (\PDOException $e) { - self::db()->rollback(); - return false; + } catch (\Exception $e) { + $db->rollback(); + throw $e; } } @@ -235,25 +264,28 @@ class Merge extends Model if (false === $this->trigger('before_delete', $this)) { return false; } - self::db()->startTrans(); + + $db = $this->db(); + $db->startTrans(); try { - $result = self::db()->delete($this->data); + $result = $db->delete($this->data); if ($result) { // 获取主键数据 $pk = $this->data[$this->getPk()]; // 删除关联数据 foreach (static::$relationModel as $key => $model) { - $table = is_int($key) ? self::db()->name($model)->getTable() : $model; - self::db()->table($table)->where($this->fk, $pk)->delete(); + $table = is_int($key) ? $db->getTable($model) : $model; + $query = clone $db; + $query->table($table)->where($this->fk, $pk)->delete(); } } $this->trigger('after_delete', $this); - self::db()->commit(); + $db->commit(); return $result; - } catch (\PDOException $e) { - self::db()->rollback(); - return false; + } catch (\Exception $e) { + $db->rollback(); + throw $e; } } diff --git a/library/think/model/Relation.php b/library/think/model/Relation.php index 654a7b9b..515bf362 100644 --- a/library/think/model/Relation.php +++ b/library/think/model/Relation.php @@ -19,10 +19,11 @@ use think\model\Pivot; class Relation { - const HAS_ONE = 1; - const HAS_MANY = 2; - const BELONGS_TO = 3; - const BELONGS_TO_MANY = 4; + const HAS_ONE = 1; + const HAS_MANY = 2; + const HAS_MANY_THROUGH = 5; + const BELONGS_TO = 3; + const BELONGS_TO_MANY = 4; // 父模型对象 protected $parent; @@ -32,17 +33,25 @@ class Relation protected $middle; // 当前关联类型 protected $type; - // 关联外键 + // 关联表外键 protected $foreignKey; - // 关联键 + // 中间关联表外键 + protected $throughKey; + // 关联表主键 protected $localKey; + // 数据表别名 + protected $alias; + // 当前关联的JOIN类型 + protected $joinType; + // 关联模型查询对象 + protected $query; /** * 架构函数 * @access public - * @param \think\Model $model 上级模型对象 + * @param Model $model 上级模型对象 */ - public function __construct($model) + public function __construct(Model $model) { $this->parent = $model; } @@ -61,6 +70,8 @@ class Relation 'middle' => $this->middle, 'foreignKey' => $this->foreignKey, 'localKey' => $this->localKey, + 'alias' => $this->alias, + 'joinType' => $this->joinType, ]; return $name ? $info[$name] : $info; } @@ -72,6 +83,7 @@ class Relation $relation = $this->parent->$name(); $foreignKey = $this->foreignKey; $localKey = $this->localKey; + // 判断关联类型执行查询 switch ($this->type) { case self::HAS_ONE: @@ -81,13 +93,16 @@ class Relation $result = $relation->where($localKey, $this->parent->$foreignKey)->find(); break; case self::HAS_MANY: - $result = $relation->where($foreignKey, $this->parent->$localKey)->select(); + $result = $relation->select(); + break; + case self::HAS_MANY_THROUGH: + $result = $relation->select(); break; case self::BELONGS_TO_MANY: // 关联查询 - $pk = $this->parent->getPk(); - $condition['pivot.' . $foreignKey] = $this->parent->$pk; - $result = $this->belongsToManyQuery($relation, $this->middle, $foreignKey, $localKey, $condition)->select(); + $pk = $this->parent->getPk(); + $condition['pivot.' . $localKey] = $this->parent->$pk; + $result = $this->belongsToManyQuery($relation, $this->middle, $foreignKey, $localKey, $condition)->select(); foreach ($result as $set) { $pivot = []; foreach ($set->toArray() as $key => $val) { @@ -112,9 +127,9 @@ class Relation /** * 预载入关联查询 返回数据集 * @access public - * @param array $resultSet 数据集 - * @param string $relation 关联名 - * @param string $class 数据集对象名 为空表示数组 + * @param array $resultSet 数据集 + * @param string $relation 关联名 + * @param string $class 数据集对象名 为空表示数组 * @return array */ public function eagerlyResultSet($resultSet, $relation, $class = '') @@ -167,7 +182,7 @@ class Relation if (!isset($data[$result->$localKey])) { $data[$result->$localKey] = []; } - $result->__set($relation, $this->resultSetBuild($data[$result->$localKey], $class)); + $result->setAttr($relation, $this->resultSetBuild($data[$result->$localKey], $class)); } } break; @@ -184,7 +199,7 @@ class Relation if (!empty($range)) { // 查询关联数据 $data = $this->eagerlyManyToMany($model, [ - 'pivot.' . $foreignKey => [ + 'pivot.' . $localKey => [ 'in', $range, ], @@ -196,7 +211,7 @@ class Relation $data[$result->$pk] = []; } - $result->__set($relation, $this->resultSetBuild($data[$result->$pk], $class)); + $result->setAttr($relation, $this->resultSetBuild($data[$result->$pk], $class)); } } break; @@ -208,8 +223,8 @@ class Relation /** * 封装关联数据集 * @access public - * @param array $resultSet 数据集 - * @param string $class 数据集类名 + * @param array $resultSet 数据集 + * @param string $class 数据集类名 * @return mixed */ protected function resultSetBuild($resultSet, $class = '') @@ -220,10 +235,10 @@ class Relation /** * 预载入关联查询 返回模型对象 * @access public - * @param Model $result 数据对象 - * @param string $relation 关联名 - * @param string $class 数据集对象名 为空表示数组 - * @return \think\Model + * @param Model $result 数据对象 + * @param string $relation 关联名 + * @param string $class 数据集对象名 为空表示数组 + * @return Model */ public function eagerlyResult($result, $relation, $class = '') { @@ -256,7 +271,7 @@ class Relation if (!isset($data[$result->$localKey])) { $data[$result->$localKey] = []; } - $result->__set($relation, $this->resultSetBuild($data[$result->$localKey], $class)); + $result->setAttr($relation, $this->resultSetBuild($data[$result->$localKey], $class)); } break; case self::BELONGS_TO_MANY: @@ -264,13 +279,13 @@ class Relation if (isset($result->$pk)) { $pk = $result->$pk; // 查询管理数据 - $data = $this->eagerlyManyToMany($model, ['pivot.' . $foreignKey => $pk], $relation, $subRelation); + $data = $this->eagerlyManyToMany($model, ['pivot.' . $localKey => $pk], $relation, $subRelation); // 关联数据封装 if (!isset($data[$pk])) { $data[$pk] = []; } - $result->__set($relation, $this->resultSetBuild($data[$pk], $class)); + $result->setAttr($relation, $this->resultSetBuild($data[$pk], $class)); } break; @@ -282,40 +297,39 @@ class Relation /** * 一对一 关联模型预查询拼装 * @access public - * @param string $model 模型名称 - * @param string $relation 关联名 - * @param Model $result 模型对象实例 + * @param string $model 模型名称 + * @param string $relation 关联名 + * @param Model $result 模型对象实例 * @return void */ protected function match($model, $relation, &$result) { - $modelName = Loader::parseName(basename(str_replace('\\', '/', $model))); // 重新组装模型数据 foreach ($result->toArray() as $key => $val) { if (strpos($key, '__')) { list($name, $attr) = explode('__', $key, 2); - if ($name == $modelName) { + if ($name == $relation) { $list[$name][$attr] = $val; unset($result->$key); } } } - if (!isset($list[$modelName])) { + if (!isset($list[$relation])) { // 设置关联模型属性 - $list[$modelName] = []; + $list[$relation] = []; } - $result->__set($relation, new $model($list[$modelName])); + $result->setAttr($relation, (new $model($list[$relation]))->isUpdate(true)); } /** * 一对多 关联模型预查询 * @access public - * @param object $model 关联模型对象 - * @param array $where 关联预查询条件 - * @param string $relation 关联名 - * @param string $subRelation 子关联 - * @param bool $closure + * @param object $model 关联模型对象 + * @param array $where 关联预查询条件 + * @param string $relation 关联名 + * @param string $subRelation 子关联 + * @param bool $closure * @return array */ protected function eagerlyOneToMany($model, $where, $relation, $subRelation = '', $closure = false) @@ -335,10 +349,10 @@ class Relation /** * 多对多 关联模型预查询 * @access public - * @param object $model 关联模型对象 - * @param array $where 关联预查询条件 - * @param string $relation 关联名 - * @param string $subRelation 子关联 + * @param object $model 关联模型对象 + * @param array $where 关联预查询条件 + * @param string $relation 关联名 + * @param string $subRelation 子关联 * @return array */ protected function eagerlyManyToMany($model, $where, $relation, $subRelation = '') @@ -362,26 +376,42 @@ class Relation } } $set->pivot = new Pivot($pivot, $this->middle); - $data[$set->$foreignKey][] = $set; + $data[$pivot[$localKey]][] = $set; } return $data; } + /** + * 设置当前关联定义的数据表别名 + * @access public + * @param array $alias 别名定义 + * @return $this + */ + public function setAlias($alias) + { + $this->alias = $alias; + return $this; + } + /** * HAS ONE 关联定义 * @access public * @param string $model 模型名 * @param string $foreignKey 关联外键 * @param string $localKey 关联主键 + * @param array $alias 别名定义 + * @param string $joinType JOIN类型 * @return $this */ - public function hasOne($model, $foreignKey, $localKey) + public function hasOne($model, $foreignKey, $localKey, $alias = [], $joinType = 'INNER') { $this->type = self::HAS_ONE; $this->model = $model; $this->foreignKey = $foreignKey; $this->localKey = $localKey; - + $this->alias = $alias; + $this->joinType = $joinType; + $this->query = (new $model)->db(); // 返回关联的模型对象 return $this; } @@ -392,16 +422,20 @@ class Relation * @param string $model 模型名 * @param string $foreignKey 关联外键 * @param string $otherKey 关联主键 + * @param array $alias 别名定义 + * @param string $joinType JOIN类型 * @return $this */ - public function belongsTo($model, $foreignKey, $otherKey) + public function belongsTo($model, $foreignKey, $otherKey, $alias = [], $joinType = 'INNER') { // 记录当前关联信息 $this->type = self::BELONGS_TO; $this->model = $model; $this->foreignKey = $foreignKey; $this->localKey = $otherKey; - + $this->alias = $alias; + $this->joinType = $joinType; + $this->query = (new $model)->db(); // 返回关联的模型对象 return $this; } @@ -412,16 +446,44 @@ class Relation * @param string $model 模型名 * @param string $foreignKey 关联外键 * @param string $localKey 关联主键 + * @param array $alias 别名定义 * @return $this */ - public function hasMany($model, $foreignKey, $localKey) + public function hasMany($model, $foreignKey, $localKey, $alias) { // 记录当前关联信息 $this->type = self::HAS_MANY; $this->model = $model; $this->foreignKey = $foreignKey; $this->localKey = $localKey; + $this->alias = $alias; + $this->query = (new $model)->db(); + // 返回关联的模型对象 + return $this; + } + /** + * HAS MANY 远程关联定义 + * @access public + * @param string $model 模型名 + * @param string $through 中间模型名 + * @param string $firstkey 关联外键 + * @param string $secondKey 关联外键 + * @param string $localKey 关联主键 + * @param array $alias 别名定义 + * @return $this + */ + public function hasManyThrough($model, $through, $foreignKey, $throughKey, $localKey, $alias) + { + // 记录当前关联信息 + $this->type = self::HAS_MANY_THROUGH; + $this->model = $model; + $this->middle = $through; + $this->foreignKey = $foreignKey; + $this->throughKey = $throughKey; + $this->localKey = $localKey; + $this->alias = $alias; + $this->query = (new $model)->db(); // 返回关联的模型对象 return $this; } @@ -433,9 +495,10 @@ class Relation * @param string $table 中间表名 * @param string $foreignKey 关联模型外键 * @param string $localKey 当前模型关联键 + * @param array $alias 别名定义 * @return $this */ - public function belongsToMany($model, $table, $foreignKey, $localKey) + public function belongsToMany($model, $table, $foreignKey, $localKey, $alias) { // 记录当前关联信息 $this->type = self::BELONGS_TO_MANY; @@ -443,7 +506,8 @@ class Relation $this->foreignKey = $foreignKey; $this->localKey = $localKey; $this->middle = $table; - + $this->alias = $alias; + $this->query = (new $model)->db(); // 返回关联的模型对象 return $this; } @@ -451,11 +515,11 @@ class Relation /** * BELONGS TO MANY 关联查询 * @access public - * @param object $model 关联模型对象 - * @param string $table 中间表名 - * @param string $foreignKey 关联模型关联键 - * @param string $localKey 当前模型关联键 - * @param array $condition 关联查询条件 + * @param object $model 关联模型对象 + * @param string $table 中间表名 + * @param string $foreignKey 关联模型关联键 + * @param string $localKey 当前模型关联键 + * @param array $condition 关联查询条件 * @return \think\db\Query|string */ protected function belongsToManyQuery($model, $table, $foreignKey, $localKey, $condition = []) @@ -472,8 +536,8 @@ class Relation /** * 保存(新增)当前关联数据对象 * @access public - * @param mixed $data 数据 可以使用数组 关联模型对象 和 关联对象的主键 - * @param array $pivot 中间表额外数据 + * @param mixed $data 数据 可以使用数组 关联模型对象 和 关联对象的主键 + * @param array $pivot 中间表额外数据 * @return integer */ public function save($data, array $pivot = []) @@ -499,8 +563,8 @@ class Relation /** * 批量保存当前关联数据对象 * @access public - * @param array $dataSet 数据集 - * @param array $pivot 中间表额外数据 + * @param array $dataSet 数据集 + * @param array $pivot 中间表额外数据 * @return integer */ public function saveAll(array $dataSet, array $pivot = []) @@ -525,8 +589,8 @@ class Relation /** * 附加关联的一个中间表数据 * @access public - * @param mixed $data 数据 可以使用数组、关联模型对象 或者 关联对象的主键 - * @param array $pivot 中间表额外数据 + * @param mixed $data 数据 可以使用数组、关联模型对象 或者 关联对象的主键 + * @param array $pivot 中间表额外数据 * @return integer */ public function attach($data, $pivot = []) @@ -534,10 +598,8 @@ class Relation if (is_array($data)) { // 保存关联表数据 $model = new $this->model; - $model->save($data); - $relationFk = $model->getPk(); - $id = $model->$relationFk; - } elseif (is_int($data)) { + $id = $model->save($data); + } elseif (is_numeric($data)) { // 根据关联表主键直接写入中间表 $id = $data; } elseif ($data instanceof Model) { @@ -551,24 +613,25 @@ class Relation $pk = $this->parent->getPk(); $pivot[$this->localKey] = $this->parent->$pk; $pivot[$this->foreignKey] = $id; - return Db::table($this->middle)->insert($pivot); + $query = clone $this->parent->db(); + return $query->table($this->middle)->insert($pivot); } else { - throw new Exception(' miss relation data'); + throw new Exception('miss relation data'); } } /** * 解除关联的一个中间表数据 * @access public - * @param integer|array $data 数据 可以使用关联对象的主键 - * @param bool $relationDel 是否同时删除关联表数据 + * @param integer|array $data 数据 可以使用关联对象的主键 + * @param bool $relationDel 是否同时删除关联表数据 * @return integer */ public function detach($data, $relationDel = false) { if (is_array($data)) { $id = $data; - } elseif (is_int($data)) { + } elseif (is_numeric($data)) { // 根据关联表主键直接写入中间表 $id = $data; } elseif ($data instanceof Model) { @@ -580,7 +643,8 @@ class Relation $pk = $this->parent->getPk(); $pivot[$this->localKey] = $this->parent->$pk; $pivot[$this->foreignKey] = is_array($id) ? ['in', $id] : $id; - Db::table($this->middle)->where($pivot)->delete(); + $query = clone $this->parent->db(); + $query->table($this->middle)->where($pivot)->delete(); // 删除关联表数据 if ($relationDel) { @@ -591,16 +655,36 @@ class Relation public function __call($method, $args) { - if ($this->model) { - $model = new $this->model; - $db = $model->db(); - if (self::HAS_MANY == $this->type && isset($this->parent->{$this->localKey})) { - // 关联查询带入关联条件 - $db->where($this->foreignKey, $this->parent->{$this->localKey}); + if ($this->query) { + switch ($this->type) { + case self::HAS_MANY: + if (isset($this->parent->{$this->localKey})) { + // 关联查询带入关联条件 + $this->query->where($this->foreignKey, $this->parent->{$this->localKey}); + } + break; + case self::HAS_MANY_THROUGH: + $through = $this->middle; + $model = $this->model; + $alias = Loader::parseName(basename(str_replace('\\', '/', $model))); + $throughTable = $through::getTable(); + $pk = (new $this->model)->getPk(); + $throughKey = $this->throughKey; + $modelTable = $this->parent->getTable(); + $result = $this->query->field($alias . '.*')->alias($alias) + ->join($throughTable, $throughTable . '.' . $pk . '=' . $alias . '.' . $throughKey) + ->join($modelTable, $modelTable . '.' . $this->localKey . '=' . $throughTable . '.' . $this->foreignKey) + ->where($throughTable . '.' . $this->foreignKey, $this->parent->{$this->localKey}); + break; + } + $result = call_user_func_array([$this->query, $method], $args); + if ($result instanceof \think\db\Query) { + return $this; + } else { + return $result; } - return call_user_func_array([$db, $method], $args); } else { - throw new Exception(__CLASS__ . ':' . $method . ' method not exist'); + throw new Exception('method not exists:' . __CLASS__ . '->' . $method); } } diff --git a/library/think/paginator/Collection.php b/library/think/paginator/Collection.php index a7939a86..82e88272 100644 --- a/library/think/paginator/Collection.php +++ b/library/think/paginator/Collection.php @@ -23,6 +23,8 @@ use think\Paginator; * @method string render() * @method Paginator fragment($fragment) * @method Paginator appends($key, $value) + * @method integer lastPage() + * @method boolean hasPages() */ class Collection extends \think\Collection { @@ -32,9 +34,6 @@ class Collection extends \think\Collection public function __construct($items = [], Paginator $paginator = null) { - if (!$paginator instanceof Paginator) { - throw new \RuntimeException('Paginator Required!'); - } $this->paginator = $paginator; parent::__construct($items); } @@ -43,30 +42,33 @@ class Collection extends \think\Collection { return new static($items, $paginator); } - - + public function toArray() { - try { - $total = $this->total(); - } catch (Exception $e) { - $total = null; - } + if ($this->paginator) { + try { + $total = $this->total(); + } catch (Exception $e) { + $total = null; + } - return [ - 'total' => $total, - 'per_page' => $this->listRows(), - 'current_page' => $this->currentPage(), - 'data' => parent::toArray() - ]; + return [ + 'total' => $total, + 'per_page' => $this->listRows(), + 'current_page' => $this->currentPage(), + 'data' => parent::toArray() + ]; + } else { + return parent::toArray(); + } } public function __call($method, $args) { - if (method_exists($this->paginator, $method)) { + if ($this->paginator && method_exists($this->paginator, $method)) { return call_user_func_array([$this->paginator, $method], $args); } else { - throw new Exception(__CLASS__ . ':' . $method . ' method not exist'); + throw new Exception('method not exists:' . __CLASS__ . '->' . $method); } } } \ No newline at end of file diff --git a/library/think/paginator/driver/Bootstrap.php b/library/think/paginator/driver/Bootstrap.php index be65ecfc..87e1fe48 100644 --- a/library/think/paginator/driver/Bootstrap.php +++ b/library/think/paginator/driver/Bootstrap.php @@ -66,23 +66,23 @@ class Bootstrap extends Paginator 'last' => null ]; - $length = 3; + $side = 3; + $window = $side * 2; - if ($this->lastPage < $length * 4) { + if ($this->lastPage < $window + 6) { $block['first'] = $this->getUrlRange(1, $this->lastPage); - } elseif ($this->currentPage <= $length * 2) { - $block['first'] = $this->getUrlRange(1, $length * 2 + 2); + } elseif ($this->currentPage <= $window) { + $block['first'] = $this->getUrlRange(1, $window + 2); $block['last'] = $this->getUrlRange($this->lastPage - 1, $this->lastPage); - } elseif ($this->currentPage > ($this->lastPage - $length * 2)) { + } elseif ($this->currentPage > ($this->lastPage - $window)) { $block['first'] = $this->getUrlRange(1, 2); - $block['last'] = $this->getUrlRange($this->lastPage - $length * 2 + 2, $this->lastPage); + $block['last'] = $this->getUrlRange($this->lastPage - ($window + 2), $this->lastPage); } else { $block['first'] = $this->getUrlRange(1, 2); - $block['slider'] = $this->getUrlRange($this->currentPage - $length, $this->currentPage + $length); + $block['slider'] = $this->getUrlRange($this->currentPage - $side, $this->currentPage + $side); $block['last'] = $this->getUrlRange($this->lastPage - 1, $this->lastPage); } - $html = ''; if (is_array($block['first'])) { @@ -132,7 +132,7 @@ class Bootstrap extends Paginator * 生成一个可点击的按钮 * * @param string $url - * @param int $page + * @param int $page * @return string */ protected function getAvailablePageWrapper($url, $page) @@ -193,7 +193,7 @@ class Bootstrap extends Paginator * 生成普通页码按钮 * * @param string $url - * @param int $page + * @param int $page * @return string */ protected function getPageLinkWrapper($url, $page) diff --git a/library/think/response/Json.php b/library/think/response/Json.php new file mode 100644 index 00000000..a137f453 --- /dev/null +++ b/library/think/response/Json.php @@ -0,0 +1,43 @@ + +// +---------------------------------------------------------------------- + +namespace think\response; + +use think\Response; + +class Json extends Response +{ + // 输出参数 + protected $options = [ + 'json_encode_param' => JSON_UNESCAPED_UNICODE, + ]; + + protected $contentType = 'application/json'; + + /** + * 处理数据 + * @access protected + * @param mixed $data 要处理的数据 + * @return mixed + */ + protected function output($data) + { + // 返回JSON数据格式到客户端 包含状态信息 + $data = json_encode($data, $this->options['json_encode_param']); + + if ($data === false) { + throw new \InvalidArgumentException(json_last_error_msg()); + } + + return $data; + } + +} diff --git a/library/think/response/Jsonp.php b/library/think/response/Jsonp.php new file mode 100644 index 00000000..fda1183a --- /dev/null +++ b/library/think/response/Jsonp.php @@ -0,0 +1,50 @@ + +// +---------------------------------------------------------------------- + +namespace think\response; + +use think\Request; +use think\Response; + +class Jsonp extends Response +{ + // 输出参数 + protected $options = [ + 'var_jsonp_handler' => 'callback', + 'default_jsonp_handler' => 'jsonpReturn', + 'json_encode_param' => JSON_UNESCAPED_UNICODE, + ]; + + protected $contentType = 'application/javascript'; + + /** + * 处理数据 + * @access protected + * @param mixed $data 要处理的数据 + * @return mixed + */ + protected function output($data) + { + // 返回JSON数据格式到客户端 包含状态信息 [当url_common_param为false时是无法获取到$_GET的数据的,故使用Request来获取] + $var_jsonp_handler = Request::instance()->param($this->options['var_jsonp_handler'], ""); + $handler = !empty($var_jsonp_handler) ? $var_jsonp_handler : $this->options['default_jsonp_handler']; + + $data = json_encode($data, $this->options['json_encode_param']); + + if ($data === false) { + throw new \InvalidArgumentException(json_last_error_msg()); + } + + $data = $handler . '(' . $data . ');'; + return $data; + } + +} diff --git a/library/think/response/Redirect.php b/library/think/response/Redirect.php new file mode 100644 index 00000000..ae6084c4 --- /dev/null +++ b/library/think/response/Redirect.php @@ -0,0 +1,78 @@ + +// +---------------------------------------------------------------------- + +namespace think\response; + +use think\Request; +use think\Response; +use think\Session; +use think\Url; + +class Redirect extends Response +{ + + protected $options = []; + + // URL参数 + protected $params = []; + + public function __construct($data = '', $code = 302, array $header = [], array $options = []) + { + parent::__construct($data, $code, $header, $options); + $this->cacheControl('no-cache,must-revalidate'); + } + + /** + * 处理数据 + * @access protected + * @param mixed $data 要处理的数据 + * @return mixed + */ + protected function output($data) + { + $this->header['Location'] = $this->getTargetUrl(); + return; + } + + /** + * 获取跳转地址 + * @return string + */ + public function getTargetUrl() + { + return preg_match('/^(https?:|\/)/', $this->data) ? $this->data : Url::build($this->data, $this->params); + } + + public function params($params = []) + { + $this->params = $params; + return $this; + } + + /** + * 记住当前url后跳转 + */ + public function remember() + { + Session::set('redirect_url', Request::instance()->url()); + } + + /** + * 跳转到上次记住的url + */ + public function restore() + { + if (Session::has('redirect_url')) { + $this->data = Session::get('redirect_url'); + Session::delete('redirect_url'); + } + } +} diff --git a/library/think/response/View.php b/library/think/response/View.php new file mode 100644 index 00000000..224e3b60 --- /dev/null +++ b/library/think/response/View.php @@ -0,0 +1,101 @@ + +// +---------------------------------------------------------------------- + +namespace think\response; + +use think\Config; +use think\Response; +use think\View as ViewTemplate; + +class View extends Response +{ + // 输出参数 + protected $options = []; + protected $vars = []; + protected $replace = []; + protected $contentType = 'text/html'; + + /** + * 处理数据 + * @access protected + * @param mixed $data 要处理的数据 + * @return mixed + */ + protected function output($data) + { + // 渲染模板输出 + return ViewTemplate::instance(Config::get('template'), Config::get('view_replace_str')) + ->fetch($data, $this->vars, $this->replace); + } + + /** + * 视图变量赋值 + * @access public + * @param array $vars 模板变量 + * @return $this + */ + public function vars($vars = []) + { + $this->vars = $vars; + return $this; + } + + /** + * 获取视图变量 + * @access public + * @param string $name 模板变量 + * @return mixed + */ + public function getVars($name = null) + { + if(is_null($name)){ + return $this->vars; + }else{ + return isset($this->vars[$name]) ? $this->vars[$name] : null; + } + } + + /** + * 模板变量赋值 + * @access public + * @param mixed $name 变量名 + * @param mixed $value 变量值 + * @return $this + */ + public function assign($name, $value = '') + { + if (is_array($name)) { + $this->vars = array_merge($this->vars, $name); + return $this; + } else { + $this->vars[$name] = $value; + } + return $this; + } + + /** + * 视图内容替换 + * @access public + * @param string|array $content 被替换内容(支持批量替换) + * @param string $replace 替换内容 + * @return $this + */ + public function replace($content, $replace = '') + { + if (is_array($content)) { + $this->replace = array_merge($this->replace, $content); + } else { + $this->replace[$content] = $replace; + } + return $this; + } + +} diff --git a/library/think/response/Xml.php b/library/think/response/Xml.php new file mode 100644 index 00000000..3a2c59bd --- /dev/null +++ b/library/think/response/Xml.php @@ -0,0 +1,95 @@ + +// +---------------------------------------------------------------------- + +namespace think\response; + +use think\Response; + +class Xml extends Response +{ + // 输出参数 + protected $options = [ + // 根节点名 + 'root_node' => 'think', + // 根节点属性 + 'root_attr' => '', + //数字索引的子节点名 + 'item_node' => 'item', + // 数字索引子节点key转换的属性名 + 'item_key' => 'id', + // 数据编码 + 'encoding' => 'utf-8', + ]; + + protected $contentType = 'text/xml'; + + /** + * 处理数据 + * @access protected + * @param mixed $data 要处理的数据 + * @return mixed + */ + protected function output($data) + { + // XML数据转换 + return $this->xmlEncode($data, $this->options['root_node'], $this->options['item_node'], $this->options['root_attr'], $this->options['item_key'], $this->options['encoding']); + } + + /** + * XML编码 + * @param mixed $data 数据 + * @param string $root 根节点名 + * @param string $item 数字索引的子节点名 + * @param string $attr 根节点属性 + * @param string $id 数字索引子节点key转换的属性名 + * @param string $encoding 数据编码 + * @return string + */ + protected function xmlEncode($data, $root, $item, $attr, $id, $encoding) + { + if (is_array($attr)) { + $array = []; + foreach ($attr as $key => $value) { + $array[] = "{$key}=\"{$value}\""; + } + $attr = implode(' ', $array); + } + $attr = trim($attr); + $attr = empty($attr) ? '' : " {$attr}"; + $xml = ""; + $xml .= "<{$root}{$attr}>"; + $xml .= $this->dataToXml($data, $item, $id); + $xml .= ""; + return $xml; + } + + /** + * 数据XML编码 + * @param mixed $data 数据 + * @param string $item 数字索引时的节点名称 + * @param string $id 数字索引key转换为的属性名 + * @return string + */ + protected function dataToXml($data, $item, $id) + { + $xml = $attr = ''; + foreach ($data as $key => $val) { + if (is_numeric($key)) { + $id && $attr = " {$id}=\"{$key}\""; + $key = $item; + } + $xml .= "<{$key}{$attr}>"; + $xml .= (is_array($val) || is_object($val)) ? $this->dataToXml($val, $item, $id) : $val; + $xml .= ""; + } + return $xml; + } +} diff --git a/library/think/session/driver/Memcache.php b/library/think/session/driver/Memcache.php index e3b0d1be..812c07d2 100644 --- a/library/think/session/driver/Memcache.php +++ b/library/think/session/driver/Memcache.php @@ -34,14 +34,14 @@ class Memcache extends SessionHandler /** * 打开Session * @access public - * @param string $savePath - * @param mixed $sessName + * @param string $savePath + * @param mixed $sessName */ public function open($savePath, $sessName) { // 检测php环境 if (!extension_loaded('memcache')) { - throw new Exception('_NOT_SUPPERT_:memcache'); + throw new Exception('not support:memcache'); } $this->handler = new \Memcache; // 支持集群 @@ -85,8 +85,8 @@ class Memcache extends SessionHandler /** * 写入Session * @access public - * @param string $sessID - * @param String $sessData + * @param string $sessID + * @param String $sessData */ public function write($sessID, $sessData) { diff --git a/library/think/session/driver/Memcached.php b/library/think/session/driver/Memcached.php index bccfdda0..027b743e 100644 --- a/library/think/session/driver/Memcached.php +++ b/library/think/session/driver/Memcached.php @@ -23,6 +23,8 @@ class Memcached extends SessionHandler 'expire' => 3600, // session有效期 'timeout' => 0, // 连接超时时间(单位:毫秒) 'session_name' => '', // memcache key前缀 + 'username' => '', //账号 + 'password' => '', //密码 ]; public function __construct($config = []) @@ -33,14 +35,14 @@ class Memcached extends SessionHandler /** * 打开Session * @access public - * @param string $savePath - * @param mixed $sessName + * @param string $savePath + * @param mixed $sessName */ public function open($savePath, $sessName) { // 检测php环境 if (!extension_loaded('memcached')) { - throw new Exception('_NOT_SUPPERT_:memcached'); + throw new Exception('not support:memcached'); } $this->handler = new \Memcached; // 设置连接超时时间(单位:毫秒) @@ -59,6 +61,10 @@ class Memcached extends SessionHandler $servers[] = [$host, (isset($ports[$i]) ? $ports[$i] : $ports[0]), 1]; } $this->handler->addServers($servers); + if('' != $this->config['username']){ + $this->handler->setOption(\Memcached::OPT_BINARY_PROTOCOL, true); + $this->handler->setSaslAuthData($this->config['username'], $this->config['password']); + } return true; } @@ -69,7 +75,7 @@ class Memcached extends SessionHandler public function close() { $this->gc(ini_get('session.gc_maxlifetime')); - $this->handler->close(); + $this->handler->quit(); $this->handler = null; return true; } diff --git a/library/think/session/driver/Redis.php b/library/think/session/driver/Redis.php index b07289dd..367df10c 100644 --- a/library/think/session/driver/Redis.php +++ b/library/think/session/driver/Redis.php @@ -16,15 +16,16 @@ use think\Exception; class Redis extends SessionHandler { + /** @var \Redis */ protected $handler = null; protected $config = [ - 'host' => '127.0.0.1', // redis主机 - 'port' => 6379, // redis端口 - 'password' => '', // 密码 - 'expire' => 3600, // 有效期(秒) - 'timeout' => 0, // 超时时间(秒) - 'persistent' => true, // 是否长连接 - 'session_name' => '', // sessionkey前缀 + 'host' => '127.0.0.1', // redis主机 + 'port' => 6379, // redis端口 + 'password' => '', // 密码 + 'expire' => 3600, // 有效期(秒) + 'timeout' => 0, // 超时时间(秒) + 'persistent' => true, // 是否长连接 + 'session_name' => '', // sessionkey前缀 ]; public function __construct($config = []) @@ -36,20 +37,22 @@ class Redis extends SessionHandler * 打开Session * @access public * @param string $savePath - * @param mixed $sessName + * @param mixed $sessName + * @return bool + * @throws Exception */ public function open($savePath, $sessName) { // 检测php环境 if (!extension_loaded('redis')) { - throw new Exception('_NOT_SUPPERT_:redis'); + throw new Exception('not support:redis'); } $this->handler = new \Redis; - + // 建立连接 $func = $this->config['persistent'] ? 'pconnect' : 'connect'; $this->handler->$func($this->config['host'], $this->config['port'], $this->config['timeout']); - + if ('' != $this->config['password']) { $this->handler->auth($this->config['password']); } @@ -72,6 +75,7 @@ class Redis extends SessionHandler * 读取Session * @access public * @param string $sessID + * @return bool|string */ public function read($sessID) { @@ -83,6 +87,7 @@ class Redis extends SessionHandler * @access public * @param string $sessID * @param String $sessData + * @return bool */ public function write($sessID, $sessData) { @@ -97,16 +102,18 @@ class Redis extends SessionHandler * 删除Session * @access public * @param string $sessID + * @return bool|void */ public function destroy($sessID) { - return $this->handler->delete($this->config['session_name'] . $sessID); + $this->handler->delete($this->config['session_name'] . $sessID); } /** * Session 垃圾回收 * @access public * @param string $sessMaxLifeTime + * @return bool */ public function gc($sessMaxLifeTime) { diff --git a/library/think/template/TagLib.php b/library/think/template/TagLib.php index 254407ea..b0e40f57 100644 --- a/library/think/template/TagLib.php +++ b/library/think/template/TagLib.php @@ -87,15 +87,15 @@ class TagLib public function parseTag(&$content, $lib = '') { $tags = []; - $_lib = $lib ? $lib . ':' : ''; + $lib = $lib ? strtolower($lib) . ':' : ''; foreach ($this->tags as $name => $val) { - $close = !isset($val['close']) || $val['close'] ? 1 : 0; - $tags[$close][$_lib . $name] = $name; + $close = !isset($val['close']) || $val['close'] ? 1 : 0; + $tags[$close][$lib . $name] = $name; if (isset($val['alias'])) { // 别名设置 $array = (array) $val['alias']; foreach (explode(',', $array[0]) as $v) { - $tags[$close][$_lib . $v] = $name; + $tags[$close][$lib . $v] = $name; } } } @@ -108,7 +108,7 @@ class TagLib $right = []; foreach ($matches as $match) { if ('' == $match[1][0]) { - $name = $match[2][0]; + $name = strtolower($match[2][0]); // 如果有没闭合的标签头则取出最后一个 if (!empty($right[$name])) { // $match[0][1]为标签结束符在模板中的位置 @@ -120,7 +120,7 @@ class TagLib } } else { // 标签头压入栈 - $right[$match[1][0]][] = $match[0]; + $right[strtolower($match[1][0])][] = $match[0]; } } unset($right, $matches); @@ -135,10 +135,10 @@ class TagLib foreach ($nodes as $pos => $node) { // 对应的标签名 $name = $tags[1][$node['name']]; - $alias = $_lib . $name != $node['name'] ? ($_lib ? strstr($node['name'], $_lib) : $node['name']) : ''; + $alias = $lib . $name != $node['name'] ? ($lib ? strstr($node['name'], $lib) : $node['name']) : ''; // 解析标签属性 $attrs = $this->parseAttr($node['begin'][0], $name, $alias); - $method = '_' . $name; + $method = 'tag' . $name; // 读取标签库中对应的标签内容 replace[0]用来替换标签头,replace[1]用来替换标签尾 $replace = explode($break, $this->$method($attrs, $break)); if (count($replace) > 1) { @@ -170,13 +170,13 @@ class TagLib // 自闭合标签 if (!empty($tags[0])) { $regex = $this->getRegex(array_keys($tags[0]), 0); - $content = preg_replace_callback($regex, function ($matches) use (&$tags, &$_lib) { + $content = preg_replace_callback($regex, function ($matches) use (&$tags, &$lib) { // 对应的标签名 - $name = $tags[0][$matches[1]]; - $alias = $_lib . $name != $matches[1] ? ($_lib ? strstr($matches[1], $_lib) : $matches[1]) : ''; + $name = $tags[0][strtolower($matches[1])]; + $alias = $lib . $name != $matches[1] ? ($lib ? strstr($matches[1], $lib) : $matches[1]) : ''; // 解析标签属性 $attrs = $this->parseAttr($matches[0], $name, $alias); - $method = '_' . $name; + $method = 'tag' . $name; return $this->$method($attrs, ''); }, $content); } @@ -186,8 +186,8 @@ class TagLib /** * 按标签生成正则 * @access private - * @param array|string $tags 标签名 - * @param boolean $close 是否为闭合标签 + * @param array|string $tags 标签名 + * @param boolean $close 是否为闭合标签 * @return string */ private function getRegex($tags, $close) @@ -255,7 +255,7 @@ class TagLib $must = explode(',', $tag['must']); foreach ($must as $name) { if (!isset($result[$name])) { - throw new Exception('_PARAM_ERROR_:' . $name); + throw new Exception('tag attr must:' . $name); } } } @@ -272,7 +272,7 @@ class TagLib $result['expression'] = rtrim($result['expression'], '/'); $result['expression'] = trim($result['expression']); } elseif (empty($this->tags[$name]) || !empty($this->tags[$name]['attr'])) { - throw new Exception('_XML_TAG_ERROR_:' . $name); + throw new Exception('tag error:' . $name); } } return $result; @@ -286,6 +286,9 @@ class TagLib */ public function parseCondition($condition) { + if (strpos($condition, ':')) { + $condition = ' ' . substr(strstr($condition, ':'), 1); + } $condition = str_ireplace(array_keys($this->comparison), array_values($this->comparison), $condition); $this->tpl->parseVar($condition); // $this->tpl->parseVarFunction($condition); // XXX: 此句能解析表达式中用|分隔的函数,但表达式中如果有|、||这样的逻辑运算就产生了歧异 diff --git a/library/think/template/driver/File.php b/library/think/template/driver/File.php index c369390e..1cd041af 100644 --- a/library/think/template/driver/File.php +++ b/library/think/template/driver/File.php @@ -17,8 +17,8 @@ class File { /** * 写入编译缓存 - * @string $cacheFile 缓存的文件名 - * @string $content 缓存的内容 + * @param string $cacheFile 缓存的文件名 + * @param string $content 缓存的内容 * @return void|array */ public function write($cacheFile, $content) @@ -26,18 +26,18 @@ class File // 检测模板目录 $dir = dirname($cacheFile); if (!is_dir($dir)) { - mkdir($dir, 0777, true); + mkdir($dir, 0755, true); } // 生成模板缓存文件 if (false === file_put_contents($cacheFile, $content)) { - throw new Exception('cache write error :' . $cacheFile, 11602); + throw new Exception('cache write error:' . $cacheFile, 11602); } } /** * 读取编译编译 - * @string $cacheFile 缓存的文件名 - * @array $vars 变量数组 + * @param string $cacheFile 缓存的文件名 + * @param array $vars 变量数组 * @return void */ public function read($cacheFile, $vars = []) @@ -52,14 +52,17 @@ class File /** * 检查编译缓存是否有效 - * @array $templates 用到的模板文件及更新时间列表 - * @string $cacheFile 缓存的文件名 - * @int $cacheTime 缓存时间 + * @param string $cacheFile 缓存的文件名 + * @param int $cacheTime 缓存时间 * @return boolean */ public function check($cacheFile, $cacheTime) { - if (0 != $cacheTime && time() > filemtime($cacheFile) + $cacheTime) { + // 缓存文件不存在, 直接返回false + if (!file_exists($cacheFile)) { + return false; + } + if (0 != $cacheTime && $_SERVER['REQUEST_TIME'] > filemtime($cacheFile) + $cacheTime) { // 缓存是否在有效期 return false; } diff --git a/library/think/template/driver/Sae.php b/library/think/template/driver/Sae.php deleted file mode 100644 index b1d18665..00000000 --- a/library/think/template/driver/Sae.php +++ /dev/null @@ -1,109 +0,0 @@ - -// +---------------------------------------------------------------------- - -namespace think\template\driver; - -use think\Exception; - -class Sae -{ - // mc 对象 - private $mc; - // 编译缓存内容 - private $contents = []; - - /** - * 架构函数 - * @access public - */ - public function __construct() - { - if (!function_exists('memcache_init')) { - throw new Exception('请在SAE平台上运行代码。'); - } - $this->mc = @memcache_init(); - if (!$this->mc) { - throw new Exception('您未开通Memcache服务,请在SAE管理平台初始化Memcache服务'); - } - } - - /** - * 写入编译缓存 - * @string $cacheFile 缓存的文件名 - * @string $content 缓存的内容 - * @return void|array - */ - public function write($cacheFile, $content) - { - // 添加写入时间 - $content = time() . $content; - if (!$this->mc->set($cacheFile, $content, MEMCACHE_COMPRESSED, 0)) { - throw new Exception('sae mc write error :' . $cacheFile); - } else { - $this->contents[$cacheFile] = $content; - return true; - } - } - - /** - * 读取编译编译 - * @string $cacheFile 缓存的文件名 - * @array $vars 变量数组 - * @return void - */ - public function read($cacheFile, $vars = []) - { - if (!empty($vars) && is_array($vars)) { - extract($vars, EXTR_OVERWRITE); - } - eval('?>' . $this->get($cacheFile, 'content')); - } - - /** - * 检查编译缓存是否有效 - * @string $cacheFile 缓存的文件名 - * @int $cacheTime 缓存时间 - * @return boolean - */ - public function check($cacheFile, $cacheTime) - { - $mtime = $this->get($cacheFile, 'mtime'); - if (0 != $cacheTime && time() > $mtime + $cacheTime) { - // 缓存是否在有效期 - return false; - } - return true; - } - - /** - * 读取文件信息 - * @access private - * @param string $filename 文件名 - * @param string $name 信息名 mtime或者content - * @return boolean - */ - private function get($filename, $name) - { - if (!isset($this->contents[$filename])) { - $this->contents[$filename] = $this->mc->get($filename); - } - $content = $this->contents[$filename]; - - if (false === $content) { - return false; - } - $info = array( - 'mtime' => substr($content, 0, 10), - 'content' => substr($content, 10), - ); - return $info[$name]; - } -} diff --git a/library/think/template/taglib/Cx.php b/library/think/template/taglib/Cx.php index a3fe83cf..6b27ea82 100644 --- a/library/think/template/taglib/Cx.php +++ b/library/think/template/taglib/Cx.php @@ -43,8 +43,7 @@ class Cx extends Taglib 'notpresent' => ['attr' => 'name'], 'defined' => ['attr' => 'name'], 'notdefined' => ['attr' => 'name'], - 'import' => ['attr' => 'file,href,type,value,basepath', 'close' => 0], - 'load' => ['attr' => 'file,href,type,value,basepath', 'close' => 0, 'alias' => ['css,js', 'type']], + 'load' => ['attr' => 'file,href,type,value,basepath', 'close' => 0, 'alias' => ['import,css,js', 'type']], 'assign' => ['attr' => 'name,value', 'close' => 0], 'define' => ['attr' => 'name,value', 'close' => 0], 'for' => ['attr' => 'start,end,name,comparison,step'], @@ -61,7 +60,7 @@ class Cx extends Taglib * @param string $content 标签内容 * @return string */ - public function _php($tag, $content) + public function tagPhp($tag, $content) { $parseStr = ''; return $parseStr; @@ -79,7 +78,7 @@ class Cx extends Taglib * @param string $content 标签内容 * @return string|void */ - public function _volist($tag, $content) + public function tagVolist($tag, $content) { $name = $tag['name']; $id = $tag['id']; @@ -131,7 +130,7 @@ class Cx extends Taglib * @param string $content 标签内容 * @return string|void */ - public function _foreach($tag, $content) + public function tagForeach($tag, $content) { // 直接使用表达式 if (!empty($tag['expression'])) { @@ -212,7 +211,7 @@ class Cx extends Taglib * @param string $content 标签内容 * @return string */ - public function _if($tag, $content) + public function tagIf($tag, $content) { $condition = !empty($tag['expression']) ? $tag['expression'] : $tag['condition']; $condition = $this->parseCondition($condition); @@ -228,7 +227,7 @@ class Cx extends Taglib * @param string $content 标签内容 * @return string */ - public function _elseif($tag, $content) + public function tagElseif($tag, $content) { $condition = !empty($tag['expression']) ? $tag['expression'] : $tag['condition']; $condition = $this->parseCondition($condition); @@ -243,7 +242,7 @@ class Cx extends Taglib * @param array $tag 标签属性 * @return string */ - public function _else($tag) + public function tagElse($tag) { $parseStr = ''; return $parseStr; @@ -262,7 +261,7 @@ class Cx extends Taglib * @param string $content 标签内容 * @return string */ - public function _switch($tag, $content) + public function tagSwitch($tag, $content) { $name = !empty($tag['expression']) ? $tag['expression'] : $tag['name']; $name = $this->autoBuildVar($name); @@ -277,7 +276,7 @@ class Cx extends Taglib * @param string $content 标签内容 * @return string */ - public function _case($tag, $content) + public function tagCase($tag, $content) { $value = !empty($tag['expression']) ? $tag['expression'] : $tag['value']; $flag = substr($value, 0, 1); @@ -309,7 +308,7 @@ class Cx extends Taglib * @param string $content 标签内容 * @return string */ - public function _default($tag) + public function tagDefault($tag) { $parseStr = ''; return $parseStr; @@ -324,7 +323,7 @@ class Cx extends Taglib * @param string $content 标签内容 * @return string */ - public function _compare($tag, $content) + public function tagCompare($tag, $content) { $name = $tag['name']; $value = $tag['value']; @@ -359,7 +358,7 @@ class Cx extends Taglib * @param string $content 标签内容 * @return string */ - public function _range($tag, $content) + public function tagRange($tag, $content) { $name = $tag['name']; $value = $tag['value']; @@ -394,7 +393,7 @@ class Cx extends Taglib * @param string $content 标签内容 * @return string */ - public function _present($tag, $content) + public function tagPresent($tag, $content) { $name = $tag['name']; $name = $this->autoBuildVar($name); @@ -411,7 +410,7 @@ class Cx extends Taglib * @param string $content 标签内容 * @return string */ - public function _notpresent($tag, $content) + public function tagNotpresent($tag, $content) { $name = $tag['name']; $name = $this->autoBuildVar($name); @@ -428,7 +427,7 @@ class Cx extends Taglib * @param string $content 标签内容 * @return string */ - public function _empty($tag, $content) + public function tagEmpty($tag, $content) { $name = $tag['name']; $name = $this->autoBuildVar($name); @@ -445,7 +444,7 @@ class Cx extends Taglib * @param string $content 标签内容 * @return string */ - public function _notempty($tag, $content) + public function tagNotempty($tag, $content) { $name = $tag['name']; $name = $this->autoBuildVar($name); @@ -460,7 +459,7 @@ class Cx extends Taglib * @param string $content * @return string */ - public function _defined($tag, $content) + public function tagDefined($tag, $content) { $name = $tag['name']; $parseStr = '' . $content . ''; @@ -474,7 +473,7 @@ class Cx extends Taglib * @param string $content * @return string */ - public function _notdefined($tag, $content) + public function tagNotdefined($tag, $content) { $name = $tag['name']; $parseStr = '' . $content . ''; @@ -482,19 +481,17 @@ class Cx extends Taglib } /** - * import 标签解析 {import file="Js.Base" /} - * 格式:{import file="Css.Base" type="css" /} + * load 标签解析 {load file="/static/js/base.js" /} + * 格式:{load file="/static/css/base.css" /} * @access public * @param array $tag 标签属性 * @param string $content 标签内容 - * @param boolean $isFile 是否文件方式 - * @param string $type 类型 * @return string */ - public function _import($tag, $content, $isFile = false) + public function tagLoad($tag, $content) { $file = isset($tag['file']) ? $tag['file'] : $tag['href']; - $type = isset($tag['type']) ? strtolower($tag['type']) : ($isFile ? null : 'js'); + $type = isset($tag['type']) ? strtolower($tag['type']) : ''; $parseStr = ''; $endStr = ''; // 判断是否存在加载条件 允许使用函数判断(默认为isset) @@ -505,58 +502,26 @@ class Cx extends Taglib $parseStr .= ''; $endStr = ''; } - if ($isFile) { - // 文件方式导入 - $array = explode(',', $file); - foreach ($array as $val) { - if (!$type || isset($reset)) { - $type = $reset = strtolower(substr(strrchr($val, '.'), 1)); - } - switch ($type) { - case 'js': - $parseStr .= ''; - break; - case 'css': - $parseStr .= ''; - break; - case 'php': - $parseStr .= ''; - break; - } - } - } else { - // 命名空间导入模式 - $basepath = !empty($tag['basepath']) ? $tag['basepath'] : '/public'; - // 命名空间方式导入外部文件 - $array = explode(',', $file); - foreach ($array as $val) { - if (strpos($val, '?')) { - list($val, $version) = explode('?', $val); - } else { - $version = ''; - } - switch ($type) { - case 'js': - $parseStr .= ''; - break; - case 'css': - $parseStr .= ''; - break; - case 'php': - $parseStr .= ''; - break; - } + + // 文件方式导入 + $array = explode(',', $file); + foreach ($array as $val) { + $type = strtolower(substr(strrchr($val, '.'), 1)); + switch ($type) { + case 'js': + $parseStr .= ''; + break; + case 'css': + $parseStr .= ''; + break; + case 'php': + $parseStr .= ''; + break; } } return $parseStr . $endStr; } - // import别名 采用文件方式加载(要使用命名空间必须用import) 例如 - public function _load($tag, $content) - { - return $this->_import($tag, $content, true); - } - /** * assign标签解析 * 在模板中给某个变量赋值 支持变量赋值 @@ -566,7 +531,7 @@ class Cx extends Taglib * @param string $content 标签内容 * @return string */ - public function _assign($tag, $content) + public function tagAssign($tag, $content) { $name = $this->autoBuildVar($tag['name']); $flag = substr($tag['value'], 0, 1); @@ -588,7 +553,7 @@ class Cx extends Taglib * @param string $content 标签内容 * @return string */ - public function _define($tag, $content) + public function tagDefine($tag, $content) { $name = '\'' . $tag['name'] . '\''; $flag = substr($tag['value'], 0, 1); @@ -612,7 +577,7 @@ class Cx extends Taglib * @param string $content 标签内容 * @return string */ - public function _for($tag, $content) + public function tagFor($tag, $content) { //设置默认值 $start = 0; @@ -655,21 +620,21 @@ class Cx extends Taglib return $parseStr; } - /** + /** * U函数的tag标签 - * 格式: + * 格式:{url link="模块/控制器/方法" vars="参数" suffix="true或者false 是否带有后缀" domain="true或者false 是否携带域名" /} * @access public * @param array $tag 标签属性 * @param string $content 标签内容 * @return string */ - public function _url($tag, $content) + public function tagUrl($tag, $content) { $url = isset($tag['link']) ? $tag['link'] : ''; $vars = isset($tag['vars']) ? $tag['vars'] : ''; $suffix = isset($tag['suffix']) ? $tag['suffix'] : 'true'; $domain = isset($tag['domain']) ? $tag['domain'] : 'false'; - return ''; + return ''; } /** @@ -689,7 +654,7 @@ class Cx extends Taglib * @param string $content 标签内容 * @return string */ - public function _function($tag, $content) + public function tagFunction($tag, $content) { $name = !empty($tag['name']) ? $tag['name'] : 'func'; $vars = !empty($tag['vars']) ? $tag['vars'] : ''; diff --git a/library/think/view/driver/Php.php b/library/think/view/driver/Php.php index 0b6edbac..13820baa 100644 --- a/library/think/view/driver/Php.php +++ b/library/think/view/driver/Php.php @@ -11,8 +11,10 @@ namespace think\view\driver; -use think\Exception; +use think\App; +use think\exception\TemplateNotFoundException; use think\Log; +use think\Request; class Php { @@ -21,7 +23,7 @@ class Php // 模板起始路径 'view_path' => '', // 模板文件后缀 - 'view_suffix' => '.php', + 'view_suffix' => 'php', // 模板文件名分隔符 'view_depr' => DS, ]; @@ -31,25 +33,40 @@ class Php $this->config = array_merge($this->config, $config); } + /** + * 检测是否存在模板文件 + * @access public + * @param string $template 模板文件或者模板规则 + * @return bool + */ + public function exists($template) + { + if ('' == pathinfo($template, PATHINFO_EXTENSION)) { + // 获取模板文件名 + $template = $this->parseTemplate($template); + } + return is_file($template); + } + /** * 渲染模板文件 * @access public - * @param string $template 模板文件 - * @param array $data 模板变量 + * @param string $template 模板文件 + * @param array $data 模板变量 * @return void */ public function fetch($template, $data = []) { - if (!is_file($template)) { + if ('' == pathinfo($template, PATHINFO_EXTENSION)) { // 获取模板文件名 $template = $this->parseTemplate($template); } // 模板不存在 抛出异常 if (!is_file($template)) { - throw new Exception('template file not exists:' . $template, 10700); + throw new TemplateNotFoundException('template not exists:' . $template, $template); } // 记录视图信息 - APP_DEBUG && Log::record('[ VIEW ] ' . $template . ' [ ' . var_export(array_keys($data), true) . ' ]', 'info'); + App::$debug && Log::record('[ VIEW ] ' . $template . ' [ ' . var_export(array_keys($data), true) . ' ]', 'info'); extract($data, EXTR_OVERWRITE); include $template; } @@ -57,8 +74,8 @@ class Php /** * 渲染模板内容 * @access public - * @param string $content 模板内容 - * @param array $data 模板变量 + * @param string $content 模板内容 + * @param array $data 模板变量 * @return void */ public function display($content, $data = []) @@ -75,29 +92,31 @@ class Php */ private function parseTemplate($template) { - if (empty($this->config['view_path']) && defined('VIEW_PATH')) { - $this->config['view_path'] = VIEW_PATH; + if (empty($this->config['view_path'])) { + $this->config['view_path'] = App::$modulePath . 'view' . DS; } - $depr = $this->config['view_depr']; - $template = str_replace(['/', ':'], $depr, $template); if (strpos($template, '@')) { list($module, $template) = explode('@', $template); - $path = APP_PATH . (APP_MULTI_MODULE ? $module . DS : '') . VIEW_LAYER . DS; + $path = APP_PATH . $module . DS . 'view' . DS; } else { $path = $this->config['view_path']; } // 分析模板文件规则 - if (defined('CONTROLLER_NAME')) { + $request = Request::instance(); + $controller = $request->controller(); + if ($controller && 0 !== strpos($template, '/')) { + $depr = $this->config['view_depr']; + $template = str_replace(['/', ':'], $depr, $template); if ('' == $template) { // 如果模板文件名为空 按照默认规则定位 - $template = str_replace('.', DS, CONTROLLER_NAME) . $depr . ACTION_NAME; + $template = str_replace('.', DS, $controller) . $depr . $request->action(); } elseif (false === strpos($template, $depr)) { - $template = str_replace('.', DS, CONTROLLER_NAME) . $depr . $template; + $template = str_replace('.', DS, $controller) . $depr . $template; } } - return $path . $template . $this->config['view_suffix']; + return $path . ltrim($template, '/') . '.' . ltrim($this->config['view_suffix'], '.'); } } diff --git a/library/think/view/driver/Think.php b/library/think/view/driver/Think.php index 6867b855..264102b2 100644 --- a/library/think/view/driver/Think.php +++ b/library/think/view/driver/Think.php @@ -11,20 +11,22 @@ namespace think\view\driver; -use think\Exception; +use think\App; +use think\exception\TemplateNotFoundException; use think\Log; +use think\Request; use think\Template; class Think { // 模板引擎实例 - private $template = null; + private $template; // 模板引擎参数 protected $config = [ // 模板起始路径 'view_path' => '', // 模板文件后缀 - 'view_suffix' => '.html', + 'view_suffix' => 'html', // 模板文件名分隔符 'view_depr' => DS, // 是否开启模板编译缓存,设为false则每次都会重新编译 @@ -34,41 +36,56 @@ class Think public function __construct($config = []) { $this->config = array_merge($this->config, $config); - if (empty($this->config['view_path']) && defined('VIEW_PATH')) { - $this->config['view_path'] = VIEW_PATH; + if (empty($this->config['view_path'])) { + $this->config['view_path'] = App::$modulePath . 'view' . DS; } $this->template = new Template($this->config); } + /** + * 检测是否存在模板文件 + * @access public + * @param string $template 模板文件或者模板规则 + * @return bool + */ + public function exists($template) + { + if ('' == pathinfo($template, PATHINFO_EXTENSION)) { + // 获取模板文件名 + $template = $this->parseTemplate($template); + } + return is_file($template); + } + /** * 渲染模板文件 * @access public - * @param string $template 模板文件 - * @param array $data 模板变量 - * @param array $config 模板参数 + * @param string $template 模板文件 + * @param array $data 模板变量 + * @param array $config 模板参数 * @return void */ public function fetch($template, $data = [], $config = []) { - if (!is_file($template)) { + if ('' == pathinfo($template, PATHINFO_EXTENSION)) { // 获取模板文件名 $template = $this->parseTemplate($template); } // 模板不存在 抛出异常 if (!is_file($template)) { - throw new Exception('template file not exists:' . $template, 10700); + throw new TemplateNotFoundException('template not exists:' . $template, $template); } // 记录视图信息 - APP_DEBUG && Log::record('[ VIEW ] ' . $template . ' [ ' . var_export(array_keys($data), true) . ' ]', 'info'); + App::$debug && Log::record('[ VIEW ] ' . $template . ' [ ' . var_export(array_keys($data), true) . ' ]', 'info'); $this->template->fetch($template, $data, $config); } /** * 渲染模板内容 * @access public - * @param string $template 模板内容 - * @param array $data 模板变量 - * @param array $config 模板参数 + * @param string $template 模板内容 + * @param array $data 模板变量 + * @param array $config 模板参数 * @return void */ public function display($template, $data = [], $config = []) @@ -84,25 +101,30 @@ class Think */ private function parseTemplate($template) { - $depr = $this->config['view_depr']; - $template = str_replace(['/', ':'], $depr, $template); + // 获取视图根目录 if (strpos($template, '@')) { + // 跨模块调用 list($module, $template) = explode('@', $template); - $path = APP_PATH . (APP_MULTI_MODULE ? $module . DS : '') . VIEW_LAYER . DS; + $path = APP_PATH . $module . DS . 'view' . DS; } else { + // 当前视图目录 $path = $this->config['view_path']; } // 分析模板文件规则 - if (defined('CONTROLLER_NAME')) { + $request = Request::instance(); + $controller = $request->controller(); + if ($controller && 0 !== strpos($template, '/')) { + $depr = $this->config['view_depr']; + $template = str_replace(['/', ':'], $depr, $template); if ('' == $template) { // 如果模板文件名为空 按照默认规则定位 - $template = str_replace('.', DS, CONTROLLER_NAME) . $depr . ACTION_NAME; + $template = str_replace('.', DS, $controller) . $depr . $request->action(); } elseif (false === strpos($template, $depr)) { - $template = str_replace('.', DS, CONTROLLER_NAME) . $depr . $template; + $template = str_replace('.', DS, $controller) . $depr . $template; } } - return $path . $template . $this->config['view_suffix']; + return $path . ltrim($template, '/') . '.' . ltrim($this->config['view_suffix'], '.'); } public function __call($method, $params) diff --git a/library/traits/controller/Jump.php b/library/traits/controller/Jump.php index c6a3a243..10f2c765 100644 --- a/library/traits/controller/Jump.php +++ b/library/traits/controller/Jump.php @@ -2,7 +2,7 @@ /** * 用法: - * T('controller/Jump'); + * load_trait('controller/Jump'); * class index * { * use \traits\controller\Jump; @@ -15,101 +15,138 @@ namespace traits\controller; use think\Config; +use think\exception\HttpResponseException; +use think\Request; use think\Response; -use think\View; +use think\response\Redirect; +use think\Url; +use think\View as ViewTemplate; trait Jump { /** * 操作成功跳转的快捷方法 - * @access public + * @access protected * @param mixed $msg 提示信息 - * @param mixed $data 返回的数据 * @param string $url 跳转的URL地址 + * @param mixed $data 返回的数据 * @param integer $wait 跳转等待时间 - * @return mixed + * @return array */ - public static function success($msg = '', $data = '', $url = null, $wait = 3) + protected function success($msg = '', $url = null, $data = '', $wait = 3) { $code = 1; if (is_numeric($msg)) { $code = $msg; $msg = ''; } + if (is_null($url) && isset($_SERVER["HTTP_REFERER"])) { + $url = $_SERVER["HTTP_REFERER"]; + } else { + $url = preg_match('/^(https?:|\/)/', $url) ? $url : Url::build($url); + } $result = [ 'code' => $code, 'msg' => $msg, 'data' => $data, - 'url' => is_null($url) && isset($_SERVER["HTTP_REFERER"]) ? $_SERVER["HTTP_REFERER"] : $url, + 'url' => $url, 'wait' => $wait, ]; - $type = IS_AJAX ? Config::get('default_ajax_return') : Config::get('default_return_type'); - - if ('html' == $type) { - $result = View::instance(Config::get('template'), Config::get('view_replace_str')) + $type = $this->getResponseType(); + if ('html' == strtolower($type)) { + $result = ViewTemplate::instance(Config::get('template'), Config::get('view_replace_str')) ->fetch(Config::get('dispatch_success_tmpl'), $result); } - Response::send($result, $type); + return Response::create($result, $type); } /** * 操作错误跳转的快捷方法 - * @access public + * @access protected * @param mixed $msg 提示信息 - * @param mixed $data 返回的数据 * @param string $url 跳转的URL地址 + * @param mixed $data 返回的数据 * @param integer $wait 跳转等待时间 - * @return mixed + * @return void */ - public static function error($msg = '', $data = '', $url = null, $wait = 3) + protected function error($msg = '', $url = null, $data = '', $wait = 3) { $code = 0; if (is_numeric($msg)) { $code = $msg; $msg = ''; } + if (is_null($url)) { + $url = 'javascript:history.back(-1);'; + } else { + $url = preg_match('/^(https?:|\/)/', $url) ? $url : Url::build($url); + } $result = [ 'code' => $code, 'msg' => $msg, 'data' => $data, - 'url' => is_null($url) ? 'javascript:history.back(-1);' : $url, + 'url' => $url, 'wait' => $wait, ]; - $type = IS_AJAX ? Config::get('default_ajax_return') : Config::get('default_return_type'); - - if ('html' == $type) { - $result = View::instance(Config::get('template'), Config::get('view_replace_str')) + $type = $this->getResponseType(); + if ('html' == strtolower($type)) { + $result = ViewTemplate::instance(Config::get('template'), Config::get('view_replace_str')) ->fetch(Config::get('dispatch_error_tmpl'), $result); } - Response::send($result, $type); + $response = Response::create($result, $type); + throw new HttpResponseException($response); } /** * 返回封装后的API数据到客户端 - * @access public + * @access protected * @param mixed $data 要返回的数据 * @param integer $code 返回的code * @param mixed $msg 提示信息 * @param string $type 返回数据格式 * @return mixed */ - public function result($data, $code = 0, $msg = '', $type = '') + protected function result($data, $code = 0, $msg = '', $type = '') { - return Response::result($data, $code, $msg, $type); + $result = [ + 'code' => $code, + 'msg' => $msg, + 'time' => $_SERVER['REQUEST_TIME'], + 'data' => $data, + ]; + $type = $type ?: $this->getResponseType(); + return Response::create($result, $type); } /** * URL重定向 * @access protected * @param string $url 跳转的URL表达式 - * @param array|int $params 其它URL参数或http code + * @param array|integer $params 其它URL参数 + * @param integer $code http code * @return void */ - public function redirect($url, $params = []) + protected function redirect($url, $params = [], $code = 302) { - Response::redirect($url, $params); + $response = new Redirect($url); + if (is_integer($params)) { + $code = $params; + $params = []; + } + $response->code($code)->params($params); + throw new HttpResponseException($response); } + /** + * 获取当前的response 输出类型 + * @access protected + * @return string + */ + protected function getResponseType() + { + $isAjax = Request::instance()->isAjax(); + return $isAjax ? Config::get('default_ajax_return') : Config::get('default_return_type'); + } } diff --git a/library/traits/think/Instance.php b/library/traits/think/Instance.php index d85975b3..206a9412 100644 --- a/library/traits/think/Instance.php +++ b/library/traits/think/Instance.php @@ -11,6 +11,8 @@ namespace traits\think; +use \think\Exception; + trait Instance { protected static $instance = null; @@ -37,7 +39,7 @@ trait Instance if (0 === strpos($method, '_') && is_callable([self::$instance, $call])) { return call_user_func_array([self::$instance, $call], $params); } else { - throw new \think\Exception("not exists method:" . $method); + throw new Exception("method not exists:" . $method); } } } diff --git a/mode/common.php b/mode/common.php deleted file mode 100644 index e29cffdc..00000000 --- a/mode/common.php +++ /dev/null @@ -1,73 +0,0 @@ - -// +---------------------------------------------------------------------- - -/** - * ThinkPHP 普通模式定义 - */ -return [ - // 命名空间 - 'namespace' => [ - 'think' => LIB_PATH . 'think' . DS, - 'behavior' => LIB_PATH . 'behavior' . DS, - 'traits' => LIB_PATH . 'traits' . DS, - APP_NAMESPACE => APP_PATH, - ], - - // 配置文件 - 'config' => THINK_PATH . 'convention' . EXT, - - // 别名定义 - 'alias' => [ - 'think\App' => CORE_PATH . 'App' . EXT, - 'think\Build' => CORE_PATH . 'Build' . EXT, - 'think\Cache' => CORE_PATH . 'Cache' . EXT, - 'think\Config' => CORE_PATH . 'Config' . EXT, - 'think\Console' => CORE_PATH . 'Console' . EXT, - 'think\Controller' => CORE_PATH . 'Controller' . EXT, - 'think\Cookie' => CORE_PATH . 'Cookie' . EXT, - 'think\Db' => CORE_PATH . 'Db' . EXT, - 'think\Debug' => CORE_PATH . 'Debug' . EXT, - 'think\Error' => CORE_PATH . 'Error' . EXT, - 'think\Exception' => CORE_PATH . 'Exception' . EXT, - 'think\exception\DbException' => CORE_PATH . 'exception' . DS . 'DbException' . EXT, - 'think\exception\PDOException' => CORE_PATH . 'exception' . DS . 'PDOException' . EXT, - 'think\exception\ErrorException' => CORE_PATH . 'exception' . DS . 'ErrorException' . EXT, - 'think\exception\DbBindParamException' => CORE_PATH . 'exception' . DS . 'DbBindParamException' . EXT, - 'think\exception\NotFoundException' => CORE_PATH . 'exception' . DS . 'NotFoundException' . EXT, - 'think\File' => CORE_PATH . 'File' . EXT, - 'think\Hook' => CORE_PATH . 'Hook' . EXT, - 'think\Input' => CORE_PATH . 'Input' . EXT, - 'think\Lang' => CORE_PATH . 'Lang' . EXT, - 'think\Log' => CORE_PATH . 'Log' . EXT, - 'think\Model' => CORE_PATH . 'Model' . EXT, - 'think\model\Relation' => CORE_PATH . 'model' . DS . 'Relation' . EXT, - 'think\model\Merge' => CORE_PATH . 'model' . DS . 'Merge' . EXT, - 'think\model\Pivot' => CORE_PATH . 'model' . DS . 'Pivot' . EXT, - 'think\Response' => CORE_PATH . 'Response' . EXT, - 'think\Process' => CORE_PATH . 'Process' . EXT, - 'think\Route' => CORE_PATH . 'Route' . EXT, - 'think\Session' => CORE_PATH . 'Session' . EXT, - 'think\Template' => CORE_PATH . 'Template' . EXT, - 'think\Url' => CORE_PATH . 'Url' . EXT, - 'think\Validate' => CORE_PATH . 'Validate' . EXT, - 'think\View' => CORE_PATH . 'View' . EXT, - 'think\db\Connection' => CORE_PATH . 'db' . DS . 'Connection' . EXT, - 'think\db\connector\Mysql' => CORE_PATH . 'db' . DS . 'connector' . DS . 'Mysql' . EXT, - 'think\db\Builder' => CORE_PATH . 'db' . DS . 'Builder' . EXT, - 'think\db\Builder\Mysql' => CORE_PATH . 'db' . DS . 'builder' . DS . 'Mysql' . EXT, - 'think\db\Query' => CORE_PATH . 'db' . DS . 'Query' . EXT, - 'think\view\driver\Think' => CORE_PATH . 'view' . DS . 'driver' . DS . 'Think' . EXT, - 'think\view\driver\Php' => CORE_PATH . 'view' . DS . 'driver' . DS . 'Php' . EXT, - 'think\template\driver\File' => CORE_PATH . 'template' . DS . 'driver' . DS . 'File' . EXT, - 'think\log\driver\File' => CORE_PATH . 'log' . DS . 'driver' . DS . 'File' . EXT, - 'think\cache\driver\File' => CORE_PATH . 'cache' . DS . 'driver' . DS . 'File' . EXT, - ], -]; diff --git a/mode/console.php b/mode/console.php deleted file mode 100644 index 5f0c488e..00000000 --- a/mode/console.php +++ /dev/null @@ -1,32 +0,0 @@ - -// +---------------------------------------------------------------------- - -/** - * ThinkPHP CLI模式定义 - */ -return [ - - // 命名空间 - 'namespace' => [ - 'think' => LIB_PATH . 'think' . DS, - 'behavior' => LIB_PATH . 'behavior' . DS, - 'traits' => LIB_PATH . 'traits' . DS, - APP_NAMESPACE => APP_PATH, - ], - // 别名定义 - 'alias' => [ - 'think\App' => MODE_PATH . 'console/App' . EXT, - 'think\Error' => MODE_PATH . 'console/Error' . EXT - ], - // 配置文件 - 'config' => THINK_PATH . 'convention' . EXT - -]; diff --git a/mode/console/App.php b/mode/console/App.php deleted file mode 100644 index fd19be16..00000000 --- a/mode/console/App.php +++ /dev/null @@ -1,107 +0,0 @@ - -// +---------------------------------------------------------------------- - -namespace think; - -use think\Console; - -class App -{ - /** - * 执行应用程序 - * @access public - * @return void - */ - public static function run() - { - self::init(); - - // 实例化console - $console = new Console('Think Console', '0.1'); - // 读取指令集 - if (is_file(APP_PATH . 'command' . EXT)) { - $commands = include APP_PATH . 'command' . EXT; - if (is_array($commands)) { - foreach ($commands as $command) { - if (class_exists($command) && is_subclass_of($command, "\\think\\console\\command\\Command")) { - // 注册指令 - $console->add(new $command()); - } - } - } - } - // 运行 - $console->run(); - } - - private static function init() - { - // 加载初始化文件 - if (is_file(APP_PATH . 'init' . EXT)) { - include APP_PATH . 'init' . EXT; - - // 加载模块配置 - $config = Config::get(); - } else { - // 加载模块配置 - $config = Config::load(APP_PATH . 'config' . EXT); - - // 加载应用状态配置 - if ($config['app_status']) { - $config = Config::load(APP_PATH . $config['app_status'] . EXT); - } - - // 读取扩展配置文件 - if ($config['extra_config_list']) { - foreach ($config['extra_config_list'] as $name => $file) { - $filename = APP_PATH . $file . EXT; - Config::load($filename, is_string($name) ? $name : pathinfo($filename, PATHINFO_FILENAME)); - } - } - - // 加载别名文件 - if (is_file(APP_PATH . 'alias' . EXT)) { - Loader::addMap(include APP_PATH . 'alias' . EXT); - } - - // 加载行为扩展文件 - if (APP_HOOK && is_file(APP_PATH . 'tags' . EXT)) { - Hook::import(include APP_PATH . 'tags' . EXT); - } - - // 加载公共文件 - if (is_file(APP_PATH . 'common' . EXT)) { - include APP_PATH . 'common' . EXT; - } - } - - // 注册根命名空间 - if (!empty($config['root_namespace'])) { - Loader::addNamespace($config['root_namespace']); - } - - // 加载额外文件 - if (!empty($config['extra_file_list'])) { - foreach ($config['extra_file_list'] as $file) { - $file = strpos($file, '.') ? $file : APP_PATH . $file . EXT; - if (is_file($file)) { - include_once $file; - } - } - } - - // 设置系统时区 - date_default_timezone_set($config['default_timezone']); - - // 监听app_init - APP_HOOK && Hook::listen('app_init'); - } -} \ No newline at end of file diff --git a/mode/sae.php b/mode/sae.php deleted file mode 100644 index c17d12c8..00000000 --- a/mode/sae.php +++ /dev/null @@ -1,121 +0,0 @@ - -// +---------------------------------------------------------------------- - -/** - * ThinkPHP SAE应用模式定义文件 - */ -return [ - // 命名空间 - 'namespace' => [ - 'think' => LIB_PATH . 'think' . DS, - 'behavior' => LIB_PATH . 'behavior' . DS, - 'traits' => LIB_PATH . 'traits' . DS, - APP_NAMESPACE => APP_PATH, - ], - - // 配置文件 - 'config' => array_merge(include THINK_PATH . 'convention' . EXT, [ - /* 数据库设置 */ - 'database' => [ - // 数据库类型 - 'type' => 'mysql', - 'dsn' => '', // - // 服务器地址 - 'hostname' => SAE_MYSQL_HOST_M . ',' . SAE_MYSQL_HOST_S, - // 数据库名 - 'database' => SAE_MYSQL_DB, - // 用户名 - 'username' => SAE_MYSQL_USER, - // 密码 - 'password' => SAE_MYSQL_PASS, - // 端口 - 'hostport' => SAE_MYSQL_PORT, - // 数据库连接参数 - 'params' => [], - // 数据库编码默认采用utf8 - 'charset' => 'utf8', - // 数据库表前缀 - 'prefix' => '', - // 数据库调试模式 - 'debug' => false, - // 数据库部署方式:0 集中式(单一服务器),1 分布式(主从服务器) - 'deploy' => 1, - // 数据库读写是否分离 主从式有效 - 'rw_separate' => true, - // 读写分离后 主服务器数量 - 'master_num' => 1, - // 指定从服务器序号 - 'slave_no' => '', - ], - 'log' => [ - 'type' => 'Sae', - ], - 'cache' => [ - 'type' => 'Sae', - 'path' => CACHE_PATH, - 'prefix' => '', - 'expire' => 0, - ], - 'file_upload_type' => 'Sae', - 'compile_type' => 'Sae', - ]), - - // 别名定义 - 'alias' => [ - 'think\App' => CORE_PATH . 'App' . EXT, - 'think\Build' => CORE_PATH . 'Build' . EXT, - 'think\Cache' => CORE_PATH . 'Cache' . EXT, - 'think\Config' => CORE_PATH . 'Config' . EXT, - 'think\Console' => CORE_PATH . 'Console' . EXT, - 'think\Controller' => CORE_PATH . 'Controller' . EXT, - 'think\Cookie' => CORE_PATH . 'Cookie' . EXT, - 'think\Db' => CORE_PATH . 'Db' . EXT, - 'think\Debug' => CORE_PATH . 'Debug' . EXT, - 'think\Error' => CORE_PATH . 'Error' . EXT, - 'think\Exception' => CORE_PATH . 'Exception' . EXT, - 'think\exception\DbException' => CORE_PATH . 'exception' . DS . 'DbException' . EXT, - 'think\exception\PDOException' => CORE_PATH . 'exception' . DS . 'PDOException' . EXT, - 'think\exception\ErrorException' => CORE_PATH . 'exception' . DS . 'ErrorException' . EXT, - 'think\exception\DbBindParamException' => CORE_PATH . 'exception' . DS . 'DbBindParamException' . EXT, - 'think\exception\NotFoundException' => CORE_PATH . 'exception' . DS . 'NotFoundException' . EXT, - 'think\File' => CORE_PATH . 'File' . EXT, - 'think\Hook' => CORE_PATH . 'Hook' . EXT, - 'think\Input' => CORE_PATH . 'Input' . EXT, - 'think\Lang' => CORE_PATH . 'Lang' . EXT, - 'think\Log' => CORE_PATH . 'Log' . EXT, - 'think\Model' => CORE_PATH . 'Model' . EXT, - 'think\model\Relation' => CORE_PATH . 'model' . DS . 'Relation' . EXT, - 'think\model\Merge' => CORE_PATH . 'model' . DS . 'Merge' . EXT, - 'think\model\Pivot' => CORE_PATH . 'model' . DS . 'Pivot' . EXT, - 'think\Process' => CORE_PATH . 'Process' . EXT, - 'think\Response' => CORE_PATH . 'Response' . EXT, - 'think\Route' => CORE_PATH . 'Route' . EXT, - 'think\Session' => CORE_PATH . 'Session' . EXT, - 'think\Template' => CORE_PATH . 'Template' . EXT, - 'think\Url' => CORE_PATH . 'Url' . EXT, - 'think\Validate' => CORE_PATH . 'Validate' . EXT, - 'think\View' => CORE_PATH . 'View' . EXT, - 'think\db\Connection' => CORE_PATH . 'db' . DS . 'Connection' . EXT, - 'think\db\connector\Mysql' => CORE_PATH . 'db' . DS . 'connector' . DS . 'Mysql' . EXT, - 'think\db\Builder' => CORE_PATH . 'db' . DS . 'Builder' . EXT, - 'think\db\Builder\Mysql' => CORE_PATH . 'db' . DS . 'builder' . DS . 'Mysql' . EXT, - 'think\db\Query' => CORE_PATH . 'db' . DS . 'Query' . EXT, - 'think\view\driver\Think' => CORE_PATH . 'view' . DS . 'driver' . DS . 'Think' . EXT, - 'think\view\driver\Php' => CORE_PATH . 'view' . DS . 'driver' . DS . 'Php' . EXT, - 'think\template\driver\File' => CORE_PATH . 'template' . DS . 'driver' . DS . 'File' . EXT, - 'think\log\driver\File' => CORE_PATH . 'log' . DS . 'driver' . DS . 'File' . EXT, - 'think\cache\driver\File' => CORE_PATH . 'cache' . DS . 'driver' . DS . 'File' . EXT, - 'think\log\driver\Sae' => CORE_PATH . 'log' . DS . 'driver' . DS . 'Sae' . EXT, - 'think\cache\driver\Sae' => CORE_PATH . 'cache' . DS . 'driver' . DS . 'Sae' . EXT, - 'think\template\driver\Sae' => CORE_PATH . 'template' . DS . 'driver' . DS . 'Sae' . EXT, - ], - -]; diff --git a/start.php b/start.php index 5ad6f069..2d517914 100644 --- a/start.php +++ b/start.php @@ -11,58 +11,8 @@ namespace think; -// ThinkPHP 实际引导文件 +// ThinkPHP 引导文件 // 加载基础文件 require __DIR__ . '/base.php'; -require CORE_PATH . 'Loader.php'; - -// 加载环境变量配置文件 -if (is_file(ROOT_PATH . 'env' . EXT)) { - $env = include ROOT_PATH . 'env' . EXT; - foreach ($env as $key => $val) { - $name = ENV_PREFIX . $key; - putenv("$name=$val"); - } -} -// 自动识别调试模式 -if (!defined('APP_DEBUG')) { - $debug = getenv(ENV_PREFIX . 'APP_DEBUG'); - define('APP_DEBUG', $debug); -} - -// 加载模式定义文件 -$mode = require MODE_PATH . APP_MODE . EXT; - -// 加载模式命名空间定义 -if (isset($mode['namespace'])) { - Loader::addNamespace($mode['namespace']); -} - -// 注册自动加载 -Loader::register(); - -// 加载模式别名定义 -if (isset($mode['alias'])) { - Loader::addMap($mode['alias']); -} - -// 注册错误和异常处理机制 -Error::register(); - -// 加载模式配置文件 -if (isset($mode['config'])) { - is_array($mode['config']) ? Config::set($mode['config']) : Config::load($mode['config']); -} - -// 是否开启HOOK -defined('APP_HOOK') or define('APP_HOOK', false); - -// 加载模式行为定义 -if (APP_HOOK && isset($mode['tags'])) { - Hook::import($mode['tags']); -} - -// 是否自动运行 -if (APP_AUTO_RUN) { - App::run(); -} +// 执行应用 +App::run()->send(); diff --git a/tests/.gitignore b/tests/.gitignore new file mode 100644 index 00000000..a0306ecc --- /dev/null +++ b/tests/.gitignore @@ -0,0 +1,4 @@ +/runtime/ +/application/common.php +/application/demo/ +/application/runtime/ \ No newline at end of file diff --git a/tests/application/database.php b/tests/application/database.php index 885ebf84..24434efb 100644 --- a/tests/application/database.php +++ b/tests/application/database.php @@ -32,7 +32,7 @@ return [ // 数据库表前缀 'prefix' => '', // 数据库调试模式 - 'debug' => APP_DEBUG, + 'debug' => true, // 数据库部署方式:0 集中式(单一服务器),1 分布式(主从服务器) 'deploy' => 0, // 数据库读写是否分离 主从式有效 diff --git a/tests/mock.php b/tests/mock.php index 536d559d..28471ee2 100644 --- a/tests/mock.php +++ b/tests/mock.php @@ -15,10 +15,6 @@ $_SERVER['REQUEST_METHOD'] = 'GET'; define('TEST_PATH', __DIR__ . '/'); // 定义项目路径 define('APP_PATH', __DIR__ . '/application/'); -// 开启调试模式 -define('APP_DEBUG', true); -// 关闭应用自动执行 -define('APP_AUTO_RUN', false); -// 加载框架引导文件 -require __DIR__ . '/../start.php'; +// 加载框架基础文件 +require __DIR__ . '/../base.php'; \think\Loader::addNamespace('tests', TEST_PATH); diff --git a/tests/thinkphp/baseTest.php b/tests/thinkphp/baseTest.php index 408974bc..18f1d3dd 100644 --- a/tests/thinkphp/baseTest.php +++ b/tests/thinkphp/baseTest.php @@ -16,42 +16,24 @@ class baseTest extends \PHPUnit_Framework_TestCase { public function testConstants() { - $this->assertNotEmpty(START_TIME); - $this->assertNotEmpty(START_MEM); + $this->assertNotEmpty(THINK_START_TIME); + $this->assertNotEmpty(THINK_START_MEM); $this->assertNotEmpty(THINK_VERSION); $this->assertNotEmpty(DS); $this->assertNotEmpty(THINK_PATH); $this->assertNotEmpty(LIB_PATH); $this->assertNotEmpty(EXTEND_PATH); - $this->assertNotEmpty(MODE_PATH); $this->assertNotEmpty(CORE_PATH); $this->assertNotEmpty(TRAIT_PATH); $this->assertNotEmpty(APP_PATH); - $this->assertNotEmpty(APP_NAMESPACE); - $this->assertNotEmpty(COMMON_MODULE); $this->assertNotEmpty(RUNTIME_PATH); $this->assertNotEmpty(LOG_PATH); $this->assertNotEmpty(CACHE_PATH); $this->assertNotEmpty(TEMP_PATH); $this->assertNotEmpty(VENDOR_PATH); $this->assertNotEmpty(EXT); - $this->assertNotEmpty(MODEL_LAYER); - $this->assertNotEmpty(VIEW_LAYER); - $this->assertNotEmpty(CONTROLLER_LAYER); - $this->assertTrue(is_bool(APP_DEBUG)); - $this->assertTrue(is_bool(APP_HOOK)); $this->assertNotEmpty(ENV_PREFIX); - $this->assertTrue(is_bool(IS_API)); - $this->assertNotEmpty(APP_MODE); - $this->assertTrue(!is_null(IS_CGI)); $this->assertTrue(!is_null(IS_WIN)); $this->assertTrue(!is_null(IS_CLI)); - $this->assertTrue(is_bool(IS_AJAX)); - $this->assertNotEmpty(NOW_TIME); - $this->assertNotEmpty(REQUEST_METHOD); - $this->assertTrue(is_bool(IS_GET)); - $this->assertTrue(is_bool(IS_POST)); - $this->assertTrue(is_bool(IS_PUT)); - $this->assertTrue(is_bool(IS_DELETE)); } } diff --git a/tests/thinkphp/library/think/appTest.php b/tests/thinkphp/library/think/appTest.php index ddf47076..366b4f55 100644 --- a/tests/thinkphp/library/think/appTest.php +++ b/tests/thinkphp/library/think/appTest.php @@ -19,6 +19,7 @@ namespace tests\thinkphp\library\think; use ReflectionClass; use think\App; use think\Config; +use think\Request; function func_trim($value) { @@ -47,17 +48,12 @@ class appTest extends \PHPUnit_Framework_TestCase { public function testRun() { - Config::set('root_namespace', ['/path/']); - - App::run(); + $response = App::run(Request::create("http://www.example.com")); $expectOutputString = '

:)

ThinkPHP V5
十年磨一剑 - 为API开发设计的高性能框架

[ V5.0 版本由 七牛云 独家赞助发布 ]
'; - $this->expectOutputString($expectOutputString); - $rc = new ReflectionClass('\think\Loader'); - $ns = $rc->getProperty('namespace'); - $ns->setAccessible(true); - $this->assertEquals(true, in_array('/path/', $ns->getValue())); + $this->assertEquals($expectOutputString, $response->getContent()); + $this->assertEquals(200, $response->getCode()); $this->assertEquals(true, function_exists('lang')); $this->assertEquals(true, function_exists('config')); @@ -86,12 +82,10 @@ class appTest extends \PHPUnit_Framework_TestCase // 类method调度 public function testInvokeMethod() { - $_GET = ['thinkphp']; - $result = App::invokeMethod(['tests\thinkphp\library\think\AppInvokeMethodTestClass', 'run']); + $result = App::invokeMethod(['tests\thinkphp\library\think\AppInvokeMethodTestClass', 'run'], ['thinkphp']); $this->assertEquals('thinkphp', $result); - $_GET = ['thinkphp']; - $result = App::invokeMethod('tests\thinkphp\library\think\AppInvokeMethodTestClass::staticRun'); + $result = App::invokeMethod('tests\thinkphp\library\think\AppInvokeMethodTestClass::staticRun', ['thinkphp']); $this->assertEquals('thinkphp', $result); } } diff --git a/tests/thinkphp/library/think/cache/driver/liteTest.php b/tests/thinkphp/library/think/cache/driver/liteTest.php new file mode 100644 index 00000000..b52bdff9 --- /dev/null +++ b/tests/thinkphp/library/think/cache/driver/liteTest.php @@ -0,0 +1,69 @@ + +// +---------------------------------------------------------------------- + +/** + * Lite缓存驱动测试 + * @author 刘志淳 + */ + +namespace tests\thinkphp\library\think\cache\driver; + +use think\Cache; + +class liteTest extends \PHPUnit_Framework_TestCase +{ + protected function getCacheInstance() + { + return Cache::connect(['type' => 'Lite', 'path' => CACHE_PATH]); + } + + /** + * 测试缓存读取 + * @return mixed + * @access public + */ + public function testGet() + { + $cache = $this->getCacheInstance(); + $this->assertFalse($cache->get('test')); + } + + /** + * 测试缓存设置 + * @return mixed + * @access public + */ + public function testSet() + { + $cache = $this->getCacheInstance(); + $this->assertNotEmpty($cache->set('test', 'test')); + } + + /** + * 删除缓存测试 + * @return mixed + * @access public + */ + public function testRm() + { + $cache = $this->getCacheInstance(); + $this->assertTrue($cache->rm('test')); + } + + /** + * 清空缓存测试 + * @return mixed + * @access public + */ + public function testClear() + { + } +} diff --git a/tests/thinkphp/library/think/cache/driver/memcachedTest.php b/tests/thinkphp/library/think/cache/driver/memcachedTest.php index 05b0877f..95435097 100644 --- a/tests/thinkphp/library/think/cache/driver/memcachedTest.php +++ b/tests/thinkphp/library/think/cache/driver/memcachedTest.php @@ -47,4 +47,8 @@ class memcachedTest extends cacheTestCase public function testExpire() { } + + public function testStaticCall() + { + } } diff --git a/tests/thinkphp/library/think/config/driver/fixtures/config.json b/tests/thinkphp/library/think/config/driver/fixtures/config.json new file mode 100644 index 00000000..716ed064 --- /dev/null +++ b/tests/thinkphp/library/think/config/driver/fixtures/config.json @@ -0,0 +1 @@ +{"jsonstring":1} \ No newline at end of file diff --git a/tests/thinkphp/library/think/config/driver/jsonTest.php b/tests/thinkphp/library/think/config/driver/jsonTest.php new file mode 100644 index 00000000..9d438d40 --- /dev/null +++ b/tests/thinkphp/library/think/config/driver/jsonTest.php @@ -0,0 +1,33 @@ + +// +---------------------------------------------------------------------- + +/** + * Xml配置测试 + * @author 7IN0SAN9 + */ + +namespace tests\thinkphp\library\think\config\driver; + +use think\config; + +class jsonTest extends \PHPUnit_Framework_TestCase +{ + public function testParse() + { + Config::parse('{"jsonstring":1}', 'json'); + $this->assertEquals(1, Config::get('jsonstring')); + Config::reset(); + Config::parse(__DIR__ . '/fixtures/config.json'); + $this->assertTrue(Config::has('jsonstring')); + $this->assertEquals(1, Config::get('jsonstring')); + Config::reset(); + } +} diff --git a/tests/thinkphp/library/think/configTest.php b/tests/thinkphp/library/think/configTest.php index f0048a44..4b581776 100644 --- a/tests/thinkphp/library/think/configTest.php +++ b/tests/thinkphp/library/think/configTest.php @@ -96,7 +96,7 @@ class configTest extends \PHPUnit_Framework_TestCase // test $_ENV configuration $name = 'test_name'; $value = 'value'; - putenv(ENV_PREFIX . $name . '=' . $value); + putenv(ENV_PREFIX . strtoupper($name) . '=' . $value); $this->assertEquals($value, Config::get($name, $range)); // test getting configuration $reflectedPropertyConfig->setValue([$range => ['abcd' => 'efg']]); diff --git a/tests/thinkphp/library/think/controllerTest.php b/tests/thinkphp/library/think/controllerTest.php index 5a4a8397..ce5f49a0 100644 --- a/tests/thinkphp/library/think/controllerTest.php +++ b/tests/thinkphp/library/think/controllerTest.php @@ -18,6 +18,7 @@ namespace tests\thinkphp\library\think; use ReflectionClass; use think\Controller; +use think\Request; use think\View; require_once CORE_PATH . '../../helper.php'; @@ -30,6 +31,48 @@ class Foo extends Controller { $this->test = 'abcd'; } + + public function assignTest() + { + $this->assign('abcd', 'dcba'); + $this->assign(['key1' => 'value1', 'key2' => 'value2']); + } + + public function fetchTest() + { + $template = dirname(__FILE__) . '/display.html'; + return $this->fetch($template, ['name' => 'ThinkPHP']); + } + + public function displayTest() + { + $template = dirname(__FILE__) . '/display.html'; + return $this->display($template, ['name' => 'ThinkPHP']); + } + public function test() + { + $data = [ + 'username' => 'username', + 'nickname' => 'nickname', + 'password' => '123456', + 'repassword' => '123456', + 'email' => 'abc@abc.com', + 'sex' => '0', + 'age' => '20', + 'code' => '1234', + ]; + + $validate = [ + ['username', 'length:5,15', '用户名长度为5到15个字符'], + ['nickname', 'require', '请填昵称'], + ['password', '[\w-]{6,15}', '密码长度为6到15个字符'], + ['repassword', 'confirm:password', '两次密码不一到致'], + ['email', 'filter:validate_email', '邮箱格式错误'], + ['sex', 'in:0,1', '性别只能为为男或女'], + ['age', 'between:1,80', '年龄只能在10-80之间'], + ]; + return $this->validate($data, $validate); + } } class Bar extends Controller @@ -87,22 +130,20 @@ class Baz extends Controller } } -define('ACTION_NAME', 'index'); - class controllerTest extends \PHPUnit_Framework_TestCase { public function testInitialize() { - $foo = new Foo; + $foo = new Foo(Request::instance()); $this->assertEquals('abcd', $foo->test); } public function testBeforeAction() { - $obj = new Bar; + $obj = new Bar(Request::instance()); $this->assertEquals(7, $obj->test); - $obj = new Baz; + $obj = new Baz(Request::instance()); $this->assertEquals(19, $obj->test); } @@ -118,12 +159,11 @@ class controllerTest extends \PHPUnit_Framework_TestCase public function testFetch() { - $controller = new Foo; + $controller = new Foo(Request::instance()); $view = $this->getView($controller); $template = dirname(__FILE__) . '/display.html'; $viewFetch = $view->fetch($template, ['name' => 'ThinkPHP']); - $controllerFetch = $controller->fetch($template, ['name' => 'ThinkPHP']); - $this->assertEquals($controllerFetch, $viewFetch); + $this->assertEquals($controller->fetchTest(), $viewFetch); } public function testDisplay() @@ -132,44 +172,23 @@ class controllerTest extends \PHPUnit_Framework_TestCase $view = $this->getView($controller); $template = dirname(__FILE__) . '/display.html'; $viewFetch = $view->display($template, ['name' => 'ThinkPHP']); - $controllerFetch = $controller->display($template, ['name' => 'ThinkPHP']); - $this->assertEquals($controllerFetch, $viewFetch); + + $this->assertEquals($controller->displayTest(), $viewFetch); } public function testAssign() { - $controller = new Foo; + $controller = new Foo(Request::instance()); $view = $this->getView($controller); - $controller->assign('abcd', 'dcba'); - $controller->assign(['key1' => 'value1', 'key2' => 'value2']); + $controller->assignTest(); $expect = ['abcd' => 'dcba', 'key1' => 'value1', 'key2' => 'value2']; $this->assertAttributeEquals($expect, 'data', $view); } public function testValidate() { - $controller = new Foo; - $data = [ - 'username' => 'username', - 'nickname' => 'nickname', - 'password' => '123456', - 'repassword' => '123456', - 'email' => 'abc@abc.com', - 'sex' => '0', - 'age' => '20', - 'code' => '1234', - ]; - - $validate = [ - ['username', 'length:5,15', '用户名长度为5到15个字符'], - ['nickname', 'require', '请填昵称'], - ['password', '[\w-]{6,15}', '密码长度为6到15个字符'], - ['repassword', 'confirm:password', '两次密码不一到致'], - ['email', 'filter:validate_email', '邮箱格式错误'], - ['sex', 'in:0,1', '性别只能为为男或女'], - ['age', 'between:1,80', '年龄只能在10-80之间'], - ]; - $result = $controller->validate($data, $validate); + $controller = new Foo(Request::instance()); + $result = $controller->test(); $this->assertTrue($result); } } diff --git a/tests/thinkphp/library/think/debugTest.php b/tests/thinkphp/library/think/debugTest.php index 8a9ac863..12ff815f 100644 --- a/tests/thinkphp/library/think/debugTest.php +++ b/tests/thinkphp/library/think/debugTest.php @@ -170,7 +170,7 @@ class debugTest extends \PHPUnit_Framework_TestCase $array = explode("array", json_encode($output)); if (IS_WIN) { $this->assertEquals("(1) {\\n [\\\"key\\\"] => string(3) \\\"val\\\"\\n}\\n\\r\\n\"", end($array)); - } else if (IS_MAC) { + } else if (strstr(PHP_OS, 'Darwin')) { $this->assertEquals("(1) {\\n [\\\"key\\\"] => string(3) \\\"val\\\"\\n}\\n\\n\"", end($array)); } else { $this->assertEquals("(1) {\\n 'key' =>\\n string(3) \\\"val\\\"\\n}\\n\\n\"", end($array)); diff --git a/tests/thinkphp/library/think/exceptionTest.php b/tests/thinkphp/library/think/exceptionTest.php index e6aa13f8..66957ce6 100644 --- a/tests/thinkphp/library/think/exceptionTest.php +++ b/tests/thinkphp/library/think/exceptionTest.php @@ -18,6 +18,7 @@ namespace tests\thinkphp\library\think; use ReflectionMethod; use think\Exception as ThinkException; +use think\exception\HttpException; class MyException extends ThinkException { @@ -29,9 +30,9 @@ class exceptionTest extends \PHPUnit_Framework_TestCase public function testGetHttpStatus() { try { - throw new ThinkException("Error Processing Request", 1); - } catch (ThinkException $e) { - $this->assertEquals(500, $e->getHttpStatus()); + throw new HttpException(404, "Error Processing Request"); + } catch (HttpException $e) { + $this->assertEquals(404, $e->getStatusCode()); } } diff --git a/tests/thinkphp/library/think/inputTest.php b/tests/thinkphp/library/think/inputTest.php deleted file mode 100644 index 608d888f..00000000 --- a/tests/thinkphp/library/think/inputTest.php +++ /dev/null @@ -1,194 +0,0 @@ - -// +---------------------------------------------------------------------- - -/** - * Input测试 - * @author Haotong Lin - */ - -namespace tests\thinkphp\library\think; - -use think\Input; - -class inputTest extends \PHPUnit_Framework_TestCase -{ - - public function testInputName() - { - $input = ['a' => 'a', 'b' => ['c' => [' one ', 'two']]]; - $this->assertEquals($input, Input::data($input)); - $this->assertEquals($input['a'], Input::data($input, 'a')); - $this->assertEquals('one', Input::data($input, 'b.c.0/s', 'default', 'trim')); - } - - public function testDefaultValue() - { - $input = ['a' => 'test']; - $default = 'default'; - $this->assertEquals($default, Input::data($input, 'b', $default)); - $this->assertEquals($default, Input::get('a', $default)); - } - - public function testStringFilter() - { - $input = ['a' => ' test ', 'b' => ' test<> ']; - $filters = 'trim'; - $this->assertEquals('test', Input::data($input, 'a', '', $filters)); - $filters = 'trim,htmlspecialchars'; - $this->assertEquals('test<>', Input::data($input, 'b', '', $filters)); - } - - public function testArrayFilter() - { - $input = ['a' => ' test ', 'b' => ' test<> ']; - $filters = ['trim']; - $this->assertEquals('test', Input::data($input, 'a', '', $filters)); - $filters = ['trim', 'htmlspecialchars']; - $this->assertEquals('test<>', Input::data($input, 'b', '', $filters)); - } - - public function testFilterExp() - { - $src = 'EXP|NEQ|GT|EGT|LT|ELT|OR|XOR|LIKE|NOTLIKE|NOT BETWEEN|NOTBETWEEN|BETWEEN|NOTIN|NOT IN|IN'; - $regexs = explode('|', $src); - $data = Input::data($regexs); - foreach ($regexs as $key => $value) { - $expected = $value . ' '; - $this->assertEquals($expected, $data[$key]); - } - } - - public function testFiltrateWithRegex() - { - $input = ['a' => 'test1', 'b' => '_test2', 'c' => '']; - $filters = '/^test/'; - $this->assertEquals('test1', Input::data($input, 'a', '', $filters)); - $default = 'default value'; - $this->assertEquals($default, Input::data($input, 'b', $default, $filters)); - $filters = '/.+/'; - $this->assertEquals('default value', Input::data($input, 'c', $default, $filters)); - } - - public function testFiltrateWithFilterVar() - { - $email = 'abc@gmail.com'; - $error = 'not email'; - $default = false; - $input = ['a' => $email, 'b' => $error]; - $filters = FILTER_VALIDATE_EMAIL; - $this->assertEquals($email, Input::data($input, 'a', '', $filters)); - $this->assertFalse(Input::data($input, 'b', $default, $filters)); - $filters = 'validate_email'; - $this->assertFalse(Input::data($input, 'b', $default, $filters)); - } - - public function testAllInput() - { - $input = [ - 'a' => ' trim ', - 'b' => 'htmlspecialchars<>', - 'c' => ' trim htmlspecialchars<> ', - 'd' => 'eXp', - 'e' => 'NEQ', - 'f' => 'gt', - ]; - $filters = 'htmlspecialchars,trim'; - $excepted = [ - 'a' => 'trim', - 'b' => 'htmlspecialchars<>', - 'c' => 'trim htmlspecialchars<>', - 'd' => 'eXp ', - 'e' => 'NEQ ', - 'f' => 'gt ', - ]; - $this->assertEquals($excepted, Input::data($input, '', '', $filters)); - } - - public function testTypeCast() - { - $_POST = [ - 'a' => [1, 2, 3], - 'b' => '1000', - 'c' => '3.14', - 'd' => 'test boolean', - ]; - $this->assertEquals([1, 2, 3], Input::post('a/a')); - $this->assertEquals(1000, Input::post('b/d')); - $this->assertEquals(3.14, Input::post('c/f')); - $this->assertEquals(true, Input::post('d/b')); - } - - public function testHasValue() - { - $_GET['name'] = 'value'; - $_GET['config']['name'] = 'value'; - $this->assertEquals(true, Input::get('?name')); - $this->assertEquals(false, Input::get('?id')); - $this->assertEquals(true, Input::get('?config.name')); - $this->assertEquals(false, Input::get('?config.id')); - } - - public function testSuperglobals() - { - Input::setFilter('trim'); - $_GET['get'] = 'get value '; - $this->assertEquals('get value', Input::get('get')); - $_POST['post'] = 'post value '; - $this->assertEquals('post value', Input::post('post')); - - $_SERVER['REQUEST_METHOD'] = 'POST'; - $this->assertEquals('post value', Input::param('post')); - $this->assertEquals(null, Input::param('get')); - $_SERVER['REQUEST_METHOD'] = 'GET'; - $this->assertEquals('get value', Input::param('get')); - $this->assertEquals(null, Input::param('post')); - $this->assertEquals(null, Input::param('put')); - $_REQUEST = array_merge($_GET, $_POST); - $this->assertEquals('get value', Input::request('get')); - - //session_start(); - $_SESSION['test'] = 'session value '; - $this->assertEquals('session value', Input::session('test')); - //session_destroy(); - - $_COOKIE['cookie'] = 'cookie value '; - $this->assertEquals('cookie value', Input::cookie('cookie')); - - $_SERVER['REQUEST_METHOD'] = 'GET '; - $this->assertEquals('GET', Input::server('REQUEST_METHOD')); - - $GLOBALS['total'] = 1000; - $this->assertEquals(1000, Input::globals('total')); - - $this->assertEquals('testing', Input::env('APP_ENV')); - - //$_SERVER['PATH_INFO'] = 'path/info'; - //$path = $_SERVER['PATH_INFO'] ? explode('/', $_SERVER['PATH_INFO'])[0] : ''; - //$this->assertEquals($path, Input::path('0', '')); - - $_FILES = ['file' => ['name' => 'test.png', 'type' => 'image/png', 'tmp_name' => '/tmp/php5Wx0aJ', 'error' => 0, 'size' => 15726]]; - $this->assertEquals(null, Input::file('file.type')); - - } - - public function testFilterMerge() - { - Input::setFilter('htmlspecialchars'); - $input = ['a' => ' test<> ', 'b' => '']; - $this->assertEquals(' test<> ', Input::data($input, 'a', '', '')); - $filters = ['trim']; - $this->assertEquals('test<>', Input::data($input, 'a', '', $filters)); - $this->assertEquals('test<>', Input::data($input, 'a', '', $filters, true)); - $filters = 'stripslashes'; - $this->assertEquals("<bar />", Input::data($input, 'b', '', $filters, true)); - } - -} diff --git a/tests/thinkphp/library/think/loaderTest.php b/tests/thinkphp/library/think/loaderTest.php index 373a320b..a33c30c9 100644 --- a/tests/thinkphp/library/think/loaderTest.php +++ b/tests/thinkphp/library/think/loaderTest.php @@ -23,17 +23,14 @@ class loaderTest extends \PHPUnit_Framework_TestCase public function testAutoload() { - $this->assertEquals(true, Loader::autoload('think\Session')); - //$this->assertEquals(false, Loader::autoload('think\COOKIE')); $this->assertEquals(false, Loader::autoload('\think\Url')); $this->assertEquals(false, Loader::autoload('think\Test')); $this->assertEquals(false, Loader::autoload('my\HelloTest')); } - public function testAddMap() + public function testAddClassMap() { - Loader::addMap('my\hello\Test', 'Test.php'); - $this->assertEquals(false, Loader::autoload('my\hello\Test')); + Loader::addClassMap('my\hello\Test', __DIR__ . DS . 'loader' . DS . 'Test.php'); } public function testAddNamespace() @@ -54,14 +51,8 @@ class loaderTest extends \PHPUnit_Framework_TestCase Loader::db('mysql://root@127.0.0.1/test#utf8'); } - public function testInstance() - { - Loader::instance('\think\Validate'); - } - public function testImport() { - $this->assertEquals(true, Loader::import('think.log.driver.Sae')); $this->assertEquals(false, Loader::import('think.log.driver.MyTest')); } diff --git a/tests/thinkphp/library/think/log/driver/fileTest.php b/tests/thinkphp/library/think/log/driver/fileTest.php index 168ef81a..3453c09b 100644 --- a/tests/thinkphp/library/think/log/driver/fileTest.php +++ b/tests/thinkphp/library/think/log/driver/fileTest.php @@ -29,6 +29,6 @@ class fileTest extends \PHPUnit_Framework_TestCase Log::record($record_msg, 'notice'); $logs = Log::getLog(); - $this->assertNotFalse(array_search(['type' => 'notice', 'msg' => $record_msg], $logs)); + $this->assertNotFalse(array_search($record_msg, $logs['notice'])); } } diff --git a/tests/thinkphp/library/think/logTest.php b/tests/thinkphp/library/think/logTest.php index 3448b49e..554cf60d 100644 --- a/tests/thinkphp/library/think/logTest.php +++ b/tests/thinkphp/library/think/logTest.php @@ -20,29 +20,32 @@ use think\Log; class logTest extends \PHPUnit_Framework_TestCase { - public function testRecord(){ + public function testRecord() + { Log::clear(); Log::record('test'); - $this->assertEquals([['type'=>'log','msg'=>'test']], Log::getLog()); - Log::record('hello','info'); - $this->assertEquals([['type'=>'log','msg'=>'test'],['type'=>'info','msg'=>'hello']], Log::getLog()); + $this->assertEquals(['log' => ['test']], Log::getLog()); + Log::record('hello', 'info'); + $this->assertEquals(['log' => ['test'], 'info' => ['hello']], Log::getLog()); Log::clear(); Log::info('test'); - $this->assertEquals([['type'=>'info','msg'=>'test']], Log::getLog()); + $this->assertEquals(['info' => ['test']], Log::getLog()); } - public function testSave(){ - Log::init(['type'=>'test']); + public function testSave() + { + Log::init(['type' => 'test']); Log::clear(); Log::record('test'); - Log::record([1,2,3]); + Log::record([1, 2, 3]); $this->assertTrue(Log::save()); } - public function testWrite(){ - Log::init(['type'=>'test']); + public function testWrite() + { + Log::init(['type' => 'test']); Log::clear(); - $this->assertTrue(Log::write('hello','info')); - $this->assertTrue(Log::write([1,2,3],'log')); + $this->assertTrue(Log::write('hello', 'info')); + $this->assertTrue(Log::write([1, 2, 3], 'log')); } } diff --git a/tests/thinkphp/library/think/paginateTest.php b/tests/thinkphp/library/think/paginateTest.php new file mode 100644 index 00000000..949b8c97 --- /dev/null +++ b/tests/thinkphp/library/think/paginateTest.php @@ -0,0 +1,44 @@ + +// +---------------------------------------------------------------------- + +namespace tests\thinkphp\library\think; + + +use think\paginator\driver\Bootstrap; + +class paginateTest extends \PHPUnit_Framework_TestCase +{ + public function testPaginatorInfo() + { + $p = Bootstrap::make($array = ['item3', 'item4'], 2, 2, 4); + + $this->assertEquals(4, $p->total()); + + $this->assertEquals(2, $p->listRows()); + + $this->assertEquals(2, $p->currentPage()); + + $p2 = Bootstrap::make($array2 = ['item3', 'item4'], 2, 2, 2); + $this->assertEquals(1, $p2->currentPage()); + } + + public function testPaginatorRender() + { + $p = Bootstrap::make($array = ['item3', 'item4'], 2, 2, 100); + $render = ''; + + $this->assertEquals($render, $p->render()); + } + + + + +} \ No newline at end of file diff --git a/tests/thinkphp/library/think/responseTest.php b/tests/thinkphp/library/think/responseTest.php index f2b5d6a6..ea2ccd56 100644 --- a/tests/thinkphp/library/think/responseTest.php +++ b/tests/thinkphp/library/think/responseTest.php @@ -70,7 +70,6 @@ class responseTest extends \PHPUnit_Framework_TestCase { Config::set('default_ajax_return', $this->default_ajax_return); Config::set('default_return_type', $this->default_return_type); - Response::type(Config::get('default_return_type')); // 会影响其他测试 } /** @@ -79,130 +78,16 @@ class responseTest extends \PHPUnit_Framework_TestCase */ public function testSend() { - $dataArr = array(); + $dataArr = []; $dataArr["key"] = "value"; - //$dataArr->key = "val"; - $result = Response::send($dataArr, "", true); - $this->assertArrayHasKey("key", $result); - - $result = Response::send($dataArr, "json", true); + $response = Response::create($dataArr, 'json'); + $result = $response->getContent(); $this->assertEquals('{"key":"value"}', $result); - - $handler = "callback"; - $_GET[Config::get('var_jsonp_handler')] = $handler; - $result = Response::send($dataArr, "jsonp", true); + $_GET['callback'] = 'callback'; + $response = Response::create($dataArr, 'jsonp'); + $result = $response->options(['var_jsonp_handler' => 'callback'])->getContent(); $this->assertEquals('callback({"key":"value"});', $result); - - Response::transform(function () { - - return "callbackreturndata"; - }); - - $result = Response::send($dataArr, "", true); - $this->assertEquals("callbackreturndata", $result); - $_GET[Config::get('var_jsonp_handler')] = ""; - } - - /** - * @covers think\Response::transform - * @todo Implement testtransform(). - */ - public function testtransform() - { - Response::transform(function () { - - return "callbackreturndata"; - }); - $dataArr = []; - $result = Response::send($dataArr, "", true); - $this->assertEquals("callbackreturndata", $result); - - Response::transform(null); - } - - /** - * @covers think\Response::type - * @todo Implement testType(). - */ - public function testType() - { - $type = "json"; - Response::type($type); - } - - /** - * @covers think\Response::data - * @todo Implement testData(). - */ - public function testData() - { - $data = "data"; - Response::data($data); - Response::data(null); - } - - /** - * @covers think\Response::isExit - * @todo Implement testIsExit(). - */ - public function testIsExit() - { - $isExit = true; - Response::isExit($isExit); - - $result = Response::isExit(); - $this->assertTrue($isExit, $result); - Response::isExit(false); - } - - /** - * @covers think\Response::result - * @todo Implement testResult(). - */ - public function testResult() - { - $data = "data"; - $code = "1001"; - $msg = "the msg"; - $type = "json"; - $result = Response::result($data, $code, $msg, $type); - - $this->assertEquals($code, $result["code"]); - $this->assertEquals($msg, $result["msg"]); - $this->assertEquals($data, $result["data"]); - $this->assertEquals($_SERVER['REQUEST_TIME'], $result["time"]); - } - - /** - * @#runInSeparateProcess - * @covers think\Response::redirect - * @todo Implement testRedirect(). - */ - public function testRedirect() - { - // $url = "http://www.testredirect.com"; - // $params = array(); - // $params[] = 301; - - // // FIXME 静态方法mock Url::build - // // echo "\r\n" . json_encode(xdebug_get_headers()) . "\r\n"; - // Response::redirect($url, $params); - - // $this->assertContains('Location: ' . $url, xdebug_get_headers()); - } - - /** - * @#runInSeparateProcess - * @covers think\Response::header - * @todo Implement testHeader(). - */ - public function testHeader() - { - // $name = "Location"; - // $url = "http://www.testheader.com/"; - // Response::header($name, $url); - // $this->assertContains($name . ': ' . $url, xdebug_get_headers()); } } diff --git a/tests/thinkphp/library/think/routeTest.php b/tests/thinkphp/library/think/routeTest.php index def9b611..34926993 100644 --- a/tests/thinkphp/library/think/routeTest.php +++ b/tests/thinkphp/library/think/routeTest.php @@ -16,175 +16,203 @@ namespace tests\thinkphp\library\think; +use think\Config; +use think\Request; use think\Route; class routeTest extends \PHPUnit_Framework_TestCase { + protected function setUp() + { + Config::set('app_multi_module', true); + } + public function testRegister() { - + $request = Request::instance(); Route::get('hello/:name', 'index/hello'); Route::get(['hello/:name' => 'index/hello']); Route::post('hello/:name', 'index/post'); Route::put('hello/:name', 'index/put'); Route::delete('hello/:name', 'index/delete'); Route::any('user/:id', 'index/user'); - $this->assertEquals(['type' => 'module', 'module' => [null, 'index', 'hello']], Route::check('hello/thinkphp')); - $this->assertEquals(['hello/:name' => ['route' => 'index/hello', 'option' => [], 'pattern' => []]], Route::getRules('GET')); - Route::register('type/:name', 'index/type', 'PUT|POST'); + $result = Route::check($request, 'hello/thinkphp'); + $this->assertEquals([null, 'index', 'hello'], $result['module']); + $this->assertEquals(['hello' => true, 'user/:id' => true, 'hello/:name' => ['rule' => 'hello/:name', 'route' => 'index/hello', 'var' => ['name' => 1], 'option' => [], 'pattern' => []]], Route::rules('GET')); + Route::rule('type/:name', 'index/type', 'PUT|POST'); } public function testResource() { + $request = Request::instance(); Route::resource('res', 'index/blog'); Route::resource(['res' => ['index/blog']]); - - $this->assertEquals(['type' => 'module', 'module' => ['index', 'blog', 'index']], Route::check('res')); - $this->assertEquals(['type' => 'module', 'module' => ['index', 'blog', 'create']], Route::check('res/create')); - $this->assertEquals(['type' => 'module', 'module' => ['index', 'blog', 'read']], Route::check('res/8')); - $this->assertEquals(['type' => 'module', 'module' => ['index', 'blog', 'edit']], Route::check('res/8/edit')); + $result = Route::check($request, 'res'); + $this->assertEquals(['index', 'blog', 'index'], $result['module']); + $result = Route::check($request, 'res/create'); + $this->assertEquals(['index', 'blog', 'create'], $result['module']); + $result = Route::check($request, 'res/8'); + $this->assertEquals(['index', 'blog', 'read'], $result['module']); + $result = Route::check($request, 'res/8/edit'); + $this->assertEquals(['index', 'blog', 'edit'], $result['module']); Route::resource('blog.comment', 'index/comment'); - $this->assertEquals(['type' => 'module', 'module' => ['index', 'comment', 'read']], Route::check('blog/8/comment/10')); - $this->assertEquals(['type' => 'module', 'module' => ['index', 'comment', 'edit']], Route::check('blog/8/comment/10/edit')); + $result = Route::check($request, 'blog/8/comment/10'); + $this->assertEquals(['index', 'comment', 'read'], $result['module']); + $result = Route::check($request, 'blog/8/comment/10/edit'); + $this->assertEquals(['index', 'comment', 'edit'], $result['module']); } public function testRest() { + $request = Request::instance(); Route::rest('read', ['GET', '/:id', 'look']); Route::rest('create', ['GET', '/create', 'add']); Route::rest(['read' => ['GET', '/:id', 'look'], 'create' => ['GET', '/create', 'add']]); Route::resource('res', 'index/blog'); + $result = Route::check($request, 'res/create'); + $this->assertEquals(['index', 'blog', 'add'], $result['module']); + $result = Route::check($request, 'res/8'); + $this->assertEquals(['index', 'blog', 'look'], $result['module']); - $this->assertEquals(['type' => 'module', 'module' => ['index', 'blog', 'add']], Route::check('res/create')); - $this->assertEquals(['type' => 'module', 'module' => ['index', 'blog', 'look']], Route::check('res/8')); - - } - - public function testRouteMap() - { - Route::map('hello', 'index/hello'); - $this->assertEquals('index/hello', Route::map('hello')); - $this->assertEquals(['type' => 'module', 'module' => ['index', 'hello', null]], Route::check('hello')); } public function testMixVar() { + $request = Request::instance(); Route::get('hello-', 'index/hello', [], ['name' => '\w+']); - $this->assertEquals(['type' => 'module', 'module' => [null, 'index', 'hello']], Route::check('hello-thinkphp')); + $result = Route::check($request, 'hello-thinkphp'); + $this->assertEquals([null, 'index', 'hello'], $result['module']); Route::get('hello-', 'index/hello', [], ['name' => '\w+', 'id' => '\d+']); - $this->assertEquals(['type' => 'module', 'module' => [null, 'index', 'hello']], Route::check('hello-thinkphp2016')); + $result = Route::check($request, 'hello-thinkphp2016'); + $this->assertEquals([null, 'index', 'hello'], $result['module']); Route::get('hello-/[:id]', 'index/hello', [], ['name' => '\w+', 'id' => '\d+']); - $this->assertEquals(['type' => 'module', 'module' => [null, 'index', 'hello']], Route::check('hello-thinkphp/2016')); + $result = Route::check($request, 'hello-thinkphp/2016'); + $this->assertEquals([null, 'index', 'hello'], $result['module']); } public function testParseUrl() { - $this->assertEquals(['type' => 'module', 'module' => ['hello', null, null]], Route::parseUrl('hello')); - $this->assertEquals(['type' => 'module', 'module' => ['index', 'hello', null]], Route::parseUrl('index/hello')); - $this->assertEquals(['type' => 'module', 'module' => ['index', 'hello', null]], Route::parseUrl('index/hello?name=thinkphp')); - $this->assertEquals(['type' => 'module', 'module' => ['index', 'user', 'hello']], Route::parseUrl('index/user/hello')); - $this->assertEquals(['type' => 'module', 'module' => ['index', 'user', 'hello']], Route::parseUrl('index/user/hello/name/thinkphp')); - $this->assertEquals(['type' => 'module', 'module' => ['index', 'index', 'hello']], Route::parseUrl('index-index-hello', '-')); + $result = Route::parseUrl('hello'); + $this->assertEquals(['hello', null, null], $result['module']); + $result = Route::parseUrl('index/hello'); + $this->assertEquals(['index', 'hello', null], $result['module']); + $result = Route::parseUrl('index/hello?name=thinkphp'); + $this->assertEquals(['index', 'hello', null], $result['module']); + $result = Route::parseUrl('index/user/hello'); + $this->assertEquals(['index', 'user', 'hello'], $result['module']); + $result = Route::parseUrl('index/user/hello/name/thinkphp'); + $this->assertEquals(['index', 'user', 'hello'], $result['module']); + $result = Route::parseUrl('index-index-hello', '-'); + $this->assertEquals(['index', 'index', 'hello'], $result['module']); } public function testCheckRoute() { Route::get('hello/:name', 'index/hello'); Route::get('blog/:id', 'blog/read', [], ['id' => '\d+']); - - $this->assertEquals(false, Route::check('test/thinkphp')); - $this->assertEquals(false, Route::check('blog/thinkphp')); - $this->assertEquals(['type' => 'module', 'module' => [null, 'blog', 'read']], Route::check('blog/5')); - $this->assertEquals(['type' => 'module', 'module' => [null, 'index', 'hello']], Route::check('hello/thinkphp/abc/test')); + $request = Request::instance(); + $this->assertEquals(false, Route::check($request, 'test/thinkphp')); + $this->assertEquals(false, Route::check($request, 'blog/thinkphp')); + $result = Route::check($request, 'blog/5'); + $this->assertEquals([null, 'blog', 'read'], $result['module']); + $result = Route::check($request, 'hello/thinkphp/abc/test'); + $this->assertEquals([null, 'index', 'hello'], $result['module']); } public function testCheckRouteGroup() { + $request = Request::instance(); Route::pattern(['id' => '\d+', 'name' => '\w{6,25}']); Route::group('group', [':id' => 'index/hello', ':name' => 'index/say']); - $this->assertEquals(false, Route::check('empty/think')); - $this->assertEquals(['type' => 'module', 'module' => [null, 'index', 'say']], Route::check('group/think')); - $this->assertEquals(['type' => 'module', 'module' => [null, 'index', 'hello']], Route::check('group/10')); - $this->assertEquals(['type' => 'module', 'module' => [null, 'index', 'say']], Route::check('group/thinkphp')); + $this->assertEquals(false, Route::check($request, 'empty/think')); + $result = Route::check($request, 'group/think'); + $this->assertEquals(false, $result['module']); + $result = Route::check($request, 'group/10'); + $this->assertEquals([null, 'index', 'hello'], $result['module']); + $result = Route::check($request, 'group/thinkphp'); + $this->assertEquals([null, 'index', 'say'], $result['module']); } public function testRouteToModule() { + $request = Request::instance(); Route::get('hello/:name', 'index/hello'); Route::get('blog/:id', 'blog/read', [], ['id' => '\d+']); - $this->assertEquals(false, Route::check('test/thinkphp')); - $this->assertEquals(false, Route::check('blog/thinkphp')); - $this->assertEquals(['type' => 'module', 'module' => [null, 'index', 'hello']], Route::check('hello/thinkphp')); - $this->assertEquals(['type' => 'module', 'module' => [null, 'blog', 'read']], Route::check('blog/5')); + $this->assertEquals(false, Route::check($request, 'test/thinkphp')); + $this->assertEquals(false, Route::check($request, 'blog/thinkphp')); + $result = Route::check($request, 'hello/thinkphp'); + $this->assertEquals([null, 'index', 'hello'], $result['module']); + $result = Route::check($request, 'blog/5'); + $this->assertEquals([null, 'blog', 'read'], $result['module']); } public function testRouteToController() { + $request = Request::instance(); Route::get('say/:name', '@app\index\controller\index\hello'); - $this->assertEquals(['type' => 'controller', 'controller' => 'app\index\controller\index\hello', 'params' => ['name' => 'thinkphp']], Route::check('say/thinkphp')); + $this->assertEquals(['type' => 'controller', 'controller' => 'app\index\controller\index\hello', 'params' => ['name' => 'thinkphp']], Route::check($request, 'say/thinkphp')); } public function testRouteToMethod() { + $request = Request::instance(); Route::get('user/:name', '\app\index\service\User::get', [], ['name' => '\w+']); - Route::get('info/:name', ['\app\index\model\Info', 'getInfo'], [], ['name' => '\w+']); - $this->assertEquals(['type' => 'method', 'method' => '\app\index\service\User::get', 'params' => ['name' => 'thinkphp']], Route::check('user/thinkphp')); - $this->assertEquals(['type' => 'method', 'method' => ['\app\index\model\Info', 'getInfo'], 'params' => ['name' => 'thinkphp']], Route::check('info/thinkphp')); + Route::get('info/:name', '\app\index\model\Info@getInfo', [], ['name' => '\w+']); + $this->assertEquals(['type' => 'method', 'method' => '\app\index\service\User::get', 'params' => ['name' => 'thinkphp']], Route::check($request, 'user/thinkphp')); + $this->assertEquals(['type' => 'method', 'method' => ['\app\index\model\Info', 'getInfo'], 'params' => ['name' => 'thinkphp']], Route::check($request, 'info/thinkphp')); } public function testRouteToRedirect() { + $request = Request::instance(); Route::get('art/:id', '/article/read/id/:id', [], ['id' => '\d+']); - $this->assertEquals(['type' => 'redirect', 'url' => '/article/read/id/8', 'status' => 301], Route::check('art/8')); + $this->assertEquals(['type' => 'redirect', 'url' => '/article/read/id/8', 'status' => 301], Route::check($request, 'art/8')); } public function testBind() { - Route::bind('module', 'index/blog'); - $this->assertEquals(['type' => 'module', 'module' => ['index', 'blog', 'read']], Route::parseUrl('read/10')); + $request = Request::instance(); + Route::bind('index/blog'); + $result = Route::parseUrl('read/10'); + $this->assertEquals(['index', 'blog', 'read'], $result['module']); Route::get('index/blog/:id', 'index/blog/read'); - $this->assertEquals(['type' => 'module', 'module' => ['index', 'blog', 'read']], Route::check('10')); + $result = Route::check($request, '10'); + $this->assertEquals(['index', 'blog', 'read'], $result['module']); - Route::bind('namespace', '\app\index\controller'); - $this->assertEquals(['type' => 'method', 'method' => ['\app\index\controller\blog', 'read'], 'params' => []], Route::check('blog/read')); + Route::bind('\app\index\controller', 'namespace'); + $this->assertEquals(['type' => 'method', 'method' => ['\app\index\controller\blog', 'read'], 'params' => []], Route::check($request, 'blog/read')); - Route::bind('class', '\app\index\controller\blog'); - $this->assertEquals(['type' => 'method', 'method' => ['\app\index\controller\blog', 'read'], 'params' => []], Route::check('read')); - } - - public function testSsl() - { - $this->assertEquals(false, Route::isSsl()); + Route::bind('\app\index\controller\blog', 'class'); + $this->assertEquals(['type' => 'method', 'method' => ['\app\index\controller\blog', 'read'], 'params' => []], Route::check($request, 'read')); } public function testDomain() { - $_SERVER['HTTP_HOST'] = 'subdomain.thinkphp.cn'; - $_SERVER['REQUEST_URI'] = ''; + $request = Request::create('http://subdomain.thinkphp.cn'); Route::domain('subdomain.thinkphp.cn', 'sub?abc=test&status=1'); - Route::checkDomain(); - $this->assertEquals('sub?abc=test&status=1', Route::domain('subdomain.thinkphp.cn')); - $this->assertEquals('sub', Route::bind('module')); + Route::checkDomain($request); + $this->assertEquals('sub?abc=test&status=1', Route::rules('domain')['subdomain.thinkphp.cn']); + $this->assertEquals('sub', Route::getbind('module')); $this->assertEquals('test', $_GET['abc']); $this->assertEquals(1, $_GET['status']); Route::domain('subdomain.thinkphp.cn', function () {return ['type' => 'module', 'module' => 'sub2'];}); - Route::checkDomain(); - $this->assertEquals('sub2', Route::bind('module')); + Route::checkDomain($request); + $this->assertEquals('sub2', Route::getbind('module')); Route::domain('subdomain.thinkphp.cn', '\app\index\controller'); - Route::checkDomain(); - $this->assertEquals('\app\index\controller', Route::bind('namespace')); + Route::checkDomain($request); + $this->assertEquals('\app\index\controller', Route::getbind('namespace')); Route::domain('subdomain.thinkphp.cn', '@\app\index\controller\blog'); - Route::checkDomain(); - $this->assertEquals('\app\index\controller\blog', Route::bind('class')); + Route::checkDomain($request); + $this->assertEquals('\app\index\controller\blog', Route::getbind('class')); Route::domain('subdomain.thinkphp.cn', '[sub3]'); - Route::checkDomain(); - $this->assertEquals('sub3', Route::bind('group')); + Route::checkDomain($request); + $this->assertEquals('sub3', Route::getbind('group')); } } diff --git a/tests/thinkphp/library/think/sessionTest.php b/tests/thinkphp/library/think/sessionTest.php index 50ae99b6..76c18ef0 100644 --- a/tests/thinkphp/library/think/sessionTest.php +++ b/tests/thinkphp/library/think/sessionTest.php @@ -156,14 +156,13 @@ class sessionTest extends \PHPUnit_Framework_TestCase 'use_cookies' => '1', 'cache_limiter' => '60', 'cache_expire' => '60', - 'type' => 'memcache', // - 'namespace' => '\\think\\session\\driver\\', // ? + 'type' => '\\think\\session\\driver\\Memcache', // 'auto_start' => '1', ]; // 测试session驱动是否存在 // @expectedException 异常类名 - $this->setExpectedException('\think\Exception', 'error session handler', 11700); + $this->setExpectedException('\think\exception\ClassNotFoundException', 'error session handler'); Session::init($config); } diff --git a/tests/thinkphp/library/think/template/taglib/cxTest.php b/tests/thinkphp/library/think/template/taglib/cxTest.php index 34f296a6..a77824b6 100644 --- a/tests/thinkphp/library/think/template/taglib/cxTest.php +++ b/tests/thinkphp/library/think/template/taglib/cxTest.php @@ -447,38 +447,11 @@ EOF; $template = new template(); $cx = new Cx($template); - $content = << -EOF; - $cx->parseTag($content); - $this->assertEquals($content, $data); - - $content = << -EOF; - $cx->parseTag($content); - $this->assertEquals($content, $data); - - $content = << -EOF; - $cx->parseTag($content); - $this->assertEquals($content, $data); - $content = << + EOF; $cx->parseTag($content); $this->assertEquals($content, $data); @@ -562,7 +535,16 @@ EOF; $template->display($content); $this->expectOutputString('123456789'); } - + public function testUrl() + { + $template = new template(); + $content = <<display($content); + $this->expectOutputString(\think\Url::build('Index/index')); + } + public function testFunction() { $template = new template(); diff --git a/tests/thinkphp/library/think/templateTest.php b/tests/thinkphp/library/think/templateTest.php index 9b21028f..593b73dc 100644 --- a/tests/thinkphp/library/think/templateTest.php +++ b/tests/thinkphp/library/think/templateTest.php @@ -78,7 +78,7 @@ EOF; {\$name.a==\$name.b?='test'} EOF; $data = << + EOF; $template->parse($content); @@ -88,7 +88,7 @@ EOF; {\$name.a==\$name.b?'a':'b'} EOF; $data = << + EOF; $template->parse($content); @@ -98,7 +98,7 @@ EOF; {\$name.a|default='test'==\$name.b?'a':'b'} EOF; $data = << + EOF; $template->parse($content); @@ -210,7 +210,7 @@ EOF; <#\$info.a??'test'#> EOF; $data = <<a) ? (is_array(\$info)?\$info['a']:\$info->a) : 'test'; ?> +a)) ? (is_array(\$info)?\$info['a']:\$info->a) : 'test'; ?> EOF; $template->parse($content); diff --git a/tests/thinkphp/library/think/urlTest.php b/tests/thinkphp/library/think/urlTest.php index 151cd4d6..1a8e22f8 100644 --- a/tests/thinkphp/library/think/urlTest.php +++ b/tests/thinkphp/library/think/urlTest.php @@ -26,21 +26,23 @@ class urlTest extends \PHPUnit_Framework_TestCase public function testBuildModule() { - Route::get('hello/:name', 'index/hello'); - Route::get('hello/:id', 'index/hello'); + Route::get('blog/:name', 'index/blog'); + Route::get('blog/:id', 'index/blog'); Config::set('pathinfo_depr', '/'); - $this->assertEquals('/hello/thinkphp', Url::build('index/hello?name=thinkphp')); - $this->assertEquals('/hello/thinkphp.html', Url::build('index/hello', 'name=thinkphp', 'html')); - $this->assertEquals('/hello/10', Url::build('index/hello?id=10')); - $this->assertEquals('/hello/10.html', Url::build('index/hello', 'id=10', 'html')); + Config::set('url_html_suffix', ''); + $this->assertEquals('/blog/thinkphp', Url::build('index/blog?name=thinkphp')); + $this->assertEquals('/blog/thinkphp.html', Url::build('index/blog', 'name=thinkphp', 'html')); + $this->assertEquals('/blog/10', Url::build('index/blog?id=10')); + $this->assertEquals('/blog/10.html', Url::build('index/blog', 'id=10', 'html')); - Route::get('item-', 'good/item', [], ['item' => '\w+', 'id' => '\d+']); - $this->assertEquals('/item-thinkphp', Url::build('good/item?item=thinkphp')); - $this->assertEquals('/item-thinkphp2016', Url::build('good/item?item=thinkphp&id=2016')); + Route::get('item-', 'blog/item', [], ['name' => '\w+', 'id' => '\d+']); + $this->assertEquals('/item-thinkphp', Url::build('blog/item?name=thinkphp')); + $this->assertEquals('/item-thinkphp2016', Url::build('blog/item?name=thinkphp&id=2016')); } public function testBuildController() { + Config::set('url_html_suffix', ''); Route::get('blog/:id', '@index/blog/read'); $this->assertEquals('/blog/10.html', Url::build('@index/blog/read', 'id=10', 'html')); @@ -70,5 +72,8 @@ class urlTest extends \PHPUnit_Framework_TestCase Route::get('blog/:id', 'index/blog'); Config::set('url_html_suffix', 'shtml'); $this->assertEquals('/blog/10.shtml#detail', Url::build('/blog/10#detail')); + + Config::set('url_common_param', true); + $this->assertEquals('/blog/10.shtml?foo=bar#detail', Url::build('/blog/10#detail', "foo=bar")); } } diff --git a/tpl/dispatch_jump.tpl b/tpl/dispatch_jump.tpl index 9b568fa1..18ee01bd 100644 --- a/tpl/dispatch_jump.tpl +++ b/tpl/dispatch_jump.tpl @@ -1,4 +1,4 @@ - +{__NOLAYOUT__} diff --git a/tpl/think_exception.tpl b/tpl/think_exception.tpl index 8b7e1f3d..a978f1de 100644 --- a/tpl/think_exception.tpl +++ b/tpl/think_exception.tpl @@ -1,3 +1,80 @@ +'.end($names).''; + } + } + + if(!function_exists('parse_file')){ + function parse_file($file, $line) + { + return ''.basename($file)." line {$line}".''; + } + } + + if(!function_exists('parse_args')){ + function parse_args($args) + { + $result = []; + + foreach ($args as $key => $item) { + switch (true) { + case is_object($item): + $value = sprintf('object(%s)', parse_class(get_class($item))); + break; + case is_array($item): + if(count($item) > 3){ + $value = sprintf('[%s, ...]', parse_args(array_slice($item, 0, 3))); + } else { + $value = sprintf('[%s]', parse_args($item)); + } + break; + case is_string($item): + if(strlen($item) > 20){ + $value = sprintf( + '\'%s...\'', + htmlentities($item), + htmlentities(substr($item, 0, 20)) + ); + } else { + $value = sprintf("'%s'", htmlentities($item)); + } + break; + case is_int($item): + case is_float($item): + $value = $item; + break; + case is_null($item): + $value = 'null'; + break; + case is_bool($item): + $value = '' . ($item ? 'true' : 'false') . ''; + break; + case is_resource($item): + $value = 'resource'; + break; + default: + $value = htmlentities(str_replace("\n", '', var_export(strval($item), true))); + break; + } + + $result[] = is_int($key) ? $value : "'{$key}' => {$value}"; + } + + return implode(', ', $result); + } + } +?> @@ -6,11 +83,11 @@ - +
+ +
+
@@ -211,7 +315,7 @@ isset($value['class']) ? parse_class($value['class']) : '', isset($value['type']) ? $value['type'] : '', $value['function'], - parse_args($value['args']) + isset($value['args'])?parse_args($value['args']):'' ); } @@ -252,8 +356,10 @@ echo htmlentities(json_encode($val, JSON_PRETTY_PRINT)); } else if(is_bool($val)) { echo $val ? 'true' : 'false'; - } else { + } else if(is_scalar($val)) { echo htmlentities($val); + } else { + echo 'Resource'; } ?> @@ -285,8 +391,10 @@ echo htmlentities(json_encode($val, JSON_PRETTY_PRINT)); } else if(is_bool($val)) { echo $val ? 'true' : 'false'; - } else { + } else if(is_scalar($val)) { echo htmlentities($val); + } else { + echo 'Resource'; } ?> @@ -304,7 +412,7 @@ V { 十年磨一剑-为API开发设计的高性能框架 }
- + - -'.end($names).''; - } - - function parse_file($file, $line) - { - return ''.basename($file)." line {$line}".''; - } - - function parse_args($args) - { - $result = []; - - foreach ($args as $key => $item) { - switch (true) { - case is_object($item): - $value = sprintf('object(%s)', parse_class(get_class($item))); - break; - case is_array($item): - if(count($item) > 3){ - $value = sprintf('[%s, ...]', parse_args(array_slice($item, 0, 3))); - } else { - $value = sprintf('[%s]', parse_args($item)); - } - break; - case is_string($item): - if(strlen($item) > 20){ - $value = sprintf( - '\'%s...\'', - htmlentities($item), - htmlentities(substr($item, 0, 20)) - ); - } else { - $value = sprintf("'%s'", htmlentities($item)); - } - break; - case is_int($item): - case is_float($item): - $value = $item; - break; - case is_null($item): - $value = 'null'; - break; - case is_bool($item): - $value = '' . ($item ? 'true' : 'false') . ''; - break; - case is_resource($item): - $value = 'resource'; - break; - default: - $value = htmlentities(str_replace("\n", '', var_export(strval($item), true))); - break; - } - - $result[] = is_int($key) ? $value : "'{$key}' => {$value}"; - } - - return implode(', ', $result); - } + \ No newline at end of file