diff --git a/.travis.yml b/.travis.yml index 96a9dffc16809e3cf75dcd5beca37ee2c92f7856..f74ffca1173a7c656b9f2d8ee0a82f4a59af8784 100644 --- a/.travis.yml +++ b/.travis.yml @@ -36,8 +36,6 @@ install: script: ## LINT - find . -path ./vendor -prune -o -type f -name \*.php -exec php -l {} \; - ## PHP_CodeSniffer - - 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 - vendor/bin/phpcpd --verbose --exclude vendor ./ || true ## PHPLOC diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6cefcb38cb2321eb2cd3f8c059bb873a4025a632..dc8e91cd53b4012d82a5d7bfc0da17e7643443c8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -7,7 +7,7 @@ ThinkPHP 目前使用 Git 来控制程序版本,如果你想为 ThinkPHP 贡献源代码,请先大致了解 Git 的使用方法。我们目前把项目托管在 GitHub 上,任何 GitHub 用户都可以向我们贡献代码。 -参与的方式很简单,`fork`一份 ThinkPHP 的代码到你的仓库中,修改后提交,并向我们发起`pull request`申请,我们会及时对代码进行审查并处理你的申请并。审查通过后,你的代码将被`merge`进我们的仓库中,这样你就会自动出现在贡献者名单里了,非常方便。 +参与的方式很简单,`fork`一份 ThinkPHP 的代码到你的仓库中,修改后提交,并向我们发起`pull request`申请,我们会及时对代码进行审查并处理你的申请。审查通过后,你的代码将被`merge`进我们的仓库中,这样你就会自动出现在贡献者名单里了,非常方便。 我们希望你贡献的代码符合: @@ -60,7 +60,7 @@ GitHub 提供了 Issue 功能,该功能可以用于: 6. 变基(衍合 `rebase`)你的分支到上游 master 分支; 7. `push` 你的本地仓库到 GitHub; 8. 提交 `pull request`; -9. 等待 CI 验证(若不通过则重复 5~7,GitHub 会自动更新你的 `pull request`); +9. 等待 CI 验证(若不通过则重复 5~7,不需要重新提交 `pull request`,GitHub 会自动更新你的 `pull request`); 10. 等待管理员处理,并及时 `rebase` 你的分支到上游 master 分支(若上游 master 分支有修改)。 *若有必要,可以 `git push -f` 强行推送 rebase 后的分支到自己的 `fork`* diff --git a/LICENSE.txt b/LICENSE.txt index 574a39c401ff71ffcb90d15edb906b8046c7661b..2cb9a8a9f76d3d0dd47bd454a0ff6c8c0633343f 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,6 +1,6 @@ ThinkPHP遵循Apache2开源协议发布,并提供免费使用。 -版权所有Copyright © 2006-2016 by ThinkPHP (http://thinkphp.cn) +版权所有Copyright © 2006-2017 by ThinkPHP (http://thinkphp.cn) All rights reserved。 ThinkPHP® 商标和著作权所有者为上海顶想信息科技有限公司。 diff --git a/README.md b/README.md index ee0676a059bcaecfdb790245a9b1c6efebf29c03..f01fd2b961ff6fb7069fe9a84c56ff68237c1e26 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ ThinkPHP 5.0 =============== +[![StyleCI](https://styleci.io/repos/48530411/shield?style=flat&branch=master)](https://styleci.io/repos/48530411) [![Build Status](https://travis-ci.org/top-think/framework.svg?branch=master)](https://travis-ci.org/top-think/framework) [![codecov.io](http://codecov.io/github/top-think/framework/coverage.svg?branch=master)](http://codecov.io/github/github/top-think/framework?branch=master) [![Total Downloads](https://poser.pugx.org/topthink/framework/downloads)](https://packagist.org/packages/topthink/framework) @@ -8,7 +9,7 @@ ThinkPHP 5.0 [![Latest Unstable Version](https://poser.pugx.org/topthink/framework/v/unstable)](https://packagist.org/packages/topthink/framework) [![License](https://poser.pugx.org/topthink/framework/license)](https://packagist.org/packages/topthink/framework) -ThinkPHP5在保持快速开发和大道至简的核心理念不变的同时,PHP版本要求提升到5.4,对已有的CBD模式做了更深的强化,优化核心,减少依赖,基于全新的架构思想和命名空间实现,是ThinkPHP突破原有框架思路的颠覆之作,其主要特性包括: +ThinkPHP5在保持快速开发和大道至简的核心理念不变的同时,PHP版本要求提升到5.4,优化核心,减少依赖,基于全新的架构思想和命名空间实现,是ThinkPHP突破原有框架思路的颠覆之作,其主要特性包括: + 基于命名空间和众多PHP新特性 + 核心功能组件化 @@ -27,10 +28,11 @@ ThinkPHP5在保持快速开发和大道至简的核心理念不变的同时,PH + 真正惰性加载 + 分布式环境支持 + 支持Composer + + 支持MongoDb > ThinkPHP5的运行环境要求PHP5.4以上。 -详细开发文档参考 [ThinkPHP5完全开发手册](http://www.kancloud.cn/manual/thinkphp5) +详细开发文档参考 [ThinkPHP5完全开发手册](http://www.kancloud.cn/manual/thinkphp5) 以及[ThinkPHP5入门系列教程](http://www.kancloud.cn/special/thinkphp5_quickstart) ## 目录结构 @@ -90,7 +92,7 @@ www WEB部署目录(或者子目录) ## 命名规范 -ThinkPHP5的命名规范遵循PSR-2规范以及PSR-4自动加载规范。 +ThinkPHP5的命名规范遵循`PSR-2`规范以及`PSR-4`自动加载规范。 ## 参与开发 注册并登录 Github 帐号, fork 本项目并进行改动。 @@ -103,7 +105,7 @@ ThinkPHP遵循Apache2开源协议发布,并提供免费使用。 本项目包含的第三方源码和二进制文件之版权信息另行标注。 -版权所有Copyright © 2006-2016 by ThinkPHP (http://thinkphp.cn) +版权所有Copyright © 2006-2018 by ThinkPHP (http://thinkphp.cn) All rights reserved。 diff --git a/base.php b/base.php index 1184dc51f6034088dc1931df018c645ad7e180e4..92c4fa5576626c71693af8ccecfb511a74034730 100644 --- a/base.php +++ b/base.php @@ -2,14 +2,14 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2016 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- // | Author: liu21st // +---------------------------------------------------------------------- -define('THINK_VERSION', '5.0.2dev'); +define('THINK_VERSION', '5.0.24'); define('THINK_START_TIME', microtime(true)); define('THINK_START_MEM', memory_get_usage()); define('EXT', '.php'); @@ -40,8 +40,10 @@ require CORE_PATH . 'Loader.php'; // 加载环境变量配置文件 if (is_file(ROOT_PATH . '.env')) { $env = parse_ini_file(ROOT_PATH . '.env', true); + foreach ($env as $key => $val) { $name = ENV_PREFIX . strtoupper($key); + if (is_array($val)) { foreach ($val as $k => $v) { $item = $name . '_' . strtoupper($k); diff --git a/composer.json b/composer.json index 444d6889aea6fb341d19ecb31b39c974267c5b40..c546e1142329c68812d8b04a9c9a6bb0fef36809 100644 --- a/composer.json +++ b/composer.json @@ -25,7 +25,11 @@ "mikey179/vfsStream": "~1.6", "phploc/phploc": "2.*", "sebastian/phpcpd": "2.*", - "squizlabs/php_codesniffer": "2.*", "phpdocumentor/reflection-docblock": "^2.0" + }, + "autoload": { + "psr-4": { + "think\\": "library/think" + } } } diff --git a/console.php b/console.php index 3d518e78fae75e2ec0474feed2f60fa972393ae0..578e4a7c45c8cba0a635633b9a15df2f8c07df84 100644 --- a/console.php +++ b/console.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK IT ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006-2016 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006-2017 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- @@ -17,4 +17,4 @@ require __DIR__ . '/base.php'; // 执行应用 App::initCommon(); -Console::init(); \ No newline at end of file +Console::init(); diff --git a/convention.php b/convention.php index f7486a4805bf72b38d90d6edef562068874eeac4..31a0a0c1110695728532bb8a9681bb68b99cfd99 100644 --- a/convention.php +++ b/convention.php @@ -4,11 +4,10 @@ return [ // +---------------------------------------------------------------------- // | 应用设置 // +---------------------------------------------------------------------- - - // 应用命名空间 - 'app_namespace' => 'app', + // 默认Host地址 + 'app_host' => '', // 应用调试模式 - 'app_debug' => true, + 'app_debug' => false, // 应用Trace 'app_trace' => false, // 应用模式状态 @@ -58,6 +57,8 @@ return [ 'default_validate' => '', // 默认的空控制器名 'empty_controller' => 'Error', + // 操作方法前缀 + 'use_action_prefix' => false, // 操作方法后缀 'action_suffix' => '', // 自动搜索控制器 @@ -73,6 +74,8 @@ return [ 'pathinfo_fetch' => ['ORIG_PATH_INFO', 'REDIRECT_PATH_INFO', 'REDIRECT_URL'], // pathinfo分隔符 'pathinfo_depr' => '/', + // HTTPS代理标识 + 'https_agent_name' => '', // URL伪静态后缀 'url_html_suffix' => 'html', // URL普通方式参数 用于自动生成 @@ -83,6 +86,8 @@ return [ 'url_route_on' => true, // 路由配置文件(支持配置多个) 'route_config_file' => ['route'], + // 路由使用完整匹配 + 'route_complete_match' => false, // 是否强制使用路由 'url_route_must' => false, // 域名部署 @@ -99,15 +104,25 @@ return [ 'var_ajax' => '_ajax', // 表单pjax伪装变量 'var_pjax' => '_pjax', + // 是否开启请求缓存 true自动缓存 支持设置请求缓存规则 + 'request_cache' => false, + // 请求缓存有效期 + 'request_cache_expire' => null, + // 全局请求缓存排除规则 + 'request_cache_except' => [], // +---------------------------------------------------------------------- // | 模板设置 // +---------------------------------------------------------------------- 'template' => [ + // 默认模板渲染规则 1 解析为小写+下划线 2 全部转换小写 + 'auto_rule' => 1, // 模板引擎类型 支持 php think 支持扩展 'type' => 'Think', - // 模板路径 + // 视图基础目录,配置目录为所有模块的视图起始目录 + 'view_base' => '', + // 当前模板的视图目录 留空为自动获取 'view_path' => '', // 模板后缀 'view_suffix' => 'html', @@ -142,6 +157,8 @@ return [ 'show_error_msg' => false, // 异常处理handle类 留空使用 \think\exception\Handle 'exception_handle' => '', + // 是否记录trace信息到日志 + 'record_trace' => false, // +---------------------------------------------------------------------- // | 日志设置 @@ -193,6 +210,8 @@ return [ 'type' => '', // 是否自动开启 SESSION 'auto_start' => true, + 'httponly' => true, + 'secure' => false, ], // +---------------------------------------------------------------------- @@ -221,39 +240,45 @@ return [ 'database' => [ // 数据库类型 - 'type' => 'mysql', + 'type' => 'mysql', // 数据库连接DSN配置 - 'dsn' => '', + 'dsn' => '', // 服务器地址 - 'hostname' => 'localhost', + 'hostname' => '127.0.0.1', // 数据库名 - '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, + 'fields_strict' => true, + // 数据集返回类型 + 'resultset_type' => 'array', // 自动写入时间戳字段 - 'auto_timestamp' => false, + 'auto_timestamp' => false, + // 时间字段取出后的默认时间格式 + 'datetime_format' => 'Y-m-d H:i:s', + // 是否需要进行SQL性能分析 + 'sql_explain' => false, ], //分页配置 @@ -263,4 +288,11 @@ return [ 'list_rows' => 15, ], + //控制台配置 + 'console' => [ + 'name' => 'Think Console', + 'version' => '0.1', + 'user' => null, + ], + ]; diff --git a/helper.php b/helper.php index 6ab2cb348c361536350164a24e3976477bca654f..12683cfd82fb8ed6e3e440bee46638d2cc4c1e2d 100644 --- a/helper.php +++ b/helper.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2016 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- @@ -23,6 +23,7 @@ use think\exception\HttpResponseException; use think\Lang; use think\Loader; use think\Log; +use think\Model; use think\Request; use think\Response; use think\Session; @@ -117,7 +118,7 @@ if (!function_exists('input')) { * @param string $filter 过滤方法 * @return mixed */ - function input($key = '', $default = null, $filter = null) + function input($key = '', $default = null, $filter = '') { if (0 === strpos($key, '?')) { $key = substr($key, 1); @@ -125,10 +126,9 @@ if (!function_exists('input')) { } if ($pos = strpos($key, '.')) { // 指定参数来源 - $method = substr($key, 0, $pos); - if (in_array($method, ['get', 'post', 'put', 'patch', 'delete', 'param', 'request', 'session', 'cookie', 'server', 'env', 'path', 'file'])) { - $key = substr($key, $pos + 1); - } else { + list($method, $key) = explode('.', $key, 2); + if (!in_array($method, ['get', 'post', 'put', 'patch', 'delete', 'route', 'param', 'request', 'session', 'cookie', 'server', 'env', 'path', 'file'])) { + $key = $method . '.' . $key; $method = 'param'; } } else { @@ -192,7 +192,7 @@ if (!function_exists('db')) { * @param bool $force 是否强制重新连接 * @return \think\db\Query */ - function db($name = '', $config = [], $force = true) + function db($name = '', $config = [], $force = false) { return Db::connect($config, $force)->name($name); } @@ -298,7 +298,7 @@ if (!function_exists('session')) { Session::init($name); } elseif (is_null($name)) { // 清除 - Session::clear($value); + Session::clear('' === $value ? null : $value); } elseif ('' === $value) { // 判断或获取 return 0 === strpos($name, '?') ? Session::has(substr($name, 1), $prefix) : Session::get($name, $prefix); @@ -330,7 +330,7 @@ if (!function_exists('cookie')) { Cookie::clear($value); } elseif ('' === $value) { // 获取 - return 0 === strpos($name, '?') ? Cookie::has(substr($name, 1), $option) : Cookie::get($name); + return 0 === strpos($name, '?') ? Cookie::has(substr($name, 1), $option) : Cookie::get($name, $option); } elseif (is_null($value)) { // 删除 return Cookie::delete($name); @@ -354,17 +354,25 @@ if (!function_exists('cache')) { { if (is_array($options)) { // 缓存操作的同时初始化 - Cache::connect($options); + $cache = Cache::connect($options); } elseif (is_array($name)) { // 缓存初始化 return Cache::connect($name); + } else { + $cache = Cache::init(); } - if ('' === $value) { + + if (is_null($name)) { + return $cache->clear($value); + } elseif ('' === $value) { // 获取缓存 - return 0 === strpos($name, '?') ? Cache::has(substr($name, 1)) : Cache::get($name); + return 0 === strpos($name, '?') ? $cache->has(substr($name, 1)) : $cache->get($name); } elseif (is_null($value)) { // 删除缓存 - return Cache::rm($name); + return $cache->rm($name); + } elseif (0 === strpos($name, '?') && '' !== $value) { + $expire = is_numeric($options) ? $options : null; + return $cache->remember(substr($name, 1), $value, $expire); } else { // 缓存数据 if (is_array($options)) { @@ -373,9 +381,9 @@ if (!function_exists('cache')) { $expire = is_numeric($options) ? $options : null; //默认快捷缓存设置过期时间 } if (is_null($tag)) { - return Cache::set($name, $value, $expire); + return $cache->set($name, $value, $expire); } else { - return Cache::tag($tag)->set($name, $value, $expire); + return $cache->tag($tag)->set($name, $value, $expire); } } } @@ -490,15 +498,16 @@ if (!function_exists('redirect')) { * @param mixed $url 重定向地址 支持Url::build方法的地址 * @param array|integer $params 额外参数 * @param integer $code 状态码 + * @param array $with 隐式传参 * @return \think\response\Redirect */ - function redirect($url = [], $params = [], $code = 302) + function redirect($url = [], $params = [], $code = 302, $with = []) { if (is_integer($params)) { $code = $params; $params = []; } - return Response::create($url, 'redirect', $code)->params($params); + return Response::create($url, 'redirect', $code)->params($params)->with($with); } } @@ -544,3 +553,37 @@ if (!function_exists('token')) { return ''; } } + +if (!function_exists('load_relation')) { + /** + * 延迟预载入关联查询 + * @param mixed $resultSet 数据集 + * @param mixed $relation 关联 + * @return array + */ + function load_relation($resultSet, $relation) + { + $item = current($resultSet); + if ($item instanceof Model) { + $item->eagerlyResultSet($resultSet, $relation); + } + return $resultSet; + } +} + +if (!function_exists('collection')) { + /** + * 数组转换为数据集对象 + * @param array $resultSet 数据集数组 + * @return \think\model\Collection|\think\Collection + */ + function collection($resultSet) + { + $item = current($resultSet); + if ($item instanceof Model) { + return \think\model\Collection::make($resultSet); + } else { + return \think\Collection::make($resultSet); + } + } +} diff --git a/lang/zh-cn.php b/lang/zh-cn.php index db43a1c418b68d9596e680caed00eaa9b2919ab1..eb7a9142ad868db2827fcf6f837dd966c078211d 100644 --- a/lang/zh-cn.php +++ b/lang/zh-cn.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2016 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- @@ -12,54 +12,125 @@ // 核心中文语言包 return [ // 系统错误提示 - 'Undefined variable' => '未定义变量', - 'Undefined index' => '未定义数组索引', - 'Undefined offset' => '未定义数组下标', - 'Parse error' => '语法解析错误', - 'Type error' => '类型错误', - 'Fatal error' => '致命错误', - 'syntax error' => '语法错误', + 'Undefined variable' => '未定义变量', + 'Undefined index' => '未定义数组索引', + 'Undefined offset' => '未定义数组下标', + 'Parse error' => '语法解析错误', + 'Type error' => '类型错误', + 'Fatal error' => '致命错误', + 'syntax 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 写入错误', - 'route name not exists' => '路由标识不存在(或参数不够)', - 'invalid request' => '非法请求', + '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' => '当前访问路由未定义', + 'Undefined 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' => '查询表达式错误', + 'not support data' => '不支持的数据表达式', + '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 写入错误', + 'route name not exists' => '路由标识不存在(或参数不够)', + 'invalid request' => '非法请求', + 'bind attr has exists' => '模型的属性已经存在', + 'relation data not exists' => '关联数据不存在', + 'relation not support' => '关联不支持', + 'chunk not support order' => 'Chunk不支持调用order方法', + 'closure not support cache(true)' => '使用闭包查询不支持cache(true),请指定缓存Key', + + // 上传错误信息 + 'unknown upload error' => '未知上传错误!', + 'file write error' => '文件写入失败!', + 'upload temp dir not found' => '找不到临时文件夹!', + 'no file to uploaded' => '没有文件被上传!', + 'only the portion of file is uploaded' => '文件只有部分被上传!', + 'upload File size exceeds the maximum value' => '上传文件大小超过了最大值!', + 'upload write error' => '文件上传保存错误!', + 'has the same filename: {:filename}' => '存在同名文件:{:filename}', + 'upload illegal files' => '非法上传文件', + 'illegal image files' => '非法图片文件', + 'extensions to upload is not allowed' => '上传文件后缀不允许', + 'mimetype to upload is not allowed' => '上传文件MIME类型不允许!', + 'filesize not match' => '上传文件大小不符!', + 'directory {:path} creation failed' => '目录 {:path} 创建失败!', + + // Validate Error Message + ':attribute require' => ':attribute不能为空', + ':attribute must be numeric' => ':attribute必须是数字', + ':attribute must be integer' => ':attribute必须是整数', + ':attribute must be float' => ':attribute必须是浮点数', + ':attribute must be bool' => ':attribute必须是布尔值', + ':attribute not a valid email address' => ':attribute格式不符', + ':attribute not a valid mobile' => ':attribute格式不符', + ':attribute must be a array' => ':attribute必须是数组', + ':attribute must be yes,on or 1' => ':attribute必须是yes、on或者1', + ':attribute not a valid datetime' => ':attribute不是一个有效的日期或时间格式', + ':attribute not a valid file' => ':attribute不是有效的上传文件', + ':attribute not a valid image' => ':attribute不是有效的图像文件', + ':attribute must be alpha' => ':attribute只能是字母', + ':attribute must be alpha-numeric' => ':attribute只能是字母和数字', + ':attribute must be alpha-numeric, dash, underscore' => ':attribute只能是字母、数字和下划线_及破折号-', + ':attribute not a valid domain or ip' => ':attribute不是有效的域名或者IP', + ':attribute must be chinese' => ':attribute只能是汉字', + ':attribute must be chinese or alpha' => ':attribute只能是汉字、字母', + ':attribute must be chinese,alpha-numeric' => ':attribute只能是汉字、字母和数字', + ':attribute must be chinese,alpha-numeric,underscore, dash' => ':attribute只能是汉字、字母、数字和下划线_及破折号-', + ':attribute not a valid url' => ':attribute不是有效的URL地址', + ':attribute not a valid ip' => ':attribute不是有效的IP地址', + ':attribute must be dateFormat of :rule' => ':attribute必须使用日期格式 :rule', + ':attribute must be in :rule' => ':attribute必须在 :rule 范围内', + ':attribute be notin :rule' => ':attribute不能在 :rule 范围内', + ':attribute must between :1 - :2' => ':attribute只能在 :1 - :2 之间', + ':attribute not between :1 - :2' => ':attribute不能在 :1 - :2 之间', + 'size of :attribute must be :rule' => ':attribute长度不符合要求 :rule', + 'max size of :attribute must be :rule' => ':attribute长度不能超过 :rule', + 'min size of :attribute must be :rule' => ':attribute长度不能小于 :rule', + ':attribute cannot be less than :rule' => ':attribute日期不能小于 :rule', + ':attribute cannot exceed :rule' => ':attribute日期不能超过 :rule', + ':attribute not within :rule' => '不在有效期内 :rule', + 'access IP is not allowed' => '不允许的IP访问', + 'access IP denied' => '禁止的IP访问', + ':attribute out of accord with :2' => ':attribute和确认字段:2不一致', + ':attribute cannot be same with :2' => ':attribute和比较字段:2不能相同', + ':attribute must greater than or equal :rule' => ':attribute必须大于等于 :rule', + ':attribute must greater than :rule' => ':attribute必须大于 :rule', + ':attribute must less than or equal :rule' => ':attribute必须小于等于 :rule', + ':attribute must less than :rule' => ':attribute必须小于 :rule', + ':attribute must equal :rule' => ':attribute必须等于 :rule', + ':attribute has exists' => ':attribute已存在', + ':attribute not conform to the rules' => ':attribute不符合指定规则', + 'invalid Request method' => '无效的请求类型', + 'invalid token' => '令牌数据无效', + 'not conform to the rules' => '规则错误', ]; diff --git a/library/think/App.php b/library/think/App.php index 7e140ca96c0081ee49b7b464b43ab3dc3d5f7703..f572b907c3ab3cf6afcad1b3c109fcd273b9bbdd 100644 --- a/library/think/App.php +++ b/library/think/App.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2016 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- @@ -11,22 +11,14 @@ namespace think; -use think\Config; -use think\Env; -use think\Exception; +use think\exception\ClassNotFoundException; 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; +use think\exception\RouteNotFoundException; /** * App 应用管理 - * @author liu21st + * @author liu21st */ class App { @@ -65,24 +57,32 @@ class App */ protected static $routeMust; + /** + * @var array 请求调度分发 + */ protected static $dispatch; + + /** + * @var array 额外加载文件 + */ protected static $file = []; /** * 执行应用程序 * @access public - * @param Request $request Request对象 + * @param Request $request 请求对象 * @return Response * @throws Exception */ public static function run(Request $request = null) { - is_null($request) && $request = Request::instance(); + $request = is_null($request) ? Request::instance() : $request; try { $config = self::initCommon(); + + // 模块/控制器绑定 if (defined('BIND_MODULE')) { - // 模块/控制器绑定 BIND_MODULE && Route::bind(BIND_MODULE); } elseif ($config['auto_bind_module']) { // 入口自动绑定 @@ -94,26 +94,28 @@ class App $request->filter($config['default_filter']); - if ($config['lang_switch_on']) { - // 开启多语言机制 检测当前语言 - Lang::detect(); - } else { - // 读取默认语言 - Lang::range($config['default_lang']); - } + // 默认语言 + Lang::range($config['default_lang']); + // 开启多语言机制 检测当前语言 + $config['lang_switch_on'] && Lang::detect(); $request->langset(Lang::range()); + // 加载系统语言包 Lang::load([ THINK_PATH . 'lang' . DS . $request->langset() . EXT, APP_PATH . 'lang' . DS . $request->langset() . EXT, ]); + // 监听 app_dispatch + Hook::listen('app_dispatch', self::$dispatch); // 获取应用调度信息 $dispatch = self::$dispatch; + + // 未设置调度信息则进行 URL 路由检测 if (empty($dispatch)) { - // 进行URL路由检测 $dispatch = self::routeCheck($request, $config); } + // 记录当前调度信息 $request->dispatch($dispatch); @@ -124,36 +126,17 @@ class App Log::record('[ PARAM ] ' . var_export($request->param(), true), 'info'); } - // 监听app_begin + // 监听 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']); - break; - case 'method': - // 执行回调方法 - $data = self::invokeMethod($dispatch['method']); - break; - case 'function': - // 执行闭包 - $data = self::invokeFunction($dispatch['function']); - break; - case 'response': - $data = $dispatch['response']; - break; - default: - throw new \InvalidArgumentException('dispatch type not support'); - } + // 请求缓存检查 + $request->cache( + $config['request_cache'], + $config['request_cache_expire'], + $config['request_cache_except'] + ); + + $data = self::exec($dispatch, $config); } catch (HttpResponseException $exception) { $data = $exception->getResponse(); } @@ -166,24 +149,151 @@ class App $response = $data; } elseif (!is_null($data)) { // 默认自动识别响应输出类型 - $isAjax = $request->isAjax(); - $type = $isAjax ? Config::get('default_ajax_return') : Config::get('default_return_type'); + $type = $request->isAjax() ? + Config::get('default_ajax_return') : + Config::get('default_return_type'); + $response = Response::create($data, $type); } else { $response = Response::create(); } - // 监听app_end + // 监听 app_end Hook::listen('app_end', $response); return $response; } + /** + * 初始化应用,并返回配置信息 + * @access public + * @return array + */ + public static function initCommon() + { + if (empty(self::$init)) { + if (defined('APP_NAMESPACE')) { + self::$namespace = APP_NAMESPACE; + } + + Loader::addNamespace(self::$namespace, APP_PATH); + + // 初始化应用 + $config = self::init(); + self::$suffix = $config['class_suffix']; + + // 应用调试模式 + self::$debug = Env::get('app_debug', Config::get('app_debug')); + + if (!self::$debug) { + ini_set('display_errors', 'Off'); + } elseif (!IS_CLI) { + // 重新申请一块比较大的 buffer + if (ob_get_level() > 0) { + $output = ob_get_clean(); + } + + ob_start(); + + if (!empty($output)) { + echo $output; + } + + } + + 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) && !isset(self::$file[$file])) { + include $file; + self::$file[$file] = true; + } + } + } + + // 设置系统时区 + date_default_timezone_set($config['default_timezone']); + + // 监听 app_init + Hook::listen('app_init'); + + self::$init = true; + } + + return Config::get(); + } + + /** + * 初始化应用或模块 + * @access public + * @param string $module 模块名 + * @return array + */ + private static function init($module = '') + { + // 定位模块目录 + $module = $module ? $module . DS : ''; + + // 加载初始化文件 + if (is_file(APP_PATH . $module . 'init' . EXT)) { + include APP_PATH . $module . 'init' . EXT; + } elseif (is_file(RUNTIME_PATH . $module . 'init' . EXT)) { + include RUNTIME_PATH . $module . 'init' . EXT; + } else { + // 加载模块配置 + $config = Config::load(CONF_PATH . $module . 'config' . CONF_EXT); + + // 读取数据库配置文件 + $filename = CONF_PATH . $module . 'database' . CONF_EXT; + Config::load($filename, 'database'); + + // 读取扩展配置文件 + if (is_dir(CONF_PATH . $module . 'extra')) { + $dir = CONF_PATH . $module . 'extra'; + $files = scandir($dir); + foreach ($files as $file) { + if ('.' . pathinfo($file, PATHINFO_EXTENSION) === CONF_EXT) { + $filename = $dir . DS . $file; + Config::load($filename, pathinfo($file, PATHINFO_FILENAME)); + } + } + } + + // 加载应用状态配置 + if ($config['app_status']) { + Config::load(CONF_PATH . $module . $config['app_status'] . CONF_EXT); + } + + // 加载行为扩展文件 + if (is_file(CONF_PATH . $module . 'tags' . EXT)) { + Hook::import(include CONF_PATH . $module . 'tags' . EXT); + } + + // 加载公共文件 + $path = APP_PATH . $module; + if (is_file($path . 'common' . EXT)) { + include $path . 'common' . EXT; + } + + // 加载当前模块语言包 + if ($module) { + Lang::load($path . 'lang' . DS . Request::instance()->langset() . EXT); + } + } + + return Config::get(); + } + /** * 设置当前请求的调度信息 * @access public * @param array|string $dispatch 调度信息 - * @param string $type 调度类型 + * @param string $type 调度类型 * @return void */ public static function dispatch($dispatch, $type = 'module') @@ -202,8 +312,10 @@ class App { $reflect = new \ReflectionFunction($function); $args = self::bindParams($reflect, $vars); + // 记录执行信息 self::$debug && Log::record('[ RUN ] ' . $reflect->__toString(), 'info'); + return $reflect->invokeArgs($args); } @@ -217,111 +329,186 @@ class App public static function invokeMethod($method, $vars = []) { if (is_array($method)) { - $class = is_object($method[0]) ? $method[0] : new $method[0](Request::instance()); + $class = is_object($method[0]) ? $method[0] : self::invokeClass($method[0]); $reflect = new \ReflectionMethod($class, $method[1]); } else { // 静态方法 $reflect = new \ReflectionMethod($method); } + $args = self::bindParams($reflect, $vars); - self::$debug && Log::record('[ RUN ] ' . $reflect->__toString(), 'info'); + self::$debug && Log::record('[ RUN ] ' . $reflect->class . '->' . $reflect->name . '[ ' . $reflect->getFileName() . ' ]', 'info'); + return $reflect->invokeArgs(isset($class) ? $class : null, $args); } /** * 调用反射执行类的实例化 支持依赖注入 * @access public - * @param string $class 类名 - * @param array $vars 变量 + * @param string $class 类名 + * @param array $vars 变量 * @return mixed */ public static function invokeClass($class, $vars = []) { $reflect = new \ReflectionClass($class); $constructor = $reflect->getConstructor(); - if ($constructor) { - $args = self::bindParams($constructor, $vars); - } else { - $args = []; - } + $args = $constructor ? self::bindParams($constructor, $vars) : []; - self::$debug && Log::record('[ RUN ] ' . $reflect->__toString(), 'info'); return $reflect->newInstanceArgs($args); } /** * 绑定参数 - * @access public + * @access private * @param \ReflectionMethod|\ReflectionFunction $reflect 反射类 - * @param array $vars 变量 + * @param array $vars 变量 * @return array */ private static function bindParams($reflect, $vars = []) { + // 自动获取请求变量 if (empty($vars)) { - // 自动获取请求变量 - if (Config::get('url_param_type')) { - $vars = Request::instance()->route(); - } else { - $vars = Request::instance()->param(); - } + $vars = Config::get('url_param_type') ? + Request::instance()->route() : + Request::instance()->param(); } + $args = []; - // 判断数组类型 数字数组时按顺序绑定参数 - reset($vars); - $type = key($vars) === 0 ? 1 : 0; if ($reflect->getNumberOfParameters() > 0) { - $params = $reflect->getParameters(); - foreach ($params as $param) { - $name = $param->getName(); - $class = $param->getClass(); - if ($class) { - $className = $class->getName(); - $bind = Request::instance()->$name; - if ($bind instanceof $className) { - $args[] = $bind; - } else { - $args[] = method_exists($className, 'instance') ? $className::instance() : new $className(); + // 判断数组类型 数字数组时按顺序绑定参数 + reset($vars); + $type = key($vars) === 0 ? 1 : 0; + + foreach ($reflect->getParameters() as $param) { + $args[] = self::getParamValue($param, $vars, $type); + } + } + + return $args; + } + + /** + * 获取参数值 + * @access private + * @param \ReflectionParameter $param 参数 + * @param array $vars 变量 + * @param string $type 类别 + * @return array + */ + private static function getParamValue($param, &$vars, $type) + { + $name = $param->getName(); + $class = $param->getClass(); + + if ($class) { + $className = $class->getName(); + $bind = Request::instance()->$name; + + if ($bind instanceof $className) { + $result = $bind; + } else { + if (method_exists($className, 'invoke')) { + $method = new \ReflectionMethod($className, 'invoke'); + + if ($method->isPublic() && $method->isStatic()) { + return $className::invoke(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 \InvalidArgumentException('method param miss:' . $name); } + + $result = method_exists($className, 'instance') ? + $className::instance() : + new $className; } - // 全局过滤 - array_walk_recursive($args, [Request::instance(), 'filterExp']); + } elseif (1 == $type && !empty($vars)) { + $result = array_shift($vars); + } elseif (0 == $type && isset($vars[$name])) { + $result = $vars[$name]; + } elseif ($param->isDefaultValueAvailable()) { + $result = $param->getDefaultValue(); + } else { + throw new \InvalidArgumentException('method param miss:' . $name); } - return $args; + + return $result; + } + + /** + * 执行调用分发 + * @access protected + * @param array $dispatch 调用信息 + * @param array $config 配置信息 + * @return Response|mixed + * @throws \InvalidArgumentException + */ + protected static function exec($dispatch, $config) + { + 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': // 执行控制器操作 + $vars = array_merge(Request::instance()->param(), $dispatch['var']); + $data = Loader::action( + $dispatch['controller'], + $vars, + $config['url_controller_layer'], + $config['controller_suffix'] + ); + break; + case 'method': // 回调方法 + $vars = array_merge(Request::instance()->param(), $dispatch['var']); + $data = self::invokeMethod($dispatch['method'], $vars); + break; + case 'function': // 闭包 + $data = self::invokeFunction($dispatch['function']); + break; + case 'response': // Response 实例 + $data = $dispatch['response']; + break; + default: + throw new \InvalidArgumentException('dispatch type not support'); + } + + return $data; } /** * 执行模块 * @access public - * @param array $result 模块/控制器/操作 - * @param array $config 配置参数 + * @param array $result 模块/控制器/操作 + * @param array $config 配置参数 * @param bool $convert 是否自动转换控制器和操作名 * @return mixed + * @throws HttpException */ public static function module($result, $config, $convert = null) { if (is_string($result)) { $result = explode('/', $result); } + $request = Request::instance(); + if ($config['app_multi_module']) { // 多模块部署 $module = strip_tags(strtolower($result[0] ?: $config['default_module'])); $bind = Route::getBind('module'); $available = false; + if ($bind) { // 绑定模块 list($bindModule) = explode('/', $bind); + if (empty($result[0])) { $module = $bindModule; $available = true; @@ -337,6 +524,13 @@ class App // 初始化模块 $request->module($module); $config = self::init($module); + + // 模块请求缓存检查 + $request->cache( + $config['request_cache'], + $config['request_cache_expire'], + $config['request_cache_except'] + ); } else { throw new HttpException(404, 'module not exists:' . $module); } @@ -345,18 +539,32 @@ class App $module = ''; $request->module($module); } + + // 设置默认过滤机制 + $request->filter($config['default_filter']); + // 当前模块路径 App::$modulePath = APP_PATH . ($module ? $module . DS : ''); // 是否自动转换控制器和操作名 $convert = is_bool($convert) ? $convert : $config['url_convert']; + // 获取控制器名 $controller = strip_tags($result[1] ?: $config['default_controller']); + + if (!preg_match('/^[A-Za-z](\w|\.)*$/', $controller)) { + throw new HttpException(404, 'controller not exists:' . $controller); + } + $controller = $convert ? strtolower($controller) : $controller; // 获取操作名 $actionName = strip_tags($result[2] ?: $config['default_action']); - $actionName = $convert ? strtolower($actionName) : $actionName; + if (!empty($config['action_convert'])) { + $actionName = Loader::parseName($actionName, 1); + } else { + $actionName = $convert ? strtolower($actionName) : $actionName; + } // 设置当前请求的控制器、操作 $request->controller(Loader::parseName($controller, 1))->action($actionName); @@ -365,152 +573,49 @@ class App Hook::listen('module_init', $request); try { - $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:' . Loader::parseName($controller, 1)); - } - // 获取当前操作名 - $action = $actionName . $config['action_suffix']; - if (!preg_match('/^[A-Za-z](\w)*$/', $action)) { - // 非法操作 - throw new \ReflectionException('illegal action name:' . $actionName); - } + $instance = Loader::controller( + $controller, + $config['url_controller_layer'], + $config['controller_suffix'], + $config['empty_controller'] + ); + } catch (ClassNotFoundException $e) { + throw new HttpException(404, 'controller not exists:' . $e->getClass()); + } + // 获取当前操作名 + $action = $actionName . $config['action_suffix']; + + $vars = []; + if (is_callable([$instance, $action])) { // 执行操作方法 $call = [$instance, $action]; - Hook::listen('action_begin', $call); - - $data = self::invokeMethod($call); - } catch (\ReflectionException $e) { + // 严格获取当前操作方法名 + $reflect = new \ReflectionMethod($instance, $action); + $methodName = $reflect->getName(); + $suffix = $config['action_suffix']; + $actionName = $suffix ? substr($methodName, 0, -strlen($suffix)) : $methodName; + $request->action($actionName); + + } elseif (is_callable([$instance, '_empty'])) { + // 空操作 + $call = [$instance, '_empty']; + $vars = [$actionName]; + } else { // 操作不存在 - if (method_exists($instance, '_empty')) { - $reflect = new \ReflectionMethod($instance, '_empty'); - $data = $reflect->invokeArgs($instance, [$action]); - self::$debug && Log::record('[ RUN ] ' . $reflect->__toString(), 'info'); - } else { - throw new HttpException(404, 'method not exists:' . (new \ReflectionClass($instance))->getName() . '->' . $action); - } - } - return $data; - } - - /** - * 初始化应用 - */ - public static function initCommon() - { - if (empty(self::$init)) { - // 初始化应用 - $config = self::init(); - self::$suffix = $config['class_suffix']; - - // 应用调试模式 - self::$debug = Env::get('app_debug', Config::get('app_debug')); - if (!self::$debug) { - ini_set('display_errors', 'Off'); - } elseif (!IS_CLI) { - //重新申请一块比较大的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) && !isset(self::$file[$file])) { - include $file; - self::$file[$file] = true; - } - } - } - - // 设置系统时区 - date_default_timezone_set($config['default_timezone']); - - // 监听app_init - Hook::listen('app_init'); - - self::$init = $config; + throw new HttpException(404, 'method not exists:' . get_class($instance) . '->' . $action . '()'); } - return self::$init; - } - - /** - * 初始化应用或模块 - * @access public - * @param string $module 模块名 - * @return array - */ - private static function init($module = '') - { - // 定位模块目录 - $module = $module ? $module . DS : ''; - - // 加载初始化文件 - if (is_file(APP_PATH . $module . 'init' . EXT)) { - include APP_PATH . $module . 'init' . EXT; - } elseif (is_file(RUNTIME_PATH . $module . 'init' . EXT)) { - include RUNTIME_PATH . $module . 'init' . EXT; - } else { - $path = APP_PATH . $module; - // 加载模块配置 - $config = Config::load(CONF_PATH . $module . 'config' . CONF_EXT); - // 读取数据库配置文件 - $filename = CONF_PATH . $module . 'database' . CONF_EXT; - Config::load($filename, 'database'); - // 读取扩展配置文件 - if (is_dir(CONF_PATH . $module . 'extra')) { - $dir = CONF_PATH . $module . 'extra'; - $files = scandir($dir); - foreach ($files as $file) { - if (strpos($file, CONF_EXT)) { - $filename = $dir . DS . $file; - Config::load($filename, pathinfo($file, PATHINFO_FILENAME)); - } - } - } - // 加载应用状态配置 - if ($config['app_status']) { - $config = Config::load(CONF_PATH . $module . $config['app_status'] . CONF_EXT); - } + Hook::listen('action_begin', $call); - // 加载行为扩展文件 - if (is_file(CONF_PATH . $module . 'tags' . EXT)) { - Hook::import(include CONF_PATH . $module . 'tags' . EXT); - } - - // 加载公共文件 - if (is_file($path . 'common' . EXT)) { - include $path . 'common' . EXT; - } - - // 加载当前模块语言包 - if ($module) { - Lang::load($path . 'lang' . DS . Request::instance()->langset() . EXT); - } - } - return Config::get(); + return self::invokeMethod($call, $vars); } /** * URL路由检测(根据PATH_INFO) * @access public - * @param \think\Request $request - * @param array $config + * @param \think\Request $request 请求实例 + * @param array $config 配置信息 * @return array * @throws \think\Exception */ @@ -519,6 +624,7 @@ class App $path = $request->path(); $depr = $config['pathinfo_depr']; $result = false; + // 路由检测 $check = !is_null(self::$routeCheck) ? self::$routeCheck : $config['url_route_on']; if ($check) { @@ -526,18 +632,14 @@ class App if (is_file(RUNTIME_PATH . 'route.php')) { // 读取路由缓存 $rules = include RUNTIME_PATH . 'route.php'; - if (is_array($rules)) { - Route::rules($rules); - } + is_array($rules) && Route::rules($rules); } else { $files = $config['route_config_file']; foreach ($files as $file) { if (is_file(CONF_PATH . $file . CONF_EXT)) { // 导入路由配置 $rules = include CONF_PATH . $file . CONF_EXT; - if (is_array($rules)) { - Route::import($rules); - } + is_array($rules) && Route::import($rules); } } } @@ -545,15 +647,18 @@ class App // 路由检测(根据路由定义返回不同的URL调度) $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 HttpException(404, 'Route Not Found'); + throw new RouteNotFoundException(); } } + + // 路由无效 解析模块/控制器/操作/参数... 支持控制器自动搜索 if (false === $result) { - // 路由无效 解析模块/控制器/操作/参数... 支持控制器自动搜索 $result = Route::parseUrl($path, $depr, $config['controller_auto_search']); } + return $result; } diff --git a/library/think/Build.php b/library/think/Build.php index dd2bd550fac2f97a2bc52e43780724c1788e5dbf..de7c3275aed77f19a2fefa95a90e93a6d467ccdc 100644 --- a/library/think/Build.php +++ b/library/think/Build.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2016 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- @@ -14,36 +14,44 @@ namespace think; class Build { /** - * 根据传入的build资料创建目录和文件 - * @access protected - * @param array $build build列表 + * 根据传入的 build 资料创建目录和文件 + * @access public + * @param array $build build 列表 * @param string $namespace 应用类库命名空间 - * @param bool $suffix 类库后缀 + * @param bool $suffix 类库后缀 * @return void + * @throws Exception */ public static function run(array $build = [], $namespace = 'app', $suffix = false) { // 锁定 - $lockfile = APP_PATH . 'build.lock'; - if (is_writable($lockfile)) { - return; - } elseif (!touch($lockfile)) { - throw new Exception('应用目录[' . APP_PATH . ']不可写,目录无法自动生成!
请手动生成项目目录~', 10006); - } - foreach ($build as $module => $list) { - if ('__dir__' == $module) { - // 创建目录列表 - self::buildDir($list); - } elseif ('__file__' == $module) { - // 创建文件列表 - self::buildFile($list); - } else { - // 创建模块 - self::module($module, $list, $namespace, $suffix); + $lock = APP_PATH . 'build.lock'; + + // 如果锁定文件不可写(不存在)则进行处理,否则表示已经有程序在处理了 + if (!is_writable($lock)) { + if (!touch($lock)) { + throw new Exception( + '应用目录[' . APP_PATH . ']不可写,目录无法自动生成!
请手动生成项目目录~', + 10006 + ); } + + foreach ($build as $module => $list) { + if ('__dir__' == $module) { + // 创建目录列表 + self::buildDir($list); + } elseif ('__file__' == $module) { + // 创建文件列表 + self::buildFile($list); + } else { + // 创建模块 + self::module($module, $list, $namespace, $suffix); + } + } + + // 解除锁定 + unlink($lock); } - // 解除锁定 - unlink($lockfile); } /** @@ -55,10 +63,8 @@ class Build protected static function buildDir($list) { foreach ($list as $dir) { - if (!is_dir(APP_PATH . $dir)) { - // 创建目录 - mkdir(APP_PATH . $dir, 0755, true); - } + // 目录不存在则创建目录 + !is_dir(APP_PATH . $dir) && mkdir(APP_PATH . $dir, 0755, true); } } @@ -71,12 +77,17 @@ class Build protected static function buildFile($list) { foreach ($list as $file) { + // 先创建目录 if (!is_dir(APP_PATH . dirname($file))) { - // 创建目录 mkdir(APP_PATH . dirname($file), 0755, true); } + + // 再创建文件 if (!is_file(APP_PATH . $file)) { - file_put_contents(APP_PATH . $file, 'php' == pathinfo($file, PATHINFO_EXTENSION) ? " ['config.php', 'common.php'], '__dir__' => ['controller', 'model', 'view'], ]; } + // 创建子目录和文件 foreach ($list as $path => $file) { $modulePath = APP_PATH . $module . DS; + if ('__dir__' == $path) { // 生成子目录 foreach ($file as $dir) { - if (!is_dir($modulePath . $dir)) { - // 创建目录 - mkdir($modulePath . $dir, 0755, true); - } + self::checkDirBuild($modulePath . $dir); } } elseif ('__file__' == $path) { // 生成(空白)文件 foreach ($file as $name) { if (!is_file($modulePath . $name)) { - file_put_contents($modulePath . $name, 'php' == pathinfo($name, PATHINFO_EXTENSION) ? "has($name); + + return self::init()->has($name); } /** * 读取缓存 * @access public - * @param string $name 缓存标识 - * @param mixed $default 默认值 + * @param string $name 缓存标识 + * @param mixed $default 默认值 * @return mixed */ public static function get($name, $default = false) { - self::init(); self::$readTimes++; - return self::$handler->get($name, $default); + + return self::init()->get($name, $default); } /** * 写入缓存 * @access public - * @param string $name 缓存标识 - * @param mixed $value 存储数据 - * @param int|null $expire 有效时间 0为永久 + * @param string $name 缓存标识 + * @param mixed $value 存储数据 + * @param int|null $expire 有效时间 0为永久 * @return boolean */ public static function set($name, $value, $expire = null) { - self::init(); self::$writeTimes++; - return self::$handler->set($name, $value, $expire); + + return self::init()->set($name, $value, $expire); } /** * 自增缓存(针对数值缓存) * @access public - * @param string $name 缓存变量名 - * @param int $step 步长 + * @param string $name 缓存变量名 + * @param int $step 步长 * @return false|int */ public static function inc($name, $step = 1) { - self::init(); self::$writeTimes++; - return self::$handler->inc($name, $step); + + return self::init()->inc($name, $step); } /** * 自减缓存(针对数值缓存) * @access public - * @param string $name 缓存变量名 - * @param int $step 步长 + * @param string $name 缓存变量名 + * @param int $step 步长 * @return false|int */ public static function dec($name, $step = 1) { - self::init(); self::$writeTimes++; - return self::$handler->dec($name, $step); + + return self::init()->dec($name, $step); } /** * 删除缓存 * @access public - * @param string $name 缓存标识 + * @param string $name 缓存标识 * @return boolean */ public static function rm($name) { - self::init(); self::$writeTimes++; - return self::$handler->rm($name); + + return self::init()->rm($name); } /** * 清除缓存 * @access public - * @param string $tag 标签名 + * @param string $tag 标签名 * @return boolean */ public static function clear($tag = null) { - self::init(); self::$writeTimes++; - return self::$handler->clear($tag); + + return self::init()->clear($tag); + } + + /** + * 读取缓存并删除 + * @access public + * @param string $name 缓存变量名 + * @return mixed + */ + public static function pull($name) + { + self::$readTimes++; + self::$writeTimes++; + + return self::init()->pull($name); + } + + /** + * 如果不存在则写入缓存 + * @access public + * @param string $name 缓存变量名 + * @param mixed $value 存储数据 + * @param int $expire 有效时间 0为永久 + * @return mixed + */ + public static function remember($name, $value, $expire = null) + { + self::$readTimes++; + + return self::init()->remember($name, $value, $expire); } /** * 缓存标签 * @access public - * @param string $name 标签名 - * @param string|array $keys 缓存标识 - * @param bool $overlay 是否覆盖 - * @return \think\cache\Driver + * @param string $name 标签名 + * @param string|array $keys 缓存标识 + * @param bool $overlay 是否覆盖 + * @return Driver */ public static function tag($name, $keys = null, $overlay = false) { - self::init(); - return self::$handler->tag($name, $keys, $overlay); + return self::init()->tag($name, $keys, $overlay); } } diff --git a/library/think/Collection.php b/library/think/Collection.php index 8315268a9d6b925e9c289db82763344bcd4ea650..f872476f9ae26681ad23deb66ca9250bb282cf06 100644 --- a/library/think/Collection.php +++ b/library/think/Collection.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2016 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- @@ -19,20 +19,35 @@ use JsonSerializable; class Collection implements ArrayAccess, Countable, IteratorAggregate, JsonSerializable { + /** + * @var array 数据 + */ protected $items = []; + /** + * Collection constructor. + * @access public + * @param array $items 数据 + */ public function __construct($items = []) { $this->items = $this->convertToArray($items); } + /** + * 创建 Collection 实例 + * @access public + * @param array $items 数据 + * @return static + */ public static function make($items = []) { return new static($items); } /** - * 是否为空 + * 判断数据是否为空 + * @access public * @return bool */ public function isEmpty() @@ -40,74 +55,96 @@ class Collection implements ArrayAccess, Countable, IteratorAggregate, JsonSeria return empty($this->items); } + /** + * 将数据转成数组 + * @access public + * @return array + */ public function toArray() { return array_map(function ($value) { - return ($value instanceof Model || $value instanceof self) ? $value->toArray() : $value; + return ($value instanceof Model || $value instanceof self) ? + $value->toArray() : + $value; }, $this->items); } + /** + * 获取全部的数据 + * @access public + * @return array + */ public function all() { return $this->items; } /** - * 合并数组 - * - * @param mixed $items + * 交换数组中的键和值 + * @access public * @return static */ - public function merge($items) + public function flip() { - return new static(array_merge($this->items, $this->convertToArray($items))); + return new static(array_flip($this->items)); } /** - * 比较数组,返回差集 - * - * @param mixed $items + * 返回数组中所有的键名组成的新 Collection 实例 + * @access public * @return static */ - public function diff($items) + public function keys() { - return new static(array_diff($this->items, $this->convertToArray($items))); + return new static(array_keys($this->items)); } /** - * 交换数组中的键和值 - * + * 返回数组中所有的值组成的新 Collection 实例 + * @access public * @return static */ - public function flip() + public function values() { - return new static(array_flip($this->items)); + return new static(array_values($this->items)); } /** - * 比较数组,返回交集 - * - * @param mixed $items + * 合并数组并返回一个新的 Collection 实例 + * @access public + * @param mixed $items 新的数据 * @return static */ - public function intersect($items) + public function merge($items) { - return new static(array_intersect($this->items, $this->convertToArray($items))); + return new static(array_merge($this->items, $this->convertToArray($items))); } /** - * 返回数组中所有的键名 - * + * 比较数组,返回差集生成的新 Collection 实例 + * @access public + * @param mixed $items 做比较的数据 * @return static */ - public function keys() + public function diff($items) { - return new static(array_keys($this->items)); + return new static(array_diff($this->items, $this->convertToArray($items))); + } + + /** + * 比较数组,返回交集组成的 Collection 新实例 + * @access public + * @param mixed $items 比较数据 + * @return static + */ + public function intersect($items) + { + return new static(array_intersect($this->items, $this->convertToArray($items))); } /** - * 删除数组的最后一个元素(出栈) - * + * 返回并删除数据中的的最后一个元素(出栈) + * @access public * @return mixed */ public function pop() @@ -116,42 +153,74 @@ class Collection implements ArrayAccess, Countable, IteratorAggregate, JsonSeria } /** - * 通过使用用户自定义函数,以字符串返回数组 - * - * @param callable $callback - * @param mixed $initial + * 返回并删除数据中首个元素 + * @access public * @return mixed */ - public function reduce(callable $callback, $initial = null) + public function shift() { - return array_reduce($this->items, $callback, $initial); + return array_shift($this->items); } /** - * 以相反的顺序返回数组。 - * - * @return static + * 在数组开头插入一个元素 + * @access public + * @param mixed $value 值 + * @param mixed $key 键名 + * @return void */ - public function reverse() + public function unshift($value, $key = null) { - return new static(array_reverse($this->items)); + if (is_null($key)) { + array_unshift($this->items, $value); + } else { + $this->items = [$key => $value] + $this->items; + } + } + + /** + * 在数组结尾插入一个元素 + * @access public + * @param mixed $value 值 + * @param mixed $key 键名 + * @return void + */ + public function push($value, $key = null) + { + if (is_null($key)) { + $this->items[] = $value; + } else { + $this->items[$key] = $value; + } } /** - * 删除数组中首个元素,并返回被删除元素的值 - * + * 通过使用用户自定义函数,以字符串返回数组 + * @access public + * @param callable $callback 回调函数 + * @param mixed $initial 初始值 * @return mixed */ - public function shift() + public function reduce(callable $callback, $initial = null) { - return array_shift($this->items); + return array_reduce($this->items, $callback, $initial); + } + + /** + * 以相反的顺序创建一个新的 Collection 实例 + * @access public + * @return static + */ + public function reverse() + { + return new static(array_reverse($this->items)); } /** - * 把一个数组分割为新的数组块. - * - * @param int $size - * @param bool $preserveKeys + * 把数据分割为新的数组块 + * @access public + * @param int $size 分隔长度 + * @param bool $preserveKeys 是否保持原数据索引 * @return static */ public function chunk($size, $preserveKeys = false) @@ -166,78 +235,70 @@ class Collection implements ArrayAccess, Countable, IteratorAggregate, JsonSeria } /** - * 在数组开头插入一个元素 - * @param mixed $value - * @param null $key - * @return int - */ - public function unshift($value, $key = null) - { - if (is_null($key)) { - array_unshift($this->items, $value); - } else { - $this->items = [$key => $value] + $this->items; - } - } - - /** - * 给每个元素执行个回调 - * - * @param callable $callback + * 给数据中的每个元素执行回调 + * @access public + * @param callable $callback 回调函数 * @return $this */ public function each(callable $callback) { foreach ($this->items as $key => $item) { - if ($callback($item, $key) === false) { + $result = $callback($item, $key); + + if (false === $result) { break; } + + if (!is_object($item)) { + $this->items[$key] = $result; + } } return $this; } /** - * 用回调函数过滤数组中的元素 - * @param callable|null $callback + * 用回调函数过滤数据中的元素 + * @access public + * @param callable|null $callback 回调函数 * @return static */ public function filter(callable $callback = null) { - if ($callback) { - return new static(array_filter($this->items, $callback)); - } - - return new static(array_filter($this->items)); + return new static(array_filter($this->items, $callback ?: null)); } /** - * 返回数组中指定的一列 - * @param $column_key - * @param null $index_key + * 返回数据中指定的一列 + * @access public + * @param mixed $columnKey 键名 + * @param null $indexKey 作为索引值的列 * @return array */ - public function column($column_key, $index_key = null) + public function column($columnKey, $indexKey = null) { if (function_exists('array_column')) { - return array_column($this->items, $column_key, $index_key); + return array_column($this->items, $columnKey, $indexKey); } $result = []; foreach ($this->items as $row) { $key = $value = null; $keySet = $valueSet = false; - if (null !== $index_key && array_key_exists($index_key, $row)) { + + if (null !== $indexKey && array_key_exists($indexKey, $row)) { + $key = (string) $row[$indexKey]; $keySet = true; - $key = (string)$row[$index_key]; } - if (null === $column_key) { + + if (null === $columnKey) { $valueSet = true; $value = $row; - } elseif (is_array($row) && array_key_exists($column_key, $row)) { + } elseif (is_array($row) && array_key_exists($columnKey, $row)) { $valueSet = true; - $value = $row[$column_key]; + $value = $row[$columnKey]; } + if ($valueSet) { if ($keySet) { $result[$key] = $value; @@ -246,34 +307,30 @@ class Collection implements ArrayAccess, Countable, IteratorAggregate, JsonSeria } } } + return $result; } /** - * 对数组排序 - * - * @param callable|null $callback + * 对数据排序,并返回排序后的数据组成的新 Collection 实例 + * @access public + * @param callable|null $callback 回调函数 * @return static */ public function sort(callable $callback = null) { - $items = $this->items; - - $callback ? uasort($items, $callback) : uasort($items, function ($a, $b) { - - if ($a == $b) { - return 0; - } - - return ($a < $b) ? -1 : 1; - }); + $items = $this->items; + $callback = $callback ?: function ($a, $b) { + return $a == $b ? 0 : (($a < $b) ? -1 : 1); + }; + uasort($items, $callback); return new static($items); } /** - * 将数组打乱 - * + * 将数据打乱后组成新的 Collection 实例 + * @access public * @return static */ public function shuffle() @@ -281,16 +338,15 @@ class Collection implements ArrayAccess, Countable, IteratorAggregate, JsonSeria $items = $this->items; shuffle($items); - return new static($items); } /** - * 截取数组 - * - * @param int $offset - * @param int $length - * @param bool $preserveKeys + * 截取数据并返回新的 Collection 实例 + * @access public + * @param int $offset 起始位置 + * @param int $length 截取长度 + * @param bool $preserveKeys 是否保持原先的键名 * @return static */ public function slice($offset, $length = null, $preserveKeys = false) @@ -298,17 +354,35 @@ class Collection implements ArrayAccess, Countable, IteratorAggregate, JsonSeria return new static(array_slice($this->items, $offset, $length, $preserveKeys)); } - // ArrayAccess + /** + * 指定的键是否存在 + * @access public + * @param mixed $offset 键名 + * @return bool + */ public function offsetExists($offset) { return array_key_exists($offset, $this->items); } + /** + * 获取指定键对应的值 + * @access public + * @param mixed $offset 键名 + * @return mixed + */ public function offsetGet($offset) { return $this->items[$offset]; } + /** + * 设置键值 + * @access public + * @param mixed $offset 键名 + * @param mixed $value 值 + * @return void + */ public function offsetSet($offset, $value) { if (is_null($offset)) { @@ -318,33 +392,51 @@ class Collection implements ArrayAccess, Countable, IteratorAggregate, JsonSeria } } + /** + * 删除指定键值 + * @access public + * @param mixed $offset 键名 + * @return void + */ public function offsetUnset($offset) { unset($this->items[$offset]); } - //Countable + /** + * 统计数据的个数 + * @access public + * @return int + */ public function count() { return count($this->items); } - //IteratorAggregate + /** + * 获取数据的迭代器 + * @access public + * @return ArrayIterator + */ public function getIterator() { return new ArrayIterator($this->items); } - //JsonSerializable + /** + * 将数据反序列化成数组 + * @access public + * @return array + */ public function jsonSerialize() { return $this->toArray(); } /** - * 转换当前数据集为JSON字符串 + * 转换当前数据集为 JSON 字符串 * @access public - * @param integer $options json参数 + * @param integer $options json 参数 * @return string */ public function toJson($options = JSON_UNESCAPED_UNICODE) @@ -352,22 +444,24 @@ class Collection implements ArrayAccess, Countable, IteratorAggregate, JsonSeria return json_encode($this->toArray(), $options); } + /** + * 将数据转换成字符串 + * @access public + * @return string + */ public function __toString() { return $this->toJson(); } /** - * 转换成数组 - * - * @param mixed $items + * 将数据转换成数组 + * @access protected + * @param mixed $items 数据 * @return array */ protected function convertToArray($items) { - if ($items instanceof self) { - return $items->all(); - } - return (array)$items; + return $items instanceof self ? $items->all() : (array) $items; } } diff --git a/library/think/Config.php b/library/think/Config.php index 5ab032fd4d1840c9f0370c11d466a7bd4759f1f3..8fa668d145afee4fd3f7c15fbb777580b5054ba0 100644 --- a/library/think/Config.php +++ b/library/think/Config.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2016 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- @@ -13,67 +13,88 @@ namespace think; class Config { - // 配置参数 + /** + * @var array 配置参数 + */ private static $config = []; - // 参数作用域 + + /** + * @var string 参数作用域 + */ private static $range = '_sys_'; - // 设定配置参数的作用域 + /** + * 设定配置参数的作用域 + * @access public + * @param string $range 作用域 + * @return void + */ public static function range($range) { self::$range = $range; - if (!isset(self::$config[$range])) { - self::$config[$range] = []; - } + + if (!isset(self::$config[$range])) self::$config[$range] = []; } /** * 解析配置文件或内容 - * @param string $config 配置文件路径或内容 - * @param string $type 配置解析类型 - * @param string $name 配置名(如设置即表示二级配置) - * @param string $range 作用域 + * @access public + * @param string $config 配置文件路径或内容 + * @param string $type 配置解析类型 + * @param string $name 配置名(如设置即表示二级配置) + * @param string $range 作用域 * @return mixed */ public static function parse($config, $type = '', $name = '', $range = '') { $range = $range ?: self::$range; - if (empty($type)) { - $type = pathinfo($config, PATHINFO_EXTENSION); - } - $class = false !== strpos($type, '\\') ? $type : '\\think\\config\\driver\\' . ucwords($type); + + if (empty($type)) $type = pathinfo($config, PATHINFO_EXTENSION); + + $class = false !== strpos($type, '\\') ? + $type : + '\\think\\config\\driver\\' . ucwords($type); + return self::set((new $class())->parse($config), $name, $range); } /** * 加载配置文件(PHP格式) - * @param string $file 配置文件名 - * @param string $name 配置名(如设置即表示二级配置) - * @param string $range 作用域 + * @access public + * @param string $file 配置文件名 + * @param string $name 配置名(如设置即表示二级配置) + * @param string $range 作用域 * @return mixed */ public static function load($file, $name = '', $range = '') { $range = $range ?: self::$range; - if (!isset(self::$config[$range])) { - self::$config[$range] = []; - } + + if (!isset(self::$config[$range])) self::$config[$range] = []; + if (is_file($file)) { + $name = strtolower($name); $type = pathinfo($file, PATHINFO_EXTENSION); - if ('php' != $type) { - return self::parse($file, $type, $name, $range); - } else { + + if ('php' == $type) { return self::set(include $file, $name, $range); } - } else { - return self::$config[$range]; + + if ('yaml' == $type && function_exists('yaml_parse_file')) { + return self::set(yaml_parse_file($file), $name, $range); + } + + return self::parse($file, $type, $name, $range); } + + return self::$config[$range]; } /** * 检测配置是否存在 - * @param string $name 配置参数名(支持二级配置 .号分割) - * @param string $range 作用域 + * @access public + * @param string $name 配置参数名(支持二级配置 . 号分割) + * @param string $range 作用域 * @return bool */ public static function has($name, $range = '') @@ -82,82 +103,108 @@ class Config if (!strpos($name, '.')) { return isset(self::$config[$range][strtolower($name)]); - } else { - // 二维数组设置和获取支持 - $name = explode('.', $name); - return isset(self::$config[$range][strtolower($name[0])][$name[1]]); } + + // 二维数组设置和获取支持 + $name = explode('.', $name, 2); + return isset(self::$config[$range][strtolower($name[0])][$name[1]]); } /** * 获取配置参数 为空则获取所有配置 - * @param string $name 配置参数名(支持二级配置 .号分割) - * @param string $range 作用域 + * @access public + * @param string $name 配置参数名(支持二级配置 . 号分割) + * @param string $range 作用域 * @return mixed */ public static function get($name = null, $range = '') { $range = $range ?: self::$range; + // 无参数时获取所有 if (empty($name) && isset(self::$config[$range])) { return self::$config[$range]; } + // 非二级配置时直接返回 if (!strpos($name, '.')) { $name = strtolower($name); return isset(self::$config[$range][$name]) ? self::$config[$range][$name] : null; - } else { - // 二维数组设置和获取支持 - $name = explode('.', $name); - $name[0] = strtolower($name[0]); - return isset(self::$config[$range][$name[0]][$name[1]]) ? self::$config[$range][$name[0]][$name[1]] : null; } + + // 二维数组设置和获取支持 + $name = explode('.', $name, 2); + $name[0] = strtolower($name[0]); + + if (!isset(self::$config[$range][$name[0]])) { + // 动态载入额外配置 + $module = Request::instance()->module(); + $file = CONF_PATH . ($module ? $module . DS : '') . 'extra' . DS . $name[0] . CONF_EXT; + + is_file($file) && self::load($file, $name[0]); + } + + return isset(self::$config[$range][$name[0]][$name[1]]) ? + self::$config[$range][$name[0]][$name[1]] : + null; } /** - * 设置配置参数 name为数组则为批量设置 - * @param string|array $name 配置参数名(支持二级配置 .号分割) - * @param mixed $value 配置值 - * @param string $range 作用域 + * 设置配置参数 name 为数组则为批量设置 + * @access public + * @param string|array $name 配置参数名(支持二级配置 . 号分割) + * @param mixed $value 配置值 + * @param string $range 作用域 * @return mixed */ public static function set($name, $value = null, $range = '') { $range = $range ?: self::$range; - if (!isset(self::$config[$range])) { - self::$config[$range] = []; - } + + if (!isset(self::$config[$range])) self::$config[$range] = []; + + // 字符串则表示单个配置设置 if (is_string($name)) { if (!strpos($name, '.')) { self::$config[$range][strtolower($name)] = $value; } else { - // 二维数组设置和获取支持 - $name = explode('.', $name); + // 二维数组 + $name = explode('.', $name, 2); self::$config[$range][strtolower($name[0])][$name[1]] = $value; } - return; - } elseif (is_array($name)) { - // 批量设置 + + return $value; + } + + // 数组则表示批量设置 + if (is_array($name)) { if (!empty($value)) { self::$config[$range][$value] = isset(self::$config[$range][$value]) ? - array_merge(self::$config[$range][$value], $name) : - self::$config[$range][$value] = $name; + array_merge(self::$config[$range][$value], $name) : + $name; + return self::$config[$range][$value]; - } else { - return self::$config[$range] = array_merge(self::$config[$range], array_change_key_case($name)); } - } else { - // 为空直接返回 已有配置 - return self::$config[$range]; + + return self::$config[$range] = array_merge( + self::$config[$range], array_change_key_case($name) + ); } + + // 为空直接返回已有配置 + return self::$config[$range]; } /** * 重置配置参数 + * @access public + * @param string $range 作用域 + * @return void */ public static function reset($range = '') { $range = $range ?: self::$range; + if (true === $range) { self::$config = []; } else { diff --git a/library/think/Console.php b/library/think/Console.php index ab8e0211ba15dd7ceae107bc2efd80224df78f9d..32b257259675b65e5986f739eef10c00447a894d 100644 --- a/library/think/Console.php +++ b/library/think/Console.php @@ -20,20 +20,49 @@ use think\console\output\driver\Buffer; class Console { - + /** + * @var string 命令名称 + */ private $name; + + /** + * @var string 命令版本 + */ private $version; - /** @var Command[] */ + /** + * @var Command[] 命令 + */ private $commands = []; + /** + * @var bool 是否需要帮助信息 + */ private $wantHelps = false; + /** + * @var bool 是否捕获异常 + */ private $catchExceptions = true; - private $autoExit = true; + + /** + * @var bool 是否自动退出执行 + */ + private $autoExit = true; + + /** + * @var InputDefinition 输入定义 + */ private $definition; + + /** + * @var string 默认执行的命令 + */ private $defaultCommand; + /** + * @var array 默认提供的命令 + */ private static $defaultCommands = [ "think\\console\\command\\Help", "think\\console\\command\\Lists", @@ -47,11 +76,22 @@ class Console "think\\console\\command\\optimize\\Schema", ]; - public function __construct($name = 'UNKNOWN', $version = 'UNKNOWN') + /** + * Console constructor. + * @access public + * @param string $name 名称 + * @param string $version 版本 + * @param null|string $user 执行用户 + */ + public function __construct($name = 'UNKNOWN', $version = 'UNKNOWN', $user = null) { $this->name = $name; $this->version = $version; + if ($user) { + $this->setUser($user); + } + $this->defaultCommand = 'list'; $this->definition = $this->getDefaultInputDefinition(); @@ -60,46 +100,67 @@ class Console } } + /** + * 设置执行用户 + * @param $user + */ + public function setUser($user) + { + $user = posix_getpwnam($user); + if ($user) { + posix_setuid($user['uid']); + posix_setgid($user['gid']); + } + } + + /** + * 初始化 Console + * @access public + * @param bool $run 是否运行 Console + * @return int|Console + */ public static function init($run = true) { static $console; + if (!$console) { - // 实例化console - $console = new self('Think Console', '0.1'); + $config = Config::get('console'); + // 实例化 console + $console = new self($config['name'], $config['version'], $config['user']); + // 读取指令集 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")) { - // 注册指令 - $console->add(new $command()); - } + class_exists($command) && + is_subclass_of($command, "\\think\\console\\Command") && + $console->add(new $command()); // 注册指令 } } } } - if ($run) { - // 运行 - return $console->run(); - } else { - return $console; - } + + return $run ? $console->run() : $console; } /** - * @param $command - * @param array $parameters - * @return Output|Buffer + * 调用命令 + * @access public + * @param string $command + * @param array $parameters + * @param string $driver + * @return Output */ - public static function call($command, array $parameters = []) + public static function call($command, array $parameters = [], $driver = 'buffer') { $console = self::init(false); array_unshift($parameters, $command); $input = new Input($parameters); - $output = new Output('buffer'); + $output = new Output($driver); $console->setCatchExceptions(false); $console->find($command)->run($input, $output); @@ -109,9 +170,9 @@ class Console /** * 执行当前的指令 + * @access public * @return int * @throws \Exception - * @api */ public function run() { @@ -123,27 +184,21 @@ class Console try { $exitCode = $this->doRun($input, $output); } catch (\Exception $e) { - if (!$this->catchExceptions) { - throw $e; - } + if (!$this->catchExceptions) throw $e; $output->renderException($e); $exitCode = $e->getCode(); + if (is_numeric($exitCode)) { - $exitCode = (int) $exitCode; - if (0 === $exitCode) { - $exitCode = 1; - } + $exitCode = ((int) $exitCode) ?: 1; } else { $exitCode = 1; } } if ($this->autoExit) { - if ($exitCode > 255) { - $exitCode = 255; - } + if ($exitCode > 255) $exitCode = 255; exit($exitCode); } @@ -153,12 +208,14 @@ class Console /** * 执行指令 - * @param Input $input - * @param Output $output + * @access public + * @param Input $input 输入 + * @param Output $output 输出 * @return int */ public function doRun(Input $input, Output $output) { + // 获取版本信息 if (true === $input->hasParameterOption(['--version', '-V'])) { $output->writeln($this->getLongVersion()); @@ -167,6 +224,7 @@ class Console $name = $this->getCommandName($input); + // 获取帮助信息 if (true === $input->hasParameterOption(['--help', '-h'])) { if (!$name) { $name = 'help'; @@ -181,25 +239,26 @@ class Console $input = new Input([$this->defaultCommand]); } - $command = $this->find($name); - - $exitCode = $this->doRunCommand($command, $input, $output); - - return $exitCode; + return $this->doRunCommand($this->find($name), $input, $output); } /** * 设置输入参数定义 - * @param InputDefinition $definition + * @access public + * @param InputDefinition $definition 输入定义 + * @return $this; */ public function setDefinition(InputDefinition $definition) { $this->definition = $definition; + + return $this; } /** * 获取输入参数定义 - * @return InputDefinition The InputDefinition instance + * @access public + * @return InputDefinition */ public function getDefinition() { @@ -207,8 +266,9 @@ class Console } /** - * Gets the help message. - * @return string A help message. + * 获取帮助信息 + * @access public + * @return string */ public function getHelp() { @@ -216,27 +276,34 @@ class Console } /** - * 是否捕获异常 - * @param bool $boolean - * @api + * 设置是否捕获异常 + * @access public + * @param bool $boolean 是否捕获 + * @return $this */ public function setCatchExceptions($boolean) { $this->catchExceptions = (bool) $boolean; + + return $this; } /** - * 是否自动退出 - * @param bool $boolean - * @api + * 设置是否自动退出 + * @access public + * @param bool $boolean 是否自动退出 + * @return $this */ public function setAutoExit($boolean) { $this->autoExit = (bool) $boolean; + + return $this; } /** * 获取名称 + * @access public * @return string */ public function getName() @@ -246,17 +313,21 @@ class Console /** * 设置名称 - * @param string $name + * @access public + * @param string $name 名称 + * @return $this */ public function setName($name) { $this->name = $name; + + return $this; } /** * 获取版本 + * @access public * @return string - * @api */ public function getVersion() { @@ -265,21 +336,30 @@ class Console /** * 设置版本 - * @param string $version + * @access public + * @param string $version 版本信息 + * @return $this */ public function setVersion($version) { $this->version = $version; + + return $this; } /** * 获取完整的版本号 + * @access public * @return string */ public function getLongVersion() { if ('UNKNOWN' !== $this->getName() && 'UNKNOWN' !== $this->getVersion()) { - return sprintf('%s version %s', $this->getName(), $this->getVersion()); + return sprintf( + '%s version %s', + $this->getName(), + $this->getVersion() + ); } return 'Console Tool'; @@ -287,7 +367,8 @@ class Console /** * 注册一个指令 - * @param string $name + * @access public + * @param string $name 指令名称 * @return Command */ public function register($name) @@ -296,32 +377,37 @@ class Console } /** - * 添加指令 - * @param Command[] $commands + * 批量添加指令 + * @access public + * @param Command[] $commands 指令实例 + * @return $this */ public function addCommands(array $commands) { - foreach ($commands as $command) { - $this->add($command); - } + foreach ($commands as $command) $this->add($command); + + return $this; } /** * 添加一个指令 - * @param Command $command - * @return Command + * @access public + * @param Command $command 命令实例 + * @return Command|bool */ public function add(Command $command) { - $command->setConsole($this); - if (!$command->isEnabled()) { $command->setConsole(null); - return null; + return false; } + $command->setConsole($this); + if (null === $command->getDefinition()) { - throw new \LogicException(sprintf('Command class "%s" is not correctly initialized. You probably forgot to call the parent constructor.', get_class($command))); + throw new \LogicException( + sprintf('Command class "%s" is not correctly initialized. You probably forgot to call the parent constructor.', get_class($command)) + ); } $this->commands[$command->getName()] = $command; @@ -335,14 +421,17 @@ class Console /** * 获取指令 - * @param string $name 指令名称 + * @access public + * @param string $name 指令名称 * @return Command * @throws \InvalidArgumentException */ public function get($name) { if (!isset($this->commands[$name])) { - throw new \InvalidArgumentException(sprintf('The command "%s" does not exist.', $name)); + throw new \InvalidArgumentException( + sprintf('The command "%s" does not exist.', $name) + ); } $command = $this->commands[$name]; @@ -362,7 +451,8 @@ class Console /** * 某个指令是否存在 - * @param string $name 指令名称 + * @access public + * @param string $name 指令名称 * @return bool */ public function has($name) @@ -372,16 +462,22 @@ class Console /** * 获取所有的命名空间 + * @access public * @return array */ public function getNamespaces() { $namespaces = []; + foreach ($this->commands as $command) { - $namespaces = array_merge($namespaces, $this->extractAllNamespaces($command->getName())); + $namespaces = array_merge( + $namespaces, $this->extractAllNamespaces($command->getName()) + ); foreach ($command->getAliases() as $alias) { - $namespaces = array_merge($namespaces, $this->extractAllNamespaces($alias)); + $namespaces = array_merge( + $namespaces, $this->extractAllNamespaces($alias) + ); } } @@ -389,21 +485,25 @@ class Console } /** - * 查找注册命名空间中的名称或缩写。 + * 查找注册命名空间中的名称或缩写 + * @access public * @param string $namespace * @return string * @throws \InvalidArgumentException */ public function findNamespace($namespace) { - $allNamespaces = $this->getNamespaces(); - $expr = preg_replace_callback('{([^:]+|)}', function ($matches) { + $expr = preg_replace_callback('{([^:]+|)}', function ($matches) { return preg_quote($matches[1]) . '[^:]*'; }, $namespace); - $namespaces = preg_grep('{^' . $expr . '}', $allNamespaces); + + $allNamespaces = $this->getNamespaces(); + $namespaces = preg_grep('{^' . $expr . '}', $allNamespaces); if (empty($namespaces)) { - $message = sprintf('There are no commands defined in the "%s" namespace.', $namespace); + $message = sprintf( + 'There are no commands defined in the "%s" namespace.', $namespace + ); if ($alternatives = $this->findAlternatives($namespace, $allNamespaces)) { if (1 == count($alternatives)) { @@ -419,8 +519,14 @@ class Console } $exact = in_array($namespace, $namespaces, true); + if (count($namespaces) > 1 && !$exact) { - throw new \InvalidArgumentException(sprintf('The namespace "%s" is ambiguous (%s).', $namespace, $this->getAbbreviationSuggestions(array_values($namespaces)))); + throw new \InvalidArgumentException( + sprintf( + 'The namespace "%s" is ambiguous (%s).', + $namespace, + $this->getAbbreviationSuggestions(array_values($namespaces))) + ); } return $exact ? $namespace : reset($namespaces); @@ -428,20 +534,22 @@ class Console /** * 查找指令 - * @param string $name 名称或者别名 + * @access public + * @param string $name 名称或者别名 * @return Command * @throws \InvalidArgumentException */ public function find($name) { - $allCommands = array_keys($this->commands); - $expr = preg_replace_callback('{([^:]+|)}', function ($matches) { + $expr = preg_replace_callback('{([^:]+|)}', function ($matches) { return preg_quote($matches[1]) . '[^:]*'; }, $name); - $commands = preg_grep('{^' . $expr . '}', $allCommands); + + $allCommands = array_keys($this->commands); + $commands = preg_grep('{^' . $expr . '}', $allCommands); if (empty($commands) || count(preg_grep('{^' . $expr . '$}', $commands)) < 1) { - if (false !== $pos = strrpos($name, ':')) { + if (false !== ($pos = strrpos($name, ':'))) { $this->findNamespace(substr($name, 0, $pos)); } @@ -472,7 +580,9 @@ class Console if (count($commands) > 1 && !$exact) { $suggestions = $this->getAbbreviationSuggestions(array_values($commands)); - throw new \InvalidArgumentException(sprintf('Command "%s" is ambiguous (%s).', $name, $suggestions)); + throw new \InvalidArgumentException( + sprintf('Command "%s" is ambiguous (%s).', $name, $suggestions) + ); } return $this->get($exact ? $name : reset($commands)); @@ -480,21 +590,20 @@ class Console /** * 获取所有的指令 - * @param string $namespace 命名空间 + * @access public + * @param string $namespace 命名空间 * @return Command[] - * @api */ public function all($namespace = null) { - if (null === $namespace) { - return $this->commands; - } + if (null === $namespace) return $this->commands; $commands = []; + foreach ($this->commands as $name => $command) { - if ($this->extractNamespace($name, substr_count($namespace, ':') + 1) === $namespace) { - $commands[$name] = $command; - } + $ext = $this->extractNamespace($name, substr_count($namespace, ':') + 1); + + if ($ext === $namespace) $commands[$name] = $command; } return $commands; @@ -502,7 +611,8 @@ class Console /** * 获取可能的指令名 - * @param array $names + * @access public + * @param array $names 指令名 * @return array */ public static function getAbbreviations($names) @@ -519,9 +629,11 @@ class Console } /** - * 配置基于用户的参数和选项的输入和输出实例。 - * @param Input $input 输入实例 - * @param Output $output 输出实例 + * 配置基于用户的参数和选项的输入和输出实例 + * @access protected + * @param Input $input 输入实例 + * @param Output $output 输出实例 + * @return void */ protected function configureIO(Input $input, Output $output) { @@ -550,9 +662,10 @@ class Console /** * 执行指令 - * @param Command $command 指令实例 - * @param Input $input 输入实例 - * @param Output $output 输出实例 + * @access protected + * @param Command $command 指令实例 + * @param Input $input 输入实例 + * @param Output $output 输出实例 * @return int * @throws \Exception */ @@ -562,8 +675,9 @@ class Console } /** - * 获取指令的基础名称 - * @param Input $input + * 获取指令的名称 + * @access protected + * @param Input $input 输入实例 * @return string */ protected function getCommandName(Input $input) @@ -573,6 +687,7 @@ class Console /** * 获取默认输入定义 + * @access protected * @return InputDefinition */ protected function getDefaultInputDefinition() @@ -590,41 +705,55 @@ class Console } /** - * 设置默认命令 - * @return Command[] An array of default Command instances + * 获取默认命令 + * @access protected + * @return Command[] */ protected function getDefaultCommands() { $defaultCommands = []; - foreach (self::$defaultCommands as $classname) { - if (class_exists($classname) && is_subclass_of($classname, "think\\console\\Command")) { - $defaultCommands[] = new $classname(); + foreach (self::$defaultCommands as $class) { + if (class_exists($class) && is_subclass_of($class, "think\\console\\Command")) { + $defaultCommands[] = new $class(); } } return $defaultCommands; } - public static function addDefaultCommands(array $classnames) + /** + * 添加默认指令 + * @access public + * @param array $classes 指令 + * @return void + */ + public static function addDefaultCommands(array $classes) { - self::$defaultCommands = array_merge(self::$defaultCommands, $classnames); + self::$defaultCommands = array_merge(self::$defaultCommands, $classes); } /** * 获取可能的建议 - * @param array $abbrevs + * @access private + * @param array $abbrevs * @return string */ private function getAbbreviationSuggestions($abbrevs) { - return sprintf('%s, %s%s', $abbrevs[0], $abbrevs[1], count($abbrevs) > 2 ? sprintf(' and %d more', count($abbrevs) - 2) : ''); + return sprintf( + '%s, %s%s', + $abbrevs[0], + $abbrevs[1], + count($abbrevs) > 2 ? sprintf(' and %d more', count($abbrevs) - 2) : '' + ); } /** - * 返回命名空间部分 - * @param string $name 指令 - * @param string $limit 部分的命名空间的最大数量 + * 返回指令的命名空间部分 + * @access public + * @param string $name 指令名称 + * @param string $limit 部分的命名空间的最大数量 * @return string */ public function extractNamespace($name, $limit = null) @@ -637,16 +766,17 @@ class Console /** * 查找可替代的建议 - * @param string $name - * @param array|\Traversable $collection + * @access private + * @param string $name 指令名称 + * @param array|\Traversable $collection 建议集合 * @return array */ private function findAlternatives($name, $collection) { - $threshold = 1e3; - $alternatives = []; - + $threshold = 1e3; + $alternatives = []; $collectionParts = []; + foreach ($collection as $item) { $collectionParts[$item] = explode(':', $item); } @@ -654,6 +784,7 @@ class Console foreach (explode(':', $name) as $i => $subname) { foreach ($collectionParts as $collectionName => $parts) { $exists = isset($alternatives[$collectionName]); + if (!isset($parts[$i]) && $exists) { $alternatives[$collectionName] += $threshold; continue; @@ -662,8 +793,14 @@ class Console } $lev = levenshtein($subname, $parts[$i]); - if ($lev <= strlen($subname) / 3 || '' !== $subname && false !== strpos($parts[$i], $subname)) { - $alternatives[$collectionName] = $exists ? $alternatives[$collectionName] + $lev : $lev; + + if ($lev <= strlen($subname) / 3 || + '' !== $subname && + false !== strpos($parts[$i], $subname) + ) { + $alternatives[$collectionName] = $exists ? + $alternatives[$collectionName] + $lev : + $lev; } elseif ($exists) { $alternatives[$collectionName] += $threshold; } @@ -672,14 +809,18 @@ class Console foreach ($collection as $item) { $lev = levenshtein($name, $item); + if ($lev <= strlen($name) / 3 || false !== strpos($item, $name)) { - $alternatives[$item] = isset($alternatives[$item]) ? $alternatives[$item] - $lev : $lev; + $alternatives[$item] = isset($alternatives[$item]) ? + $alternatives[$item] - $lev : + $lev; } } $alternatives = array_filter($alternatives, function ($lev) use ($threshold) { return $lev < 2 * $threshold; }); + asort($alternatives); return array_keys($alternatives); @@ -687,24 +828,28 @@ class Console /** * 设置默认的指令 - * @param string $commandName The Command name + * @access public + * @param string $commandName 指令名称 + * @return $this */ public function setDefaultCommand($commandName) { $this->defaultCommand = $commandName; + + return $this; } /** * 返回所有的命名空间 - * @param string $name + * @access private + * @param string $name 指令名称 * @return array */ private function extractAllNamespaces($name) { - $parts = explode(':', $name, -1); $namespaces = []; - foreach ($parts as $part) { + foreach (explode(':', $name, -1) as $part) { if (count($namespaces)) { $namespaces[] = end($namespaces) . ':' . $part; } else { diff --git a/library/think/Controller.php b/library/think/Controller.php index a889e4d9e4fe56275408303e3356c90161fd3542..77225b735c74ada72ea02f691268cca890010b75 100644 --- a/library/think/Controller.php +++ b/library/think/Controller.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2016 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- @@ -11,43 +11,49 @@ namespace think; -\think\Loader::import('controller/Jump', TRAIT_PATH, EXT); - -use think\Exception; use think\exception\ValidateException; +use traits\controller\Jump; + +Loader::import('controller/Jump', TRAIT_PATH, EXT); class Controller { - use \traits\controller\Jump; + use Jump; - // 视图类实例 + /** + * @var \think\View 视图类实例 + */ protected $view; - // Request实例 + + /** + * @var \think\Request Request 实例 + */ protected $request; - // 验证失败是否抛出异常 + + /** + * @var bool 验证失败是否抛出异常 + */ protected $failException = false; - // 是否批量验证 + + /** + * @var bool 是否批量验证 + */ protected $batchValidate = false; /** - * 前置操作方法列表 - * @var array $beforeActionList - * @access protected + * @var array 前置操作方法列表 */ protected $beforeActionList = []; /** - * 架构函数 - * @param Request $request Request对象 + * 构造方法 * @access public + * @param Request $request Request 对象 */ public function __construct(Request $request = null) { - if (is_null($request)) { - $request = Request::instance(); - } $this->view = View::instance(Config::get('template'), Config::get('view_replace_str')); - $this->request = $request; + $this->request = is_null($request) ? Request::instance() : $request; // 控制器初始化 $this->_initialize(); @@ -62,7 +68,10 @@ class Controller } } - // 初始化 + /** + * 初始化操作 + * @access protected + */ protected function _initialize() { } @@ -70,8 +79,9 @@ class Controller /** * 前置操作 * @access protected - * @param string $method 前置操作方法名 - * @param array $options 调用参数 ['only'=>[...]] 或者['except'=>[...]] + * @param string $method 前置操作方法名 + * @param array $options 调用参数 ['only'=>[...]] 或者 ['except'=>[...]] + * @return void */ protected function beforeAction($method, $options = []) { @@ -79,6 +89,7 @@ class Controller if (is_string($options['only'])) { $options['only'] = explode(',', $options['only']); } + if (!in_array($this->request->action(), $options['only'])) { return; } @@ -86,6 +97,7 @@ class Controller if (is_string($options['except'])) { $options['except'] = explode(',', $options['except']); } + if (in_array($this->request->action(), $options['except'])) { return; } @@ -97,10 +109,10 @@ class Controller /** * 加载模板输出 * @access protected - * @param string $template 模板文件名 - * @param array $vars 模板输出变量 - * @param array $replace 模板替换 - * @param array $config 模板参数 + * @param string $template 模板文件名 + * @param array $vars 模板输出变量 + * @param array $replace 模板替换 + * @param array $config 模板参数 * @return mixed */ protected function fetch($template = '', $vars = [], $replace = [], $config = []) @@ -111,10 +123,10 @@ class Controller /** * 渲染内容输出 * @access protected - * @param string $content 模板内容 - * @param array $vars 模板输出变量 - * @param array $replace 替换内容 - * @param array $config 模板参数 + * @param string $content 模板内容 + * @param array $vars 模板输出变量 + * @param array $replace 替换内容 + * @param array $config 模板参数 * @return mixed */ protected function display($content = '', $vars = [], $replace = [], $config = []) @@ -125,24 +137,28 @@ class Controller /** * 模板变量赋值 * @access protected - * @param mixed $name 要显示的模板变量 - * @param mixed $value 变量的值 - * @return void + * @param mixed $name 要显示的模板变量 + * @param mixed $value 变量的值 + * @return $this */ protected function assign($name, $value = '') { $this->view->assign($name, $value); + + return $this; } /** * 初始化模板引擎 * @access protected * @param array|string $engine 引擎参数 - * @return void + * @return $this */ protected function engine($engine) { $this->view->engine($engine); + + return $this; } /** @@ -154,17 +170,18 @@ class Controller protected function validateFailException($fail = true) { $this->failException = $fail; + return $this; } /** * 验证数据 * @access protected - * @param array $data 数据 - * @param string|array $validate 验证器名或者验证规则数组 - * @param array $message 提示信息 - * @param bool $batch 是否批量验证 - * @param mixed $callback 回调方法(闭包) + * @param array $data 数据 + * @param string|array $validate 验证器名或者验证规则数组 + * @param array $message 提示信息 + * @param bool $batch 是否批量验证 + * @param mixed $callback 回调方法(闭包) * @return array|string|true * @throws ValidateException */ @@ -174,24 +191,27 @@ class Controller $v = Loader::validate(); $v->rule($validate); } else { + // 支持场景 if (strpos($validate, '.')) { - // 支持场景 list($validate, $scene) = explode('.', $validate); } + $v = Loader::validate($validate); - if (!empty($scene)) { - $v->scene($scene); - } + + !empty($scene) && $v->scene($scene); } - // 是否批量验证 + + // 批量验证 if ($batch || $this->batchValidate) { $v->batch(true); } + // 设置错误信息 if (is_array($message)) { $v->message($message); } + // 使用回调验证 if ($callback && is_callable($callback)) { call_user_func_array($callback, [$v, &$data]); } @@ -199,11 +219,11 @@ class Controller if (!$v->check($data)) { if ($this->failException) { throw new ValidateException($v->getError()); - } else { - return $v->getError(); } - } else { - return true; + + return $v->getError(); } + + return true; } } diff --git a/library/think/Cookie.php b/library/think/Cookie.php index 648b2c6086c6937b74ec0b59995606bc52608d45..61b47cced841bdeefa921375a1c0cd8c12093e64 100644 --- a/library/think/Cookie.php +++ b/library/think/Cookie.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2016 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- @@ -13,28 +13,28 @@ namespace think; class Cookie { + /** + * @var array cookie 设置参数 + */ protected static $config = [ - // cookie 名称前缀 - 'prefix' => '', - // cookie 保存时间 - 'expire' => 0, - // cookie 保存路径 - 'path' => '/', - // cookie 有效域名 - 'domain' => '', - // cookie 启用安全传输 - 'secure' => false, - // httponly设置 - 'httponly' => '', - // 是否使用 setcookie - 'setcookie' => true, + 'prefix' => '', // cookie 名称前缀 + 'expire' => 0, // cookie 保存时间 + 'path' => '/', // cookie 保存路径 + 'domain' => '', // cookie 有效域名 + 'secure' => false, // cookie 启用安全传输 + 'httponly' => false, // httponly 设置 + 'setcookie' => true, // 是否使用 setcookie ]; + /** + * @var bool 是否完成初始化了 + */ protected static $init; /** * Cookie初始化 - * @param array $config + * @access public + * @param array $config 配置参数 * @return void */ public static function init(array $config = []) @@ -42,39 +42,43 @@ class Cookie 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); } + self::$init = true; } /** - * 设置或者获取cookie作用域(前缀) - * @param string $prefix - * @return string|void + * 设置或者获取 cookie 作用域(前缀) + * @access public + * @param string $prefix 前缀 + * @return string| */ public static function prefix($prefix = '') { if (empty($prefix)) { return self::$config['prefix']; } - self::$config['prefix'] = $prefix; + + return self::$config['prefix'] = $prefix; } /** * Cookie 设置、获取、删除 - * - * @param string $name cookie名称 - * @param mixed $value cookie值 - * @param mixed $option 可选参数 可能会是 null|integer|string - * - * @return mixed - * @internal param mixed $options cookie参数 + * @access public + * @param string $name cookie 名称 + * @param mixed $value cookie 值 + * @param mixed $option 可选参数 可能会是 null|integer|string + * @return void */ public static function set($name, $value = '', $option = null) { !isset(self::$init) && self::init(); + // 参数设置(会覆盖黙认设置) if (!is_null($option)) { if (is_numeric($option)) { @@ -82,114 +86,183 @@ class Cookie } elseif (is_string($option)) { parse_str($option, $option); } + $config = array_merge(self::$config, array_change_key_case($option)); } else { $config = self::$config; } + $name = $config['prefix'] . $name; - // 设置cookie + + // 设置 cookie if (is_array($value)) { array_walk_recursive($value, 'self::jsonFormatProtect', 'encode'); $value = 'think:' . json_encode($value); } - $expire = !empty($config['expire']) ? $_SERVER['REQUEST_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']); + 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前缀 + * 永久保存 Cookie 数据 + * @access public + * @param string $name cookie 名称 + * @param mixed $value cookie 值 + * @param mixed $option 可选参数 可能会是 null|integer|string + * @return void + */ + public static function forever($name, $value = '', $option = null) + { + if (is_null($option) || is_numeric($option)) { + $option = []; + } + + $option['expire'] = 315360000; + + self::set($name, $value, $option); + } + + /** + * 判断是否有 Cookie 数据 + * @access public + * @param string $name cookie 名称 + * @param string|null $prefix cookie 前缀 * @return bool */ public static function has($name, $prefix = null) { !isset(self::$init) && self::init(); + $prefix = !is_null($prefix) ? $prefix : self::$config['prefix']; - $name = $prefix . $name; - return isset($_COOKIE[$name]); + + return isset($_COOKIE[$prefix . $name]); } /** - * Cookie获取 - * @param string $name cookie名称 - * @param string|null $prefix cookie前缀 + * 获取 Cookie 的值 + * @access public + * @param string $name cookie 名称 + * @param string|null $prefix cookie 前缀 * @return mixed */ - public static function get($name, $prefix = null) + public static function get($name = '', $prefix = null) { !isset(self::$init) && self::init(); + $prefix = !is_null($prefix) ? $prefix : self::$config['prefix']; - $name = $prefix . $name; - if (isset($_COOKIE[$name])) { - $value = $_COOKIE[$name]; + $key = $prefix . $name; + + if ('' == $name) { + // 获取全部 + if ($prefix) { + $value = []; + + foreach ($_COOKIE as $k => $val) { + if (0 === strpos($k, $prefix)) { + $value[$k] = $val; + } + + } + } else { + $value = $_COOKIE; + } + } elseif (isset($_COOKIE[$key])) { + $value = $_COOKIE[$key]; + if (0 === strpos($value, 'think:')) { - $value = substr($value, 6); - $value = json_decode($value, true); + $value = json_decode(substr($value, 6), true); array_walk_recursive($value, 'self::jsonFormatProtect', 'decode'); } - return $value; } else { - return null; + $value = null; } + + return $value; } /** - * Cookie删除 - * @param string $name cookie名称 - * @param string|null $prefix cookie前缀 - * @return mixed + * 删除 Cookie + * @access public + * @param string $name cookie 名称 + * @param string|null $prefix cookie 前缀 + * @return void */ public static function delete($name, $prefix = null) { !isset(self::$init) && self::init(); + $config = self::$config; $prefix = !is_null($prefix) ? $prefix : $config['prefix']; $name = $prefix . $name; + if ($config['setcookie']) { - setcookie($name, '', $_SERVER['REQUEST_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 + + // 删除指定 cookie unset($_COOKIE[$name]); } /** - * Cookie清空 - * @param string|null $prefix cookie前缀 - * @return mixed + * 清除指定前缀的所有 cookie + * @access public + * @param string|null $prefix cookie 前缀 + * @return void */ public static function clear($prefix = null) { - // 清除指定前缀的所有cookie if (empty($_COOKIE)) { return; } + !isset(self::$init) && self::init(); - // 要删除的cookie前缀,不指定则删除config设置的指定前缀 + + // 要删除的 cookie 前缀,不指定则删除 config 设置的指定前缀 $config = self::$config; $prefix = !is_null($prefix) ? $prefix : $config['prefix']; + if ($prefix) { - // 如果前缀为空字符串将不作处理直接返回 foreach ($_COOKIE as $key => $val) { if (0 === strpos($key, $prefix)) { if ($config['setcookie']) { - setcookie($key, '', $_SERVER['REQUEST_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]); } } } - return; } - private static function jsonFormatProtect(&$val, $key, $type = 'encode') + /** + * json 转换时的格式保护 + * @access protected + * @param mixed $val 要转换的值 + * @param string $key 键名 + * @param string $type 转换类别 + * @return void + */ + protected static function jsonFormatProtect(&$val, $key, $type = 'encode') { if (!empty($val) && true !== $val) { $val = 'decode' == $type ? urldecode($val) : urlencode($val); } } - } diff --git a/library/think/Db.php b/library/think/Db.php index b00aee6100a690908bee48c899ffa735e6a7ee92..80f08d243be05353cad1ec77cfcdb2d0a3a6e3c9 100644 --- a/library/think/Db.php +++ b/library/think/Db.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2016 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- @@ -11,10 +11,8 @@ namespace think; -use think\App; -use think\Collection; +use think\db\Connection; use think\db\Query; -use think\paginator\Collection as PaginatorCollection; /** * Class Db @@ -26,40 +24,52 @@ use think\paginator\Collection as PaginatorCollection; * @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 Query cache(mixed $key = true , integer $expire = null) static 设置查询缓存 + * @method Query cache(mixed $key = null , integer $expire = null) static 设置查询缓存 * @method mixed value(string $field) static 获取某个字段的值 * @method array column(string $field, string $key = '') static 获取某个列的值 * @method Query view(mixed $join, mixed $field = null, mixed $on = null, string $type = 'INNER') static 视图查询 - * @method mixed find(mixed $data = []) static 查询单个记录 - * @method mixed select(mixed $data = []) static 查询多个记录 + * @method mixed find(mixed $data = null) static 查询单个记录 + * @method mixed select(mixed $data = null) static 查询多个记录 * @method integer insert(array $data, boolean $replace = false, boolean $getLastInsID = false, string $sequence = null) static 插入一条记录 * @method integer insertGetId(array $data, boolean $replace = false, string $sequence = null) static 插入一条记录并返回自增ID * @method integer insertAll(array $dataSet) static 插入多条记录 * @method integer update(array $data) static 更新记录 - * @method integer delete(mixed $data = []) static 删除记录 + * @method integer delete(mixed $data = null) 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 mixed query(string $sql, array $bind = [], boolean $master = false, bool $pdo = 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, mixed $simple = false, array $config = []) static 分页查询 + * @method Paginator paginate(integer $listRows = 15, mixed $simple = null, array $config = []) static 分页查询 * @method mixed transaction(callable $callback) static 执行数据库事务 + * @method void startTrans() static 启动事务 + * @method void commit() static 用于非自动提交状态下面的查询提交 + * @method void rollback() static 事务回滚 * @method boolean batchQuery(array $sqlArray) static 批处理执行SQL语句 + * @method string quote(string $str) static SQL指令安全过滤 + * @method string getLastInsID($sequence = null) static 获取最近插入的ID */ class Db { - // 数据库连接实例 + /** + * @var Connection[] 数据库连接实例 + */ private static $instance = []; - // 查询次数 + + /** + * @var int 查询次数 + */ public static $queryTimes = 0; - // 执行次数 + + /** + * @var int 执行次数 + */ public static $executeTimes = 0; /** - * 数据库初始化 并取得数据库类实例 - * @static + * 数据库初始化,并取得数据库类实例 * @access public - * @param mixed $config 连接配置 - * @param bool|string $name 连接标识 true 强制重新连接 - * @return \think\db\Connection + * @param mixed $config 连接配置 + * @param bool|string $name 连接标识 true 强制重新连接 + * @return Connection * @throws Exception */ public static function connect($config = [], $name = false) @@ -67,31 +77,48 @@ class Db if (false === $name) { $name = md5(serialize($config)); } + if (true === $name || !isset(self::$instance[$name])) { // 解析连接参数 支持数组和字符串 $options = self::parseConfig($config); + if (empty($options['type'])) { - throw new \InvalidArgumentException('Underfined db type'); + throw new \InvalidArgumentException('Undefined db type'); } - $class = false !== strpos($options['type'], '\\') ? $options['type'] : '\\think\\db\\connector\\' . ucwords($options['type']); + + $class = false !== strpos($options['type'], '\\') ? + $options['type'] : + '\\think\\db\\connector\\' . ucwords($options['type']); + // 记录初始化信息 if (App::$debug) { Log::record('[ DB ] INIT ' . $options['type'], 'info'); } + if (true === $name) { - return new $class($options); - } else { - self::$instance[$name] = new $class($options); + $name = md5(serialize($config)); } + + self::$instance[$name] = new $class($options); } + return self::$instance[$name]; } + /** + * 清除连接实例 + * @access public + * @return void + */ + public static function clear() + { + self::$instance = []; + } + /** * 数据库连接参数解析 - * @static * @access private - * @param mixed $config + * @param mixed $config 连接参数 * @return array */ private static function parseConfig($config) @@ -99,30 +126,27 @@ class Db if (empty($config)) { $config = Config::get('database'); } elseif (is_string($config) && false === strpos($config, '/')) { - // 支持读取配置参数 - $config = Config::get($config); - } - if (is_string($config)) { - return self::parseDsn($config); - } else { - return $config; + $config = Config::get($config); // 支持读取配置参数 } + + return is_string($config) ? self::parseDsn($config) : $config; } /** - * DSN解析 + * DSN 解析 * 格式: mysql://username:passwd@localhost:3306/DbName?param1=val1¶m2=val2#utf8 - * @static * @access private - * @param string $dsnStr + * @param string $dsnStr 数据库 DSN 字符串解析 * @return array */ private static function parseDsn($dsnStr) { $info = parse_url($dsnStr); + if (!$info) { return []; } + $dsn = [ 'type' => $info['scheme'], 'username' => isset($info['user']) ? $info['user'] : '', @@ -138,13 +162,19 @@ class Db } else { $dsn['params'] = []; } + return $dsn; } - // 调用驱动类的方法 + /** + * 调用驱动类的方法 + * @access public + * @param string $method 方法名 + * @param array $params 参数 + * @return mixed + */ public static function __callStatic($method, $params) { - // 自动初始化数据库 return call_user_func_array([self::connect(), $method], $params); } } diff --git a/library/think/Debug.php b/library/think/Debug.php index e36a764e7e939f78f59ed4bab5002c3dc7d4534b..df487485c91df86e356a874c123d93245afc7d2c 100644 --- a/library/think/Debug.php +++ b/library/think/Debug.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2016 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- @@ -11,30 +11,32 @@ namespace think; -use think\Config; use think\exception\ClassNotFoundException; -use think\Log; -use think\Request; -use think\Response; use think\response\Redirect; class Debug { - // 区间时间信息 + /** + * @var array 区间时间信息 + */ protected static $info = []; - // 区间内存信息 + + /** + * @var array 区间内存信息 + */ protected static $mem = []; /** * 记录时间(微秒)和内存使用情况 - * @param string $name 标记位置 - * @param mixed $value 标记值 留空则取当前 time 表示仅记录时间 否则同时记录时间和内存 - * @return mixed + * @access public + * @param string $name 标记位置 + * @param mixed $value 标记值(留空则取当前 time 表示仅记录时间 否则同时记录时间和内存) + * @return void */ public static function remark($name, $value = '') { - // 记录时间和内存使用 self::$info[$name] = is_float($value) ? $value : microtime(true); + if ('time' != $value) { self::$mem['mem'][$name] = is_float($value) ? $value : memory_get_usage(); self::$mem['peak'][$name] = memory_get_peak_usage(); @@ -42,24 +44,27 @@ class Debug } /** - * 统计某个区间的时间(微秒)使用情况 - * @param string $start 开始标签 - * @param string $end 结束标签 - * @param integer|string $dec 小数位 - * @return integer + * 统计某个区间的时间(微秒)使用情况 返回值以秒为单位 + * @access public + * @param string $start 开始标签 + * @param string $end 结束标签 + * @param integer $dec 小数位 + * @return string */ public static function getRangeTime($start, $end, $dec = 6) { if (!isset(self::$info[$end])) { self::$info[$end] = microtime(true); } + return number_format((self::$info[$end] - self::$info[$start]), $dec); } /** - * 统计从开始到统计时的时间(微秒)使用情况 - * @param integer|string $dec 小数位 - * @return integer + * 统计从开始到统计时的时间(微秒)使用情况 返回值以秒为单位 + * @access public + * @param integer $dec 小数位 + * @return string */ public static function getUseTime($dec = 6) { @@ -68,6 +73,7 @@ class Debug /** * 获取当前访问的吞吐率情况 + * @access public * @return string */ public static function getThroughputRate() @@ -77,9 +83,10 @@ class Debug /** * 记录区间的内存使用情况 - * @param string $start 开始标签 - * @param string $end 结束标签 - * @param integer|string $dec 小数位 + * @access public + * @param string $start 开始标签 + * @param string $end 结束标签 + * @param integer $dec 小数位 * @return string */ public static function getRangeMem($start, $end, $dec = 2) @@ -87,19 +94,23 @@ class Debug if (!isset(self::$mem['mem'][$end])) { self::$mem['mem'][$end] = memory_get_usage(); } + $size = self::$mem['mem'][$end] - self::$mem['mem'][$start]; $a = ['B', 'KB', 'MB', 'GB', 'TB']; $pos = 0; + while ($size >= 1024) { $size /= 1024; $pos++; } + return round($size, $dec) . " " . $a[$pos]; } /** * 统计从开始到统计时的内存使用情况 - * @param integer|string $dec 小数位 + * @access public + * @param integer $dec 小数位 * @return string */ public static function getUseMem($dec = 2) @@ -107,103 +118,128 @@ class Debug $size = memory_get_usage() - THINK_START_MEM; $a = ['B', 'KB', 'MB', 'GB', 'TB']; $pos = 0; + while ($size >= 1024) { $size /= 1024; $pos++; } + return round($size, $dec) . " " . $a[$pos]; } /** * 统计区间的内存峰值情况 - * @param string $start 开始标签 - * @param string $end 结束标签 - * @param integer|string $dec 小数位 - * @return mixed + * @access public + * @param string $start 开始标签 + * @param string $end 结束标签 + * @param integer $dec 小数位 + * @return string */ public static function getMemPeak($start, $end, $dec = 2) { if (!isset(self::$mem['peak'][$end])) { self::$mem['peak'][$end] = memory_get_peak_usage(); } + $size = self::$mem['peak'][$end] - self::$mem['peak'][$start]; $a = ['B', 'KB', 'MB', 'GB', 'TB']; $pos = 0; + while ($size >= 1024) { $size /= 1024; $pos++; } + return round($size, $dec) . " " . $a[$pos]; } /** * 获取文件加载信息 - * @param bool $detail 是否显示详细 + * @access public + * @param bool $detail 是否显示详细 * @return integer|array */ public static function getFile($detail = false) { + $files = get_included_files(); + if ($detail) { - $files = get_included_files(); - $info = []; - foreach ($files as $key => $file) { + $info = []; + + foreach ($files as $file) { $info[] = $file . ' ( ' . number_format(filesize($file) / 1024, 2) . ' KB )'; } + return $info; } - return count(get_included_files()); + + return count($files); } /** * 浏览器友好的变量输出 - * @param mixed $var 变量 - * @param boolean $echo 是否输出 默认为true 如果为false 则返回输出字符串 - * @param string $label 标签 默认为空 - * @param integer $flags htmlspecialchars flags - * @return void|string + * @access public + * @param mixed $var 变量 + * @param boolean $echo 是否输出(默认为 true,为 false 则返回输出字符串) + * @param string|null $label 标签(默认为空) + * @param integer $flags htmlspecialchars 的标志 + * @return null|string */ public static function dump($var, $echo = true, $label = null, $flags = ENT_SUBSTITUTE) { $label = (null === $label) ? '' : rtrim($label) . ':'; + ob_start(); var_dump($var); - $output = ob_get_clean(); - $output = preg_replace('/\]\=\>\n(\s+)/m', '] => ', $output); + $output = preg_replace('/\]\=\>\n(\s+)/m', '] => ', ob_get_clean()); + if (IS_CLI) { $output = PHP_EOL . $label . $output . PHP_EOL; } else { if (!extension_loaded('xdebug')) { $output = htmlspecialchars($output, $flags); } + $output = '
' . $label . $output . '
'; } + if ($echo) { - echo ($output); - return null; - } else { - return $output; + echo($output); + return; } + + return $output; } + /** + * 调试信息注入到响应中 + * @access public + * @param Response $response 响应实例 + * @param string $content 返回的字符串 + * @return void + */ public static function inject(Response $response, &$content) { - $config = Config::get('trace'); - $type = isset($config['type']) ? $config['type'] : 'Html'; - $request = Request::instance(); - $class = false !== strpos($type, '\\') ? $type : '\\think\\debug\\' . ucwords($type); + $config = Config::get('trace'); + $type = isset($config['type']) ? $config['type'] : 'Html'; + $class = false !== strpos($type, '\\') ? $type : '\\think\\debug\\' . ucwords($type); + unset($config['type']); - if (class_exists($class)) { - $trace = new $class($config); - } else { + + if (!class_exists($class)) { throw new ClassNotFoundException('class not exists:' . $class, $class); } + /** @var \think\debug\Console|\think\debug\Html $trace */ + $trace = new $class($config); + if ($response instanceof Redirect) { - //TODO 记录 + // TODO 记录 } else { $output = $trace->output($response, Log::getLog()); + if (is_string($output)) { - // trace调试信息注入 + // trace 调试信息注入 $pos = strripos($content, ''); if (false !== $pos) { $content = substr($content, 0, $pos) . $output . substr($content, $pos); diff --git a/library/think/Env.php b/library/think/Env.php index a23d9925e335c33009eebbba986ce79e5c78d70b..0a8b250974d66b4769f66dd58097a9f4cfea87e5 100644 --- a/library/think/Env.php +++ b/library/think/Env.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2016 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- @@ -15,17 +15,25 @@ class Env { /** * 获取环境变量值 - * @param string $name 环境变量名(支持二级 .号分割) - * @param string $default 默认值 + * @access public + * @param string $name 环境变量名(支持二级 . 号分割) + * @param string $default 默认值 * @return mixed */ public static function get($name, $default = null) { $result = getenv(ENV_PREFIX . strtoupper(str_replace('.', '_', $name))); + if (false !== $result) { + if ('false' === $result) { + $result = false; + } elseif ('true' === $result) { + $result = true; + } + return $result; - } else { - return $default; } + + return $default; } } diff --git a/library/think/Error.php b/library/think/Error.php index 28f758823275766a9dc7cedd4866e8d6b5400b31..5f361d5896dbdc13b4b3e0d347f48abb8337c1b7 100644 --- a/library/think/Error.php +++ b/library/think/Error.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2016 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- @@ -20,6 +20,7 @@ class Error { /** * 注册异常处理 + * @access public * @return void */ public static function register() @@ -31,8 +32,10 @@ class Error } /** - * Exception Handler - * @param \Exception|\Throwable $e + * 异常处理 + * @access public + * @param \Exception|\Throwable $e 异常 + * @return void */ public static function appException($e) { @@ -40,44 +43,50 @@ class Error $e = new ThrowableError($e); } - self::getExceptionHandler()->report($e); + $handler = self::getExceptionHandler(); + $handler->report($e); + if (IS_CLI) { - self::getExceptionHandler()->renderForConsole(new ConsoleOutput, $e); + $handler->renderForConsole(new ConsoleOutput, $e); } else { - self::getExceptionHandler()->render($e)->send(); + $handler->render($e)->send(); } } /** - * Error Handler - * @param integer $errno 错误编号 - * @param integer $errstr 详细错误信息 - * @param string $errfile 出错的文件 - * @param integer $errline 出错行号 - * @param array $errcontext + * 错误处理 + * @access public + * @param integer $errno 错误编号 + * @param integer $errstr 详细错误信息 + * @param string $errfile 出错的文件 + * @param integer $errline 出错行号 + * @return void * @throws ErrorException */ - public static function appError($errno, $errstr, $errfile = '', $errline = 0, $errcontext = []) + public static function appError($errno, $errstr, $errfile = '', $errline = 0) { - $exception = new ErrorException($errno, $errstr, $errfile, $errline, $errcontext); + $exception = new ErrorException($errno, $errstr, $errfile, $errline); + + // 符合异常处理的则将错误信息托管至 think\exception\ErrorException if (error_reporting() & $errno) { - // 将错误信息托管至 think\exception\ErrorException throw $exception; - } else { - self::getExceptionHandler()->report($exception); } + + self::getExceptionHandler()->report($exception); } /** - * Shutdown Handler + * 异常中止处理 + * @access public + * @return void */ public static function appShutdown() { + // 将错误信息托管至 think\ErrorException 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); + self::appException(new ErrorException( + $error['type'], $error['message'], $error['file'], $error['line'] + )); } // 写入日志 @@ -86,8 +95,8 @@ class Error /** * 确定错误类型是否致命 - * - * @param int $type + * @access protected + * @param int $type 错误类型 * @return bool */ protected static function isFatal($type) @@ -96,22 +105,32 @@ class Error } /** - * Get an instance of the exception handler. - * + * 获取异常处理的实例 + * @access public * @return Handle */ public static function getExceptionHandler() { static $handle; + if (!$handle) { - // 异常处理handle + // 异常处理 handle $class = Config::get('exception_handle'); - if ($class && class_exists($class) && is_subclass_of($class, "\\think\\exception\\Handle")) { + + if ($class && is_string($class) && class_exists($class) && + is_subclass_of($class, "\\think\\exception\\Handle") + ) { $handle = new $class; } else { $handle = new Handle; + + if ($class instanceof \Closure) { + $handle->setRender($class); + } + } } + return $handle; } } diff --git a/library/think/Exception.php b/library/think/Exception.php index ac6487647b97c1ec4c4dedd9115ed2cbf629dcb8..1ef06bdbd3a03979100e8f2d3e33f2f254f819bd 100644 --- a/library/think/Exception.php +++ b/library/think/Exception.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2016 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- @@ -13,15 +13,13 @@ namespace think; class Exception extends \Exception { - /** - * 保存异常页面显示的额外Debug数据 - * @var array + * @var array 保存异常页面显示的额外 Debug 数据 */ protected $data = []; /** - * 设置异常额外的Debug数据 + * 设置异常额外的 Debug 数据 * 数据将会显示为下面的格式 * * Exception Data @@ -33,8 +31,10 @@ class Exception extends \Exception * key1 value1 * key2 value2 * - * @param string $label 数据分类,用于异常页面显示 - * @param array $data 需要显示的数据,必须为关联数组 + * @access protected + * @param string $label 数据分类,用于异常页面显示 + * @param array $data 需要显示的数据,必须为关联数组 + * @return void */ final protected function setData($label, array $data) { @@ -42,13 +42,14 @@ class Exception extends \Exception } /** - * 获取异常额外Debug数据 + * 获取异常额外 Debug 数据 * 主要用于输出到异常页面便于调试 - * @return array 由setData设置的Debug数据 + * @access public + * @return array */ final public function getData() { return $this->data; } - + } diff --git a/library/think/File.php b/library/think/File.php index 43f3857519d038fc0a4c3a60f0bb7becaf790e41..d2ed22083f0732ef405e02aed7461e572db2bbf6 100644 --- a/library/think/File.php +++ b/library/think/File.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2016 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- @@ -11,62 +11,92 @@ namespace think; -use SplFileInfo; use SplFileObject; class File extends SplFileObject { /** - * 错误信息 - * @var string + * @var string 错误信息 */ private $error = ''; - // 当前完整文件名 + + /** + * @var string 当前完整文件名 + */ protected $filename; - // 上传文件名 + + /** + * @var string 上传文件名 + */ protected $saveName; - // 文件上传命名规则 + + /** + * @var string 文件上传命名规则 + */ protected $rule = 'date'; - // 文件上传验证规则 + + /** + * @var array 文件上传验证规则 + */ protected $validate = []; - // 单元测试 + + /** + * @var bool 单元测试 + */ protected $isTest; - // 上传文件信息 + + /** + * @var array 上传文件信息 + */ protected $info; - // 文件hash信息 + + /** + * @var array 文件 hash 信息 + */ protected $hash = []; + /** + * File constructor. + * @access public + * @param string $filename 文件名称 + * @param string $mode 访问模式 + */ public function __construct($filename, $mode = 'r') { parent::__construct($filename, $mode); - $this->filename = $this->getRealPath(); + $this->filename = $this->getRealPath() ?: $this->getPathname(); } /** - * 是否测试 - * @param bool $test 是否测试 + * 设置是否是单元测试 + * @access public + * @param bool $test 是否是测试 * @return $this */ public function isTest($test = false) { $this->isTest = $test; + return $this; } /** * 设置上传信息 - * @param array $info 上传文件信息 + * @access public + * @param array $info 上传文件信息 * @return $this */ public function setUploadInfo($info) { $this->info = $info; + return $this; } /** * 获取上传文件的信息 - * @param string $name + * @access public + * @param string $name 信息名称 * @return array|string */ public function getInfo($name = '') @@ -76,6 +106,7 @@ class File extends SplFileObject /** * 获取上传文件的文件名 + * @access public * @return string */ public function getSaveName() @@ -85,93 +116,101 @@ class File extends SplFileObject /** * 设置上传文件的保存文件名 - * @param string $saveName + * @access public + * @param string $saveName 保存名称 * @return $this */ public function setSaveName($saveName) { $this->saveName = $saveName; + return $this; } /** * 获取文件的哈希散列值 - * @return $string + * @access public + * @param string $type 类型 + * @return string */ public function hash($type = 'sha1') { if (!isset($this->hash[$type])) { $this->hash[$type] = hash_file($type, $this->filename); } + return $this->hash[$type]; } /** * 检查目录是否可写 - * @param string $path 目录 + * @access protected + * @param string $path 目录 * @return boolean */ protected function checkPath($path) { - if (is_dir($path)) { + if (is_dir($path) || mkdir($path, 0755, true)) { return true; } - if (mkdir($path, 0755, true)) { - return true; - } else { - $this->error = "目录 {$path} 创建失败!"; - return false; - } + $this->error = ['directory {:path} creation failed', ['path' => $path]]; + + return false; } /** * 获取文件类型信息 + * @access public * @return string */ public function getMime() { $finfo = finfo_open(FILEINFO_MIME_TYPE); + return finfo_file($finfo, $this->filename); } /** * 设置文件的命名规则 - * @param string $rule 文件命名规则 + * @access public + * @param string $rule 文件命名规则 * @return $this */ public function rule($rule) { $this->rule = $rule; + return $this; } /** * 设置上传文件的验证规则 - * @param array $rule 验证规则 + * @access public + * @param array $rule 验证规则 * @return $this */ - public function validate($rule = []) + public function validate(array $rule = []) { $this->validate = $rule; + return $this; } /** * 检测是否合法的上传文件 + * @access public * @return bool */ public function isValid() { - if ($this->isTest) { - return is_file($this->filename); - } - return is_uploaded_file($this->filename); + return $this->isTest ? is_file($this->filename) : is_uploaded_file($this->filename); } /** * 检测上传文件 - * @param array $rule 验证规则 + * @access public + * @param array $rule 验证规则 * @return bool */ public function check($rule = []) @@ -180,25 +219,25 @@ class File extends SplFileObject /* 检查文件大小 */ if (isset($rule['size']) && !$this->checkSize($rule['size'])) { - $this->error = '上传文件大小不符!'; + $this->error = 'filesize not match'; return false; } - /* 检查文件Mime类型 */ + /* 检查文件 Mime 类型 */ if (isset($rule['type']) && !$this->checkMime($rule['type'])) { - $this->error = '上传文件MIME类型不允许!'; + $this->error = 'mimetype to upload is not allowed'; return false; } /* 检查文件后缀 */ if (isset($rule['ext']) && !$this->checkExt($rule['ext'])) { - $this->error = '上传文件后缀不允许'; + $this->error = 'extensions to upload is not allowed'; return false; } /* 检查图像文件 */ if (!$this->checkImg()) { - $this->error = '非法图像文件!'; + $this->error = 'illegal image files'; return false; } @@ -207,7 +246,8 @@ class File extends SplFileObject /** * 检测上传文件后缀 - * @param array|string $ext 允许后缀 + * @access public + * @param array|string $ext 允许后缀 * @return bool */ public function checkExt($ext) @@ -215,73 +255,76 @@ class File extends SplFileObject if (is_string($ext)) { $ext = explode(',', $ext); } + $extension = strtolower(pathinfo($this->getInfo('name'), PATHINFO_EXTENSION)); - if (!in_array($extension, $ext)) { - return false; - } - return true; + + return in_array($extension, $ext); } /** * 检测图像文件 + * @access public * @return bool */ public function checkImg() { $extension = strtolower(pathinfo($this->getInfo('name'), PATHINFO_EXTENSION)); - /* 对图像文件进行严格检测 */ - if (in_array($extension, ['gif', 'jpg', 'jpeg', 'bmp', 'png', 'swf']) && !in_array($this->getImageType($this->filename), [1, 2, 3, 4, 6])) { - return false; - } - return true; + + // 如果上传的不是图片,或者是图片而且后缀确实符合图片类型则返回 true + return !in_array($extension, ['gif', 'jpg', 'jpeg', 'bmp', 'png', 'swf']) || in_array($this->getImageType($this->filename), [1, 2, 3, 4, 6, 13]); } - // 判断图像类型 + /** + * 判断图像类型 + * @access protected + * @param string $image 图片名称 + * @return bool|int + */ protected function getImageType($image) { if (function_exists('exif_imagetype')) { return exif_imagetype($image); - } else { + } + + try { $info = getimagesize($image); - return $info[2]; + return $info ? $info[2] : false; + } catch (\Exception $e) { + return false; } } /** * 检测上传文件大小 - * @param integer $size 最大大小 + * @access public + * @param integer $size 最大大小 * @return bool */ public function checkSize($size) { - if ($this->getSize() > $size) { - return false; - } - return true; + return $this->getSize() <= $size; } /** * 检测上传文件类型 - * @param array|string $mime 允许类型 + * @access public + * @param array|string $mime 允许类型 * @return bool */ public function checkMime($mime) { - if (is_string($mime)) { - $mime = explode(',', $mime); - } - if (!in_array(strtolower($this->getMime()), $mime)) { - return false; - } - return true; + $mime = is_string($mime) ? explode(',', $mime) : $mime; + + return in_array(strtolower($this->getMime()), $mime); } /** * 移动文件 - * @param string $path 保存路径 - * @param string|bool $savename 保存的文件名 默认自动生成 - * @param boolean $replace 同名文件是否覆盖 - * @return false|SplFileInfo false-失败 否则返回SplFileInfo实例 + * @access public + * @param string $path 保存路径 + * @param string|bool $savename 保存的文件名 默认自动生成 + * @param boolean $replace 同名文件是否覆盖 + * @return false|File */ public function move($path, $savename = true, $replace = true) { @@ -293,7 +336,7 @@ class File extends SplFileObject // 检测合法性 if (!$this->isValid()) { - $this->error = '非法上传文件'; + $this->error = 'upload illegal files'; return false; } @@ -301,6 +344,7 @@ class File extends SplFileObject if (!$this->check()) { return false; } + $path = rtrim($path, DS) . DS; // 文件保存命名规则 $saveName = $this->buildSaveName($savename); @@ -311,9 +355,9 @@ class File extends SplFileObject return false; } - /* 不覆盖同名文件 */ + // 不覆盖同名文件 if (!$replace && is_file($filename)) { - $this->error = '存在同名文件' . $filename; + $this->error = ['has the same filename: {:filename}', ['filename' => $filename]]; return false; } @@ -321,25 +365,27 @@ class File extends SplFileObject if ($this->isTest) { rename($this->filename, $filename); } elseif (!move_uploaded_file($this->filename, $filename)) { - $this->error = '文件上传保存错误!'; + $this->error = 'upload write error'; return false; } - // 返回 File对象实例 + + // 返回 File 对象实例 $file = new self($filename); - $file->setSaveName($saveName); - $file->setUploadInfo($this->info); + $file->setSaveName($saveName)->setUploadInfo($this->info); + return $file; } /** * 获取保存文件名 - * @param string|bool $savename 保存的文件名 默认自动生成 + * @access protected + * @param string|bool $savename 保存的文件名 默认自动生成 * @return string */ protected function buildSaveName($savename) { + // 自动生成文件名 if (true === $savename) { - // 自动生成文件名 if ($this->rule instanceof \Closure) { $savename = call_user_func_array($this->rule, [$this]); } else { @@ -358,52 +404,73 @@ class File extends SplFileObject } } } - } elseif ('' === $savename) { + } elseif ('' === $savename || false === $savename) { $savename = $this->getInfo('name'); } + if (!strpos($savename, '.')) { $savename .= '.' . pathinfo($this->getInfo('name'), PATHINFO_EXTENSION); } + return $savename; } /** * 获取错误代码信息 - * @param int $errorNo 错误号 + * @access private + * @param int $errorNo 错误号 + * @return $this */ private function error($errorNo) { switch ($errorNo) { case 1: case 2: - $this->error = '上传文件大小超过了最大值!'; + $this->error = 'upload File size exceeds the maximum value'; break; case 3: - $this->error = '文件只有部分被上传!'; + $this->error = 'only the portion of file is uploaded'; break; case 4: - $this->error = '没有文件被上传!'; + $this->error = 'no file to uploaded'; break; case 6: - $this->error = '找不到临时文件夹!'; + $this->error = 'upload temp dir not found'; break; case 7: - $this->error = '文件写入失败!'; + $this->error = 'file write error'; break; default: - $this->error = '未知上传错误!'; + $this->error = 'unknown upload error'; } + + return $this; } /** - * 获取错误信息 - * @return mixed + * 获取错误信息(支持多语言) + * @access public + * @return string */ public function getError() { - return $this->error; + if (is_array($this->error)) { + list($msg, $vars) = $this->error; + } else { + $msg = $this->error; + $vars = []; + } + + return Lang::has($msg) ? Lang::get($msg, $vars) : $msg; } + /** + * 魔法方法,获取文件的 hash 值 + * @access public + * @param string $method 方法名 + * @param mixed $args 调用参数 + * @return string + */ public function __call($method, $args) { return $this->hash($method); diff --git a/library/think/Hook.php b/library/think/Hook.php index 062ac962d6588fdc31efe2c48ca7e3eb6ffb0595..a69ce546d5a3854722a945f6a0dde1b15660688c 100644 --- a/library/think/Hook.php +++ b/library/think/Hook.php @@ -1,9 +1,8 @@ $behavior) { + self::add($tag, $behavior); + } + } else { + self::$tags = $tags + self::$tags; + } + } + + /** + * 获取插件信息 + * @access public + * @param string $tag 插件位置(留空获取全部) + * @return array + */ + public static function get($tag = '') + { + if (empty($tag)) { + return self::$tags; + } + + return array_key_exists($tag, self::$tags) ? self::$tags[$tag] : []; + } + + /** + * 监听标签的行为 + * @access public + * @param string $tag 标签名称 + * @param mixed $params 传入参数 + * @param mixed $extra 额外参数 + * @param bool $once 只获取一个有效返回值 + * @return mixed + */ + public static function listen($tag, &$params = null, $extra = null, $once = false) + { + $results = []; + + foreach (static::get($tag) as $key => $name) { + $results[$key] = self::exec($name, $tag, $params, $extra); + + // 如果返回 false,或者仅获取一个有效返回则中断行为执行 + if (false === $results[$key] || (!is_null($results[$key]) && $once)) { + break; + } + } + + return $once ? end($results) : $results; + } + + /** + * 执行某个行为 + * @access public + * @param mixed $class 要执行的行为 + * @param string $tag 方法名(标签名) + * @param mixed $params 传人的参数 + * @param mixed $extra 额外参数 + * @return mixed + */ + public static function exec($class, $tag = '', &$params = null, $extra = null) + { + App::$debug && Debug::remark('behavior_start', 'time'); + + $method = Loader::parseName($tag, 1, false); + + if ($class instanceof \Closure) { + $result = call_user_func_array($class, [ & $params, $extra]); + $class = 'Closure'; + } elseif (is_array($class)) { + list($class, $method) = $class; + + $result = (new $class())->$method($params, $extra); + $class = $class . '->' . $method; + } elseif (is_object($class)) { + $result = $class->$method($params, $extra); + $class = get_class($class); + } elseif (strpos($class, '::')) { + $result = call_user_func_array($class, [ & $params, $extra]); + } else { + $obj = new $class(); + $method = ($tag && is_callable([$obj, $method])) ? $method : 'run'; + $result = $obj->$method($params, $extra); + } + + if (App::$debug) { + Debug::remark('behavior_end', 'time'); + Log::record('[ BEHAVIOR ] Run ' . $class . ' @' . $tag . ' [ RunTime:' . Debug::getRangeTime('behavior_start', 'behavior_end') . 's ]', 'info'); + } - private static $tags = []; - - /** - * 动态添加行为扩展到某个标签 - * @param string $tag 标签名称 - * @param mixed $behavior 行为名称 - * @param bool $first 是否放到开头执行 - * @return void - */ - public static function add($tag, $behavior, $first = false) - { - isset(self::$tags[$tag]) || self::$tags[$tag] = []; - if (is_array($behavior) && !is_callable($behavior)) { - if (!array_key_exists('_overlay', $behavior) || !$behavior['_overlay']) { - unset($behavior['_overlay']); - self::$tags[$tag] = array_merge(self::$tags[$tag], $behavior); - } else { - unset($behavior['_overlay']); - self::$tags[$tag] = $behavior; - } - } elseif ($first) { - array_unshift(self::$tags[$tag], $behavior); - } else { - self::$tags[$tag][] = $behavior; - } - } - - /** - * 批量导入插件 - * @param array $tags 插件信息 - * @param boolean $recursive 是否递归合并 - */ - public static function import(array $tags, $recursive = true) - { - if ($recursive) { - foreach ($tags as $tag => $behavior) { - self::add($tag, $behavior); - } - } else { - self::$tags = $tags + self::$tags; - } - } - - /** - * 获取插件信息 - * @param string $tag 插件位置 留空获取全部 - * @return array - */ - public static function get($tag = '') - { - if (empty($tag)) {//获取全部的插件信息 - return self::$tags; - } else { - return array_key_exists($tag, self::$tags) ? self::$tags[$tag] : []; - } - } - - /** - * 监听标签的行为 - * @param string $tag 标签名称 - * @param mixed $params 传入参数 - * @param mixed $extra 额外参数 - * @param bool $once 只获取一个有效返回值 - * @return mixed - */ - public static function listen($tag, &$params = null, $extra = null, $once = false) - { - $results = []; - $tags = static::get($tag); - foreach ($tags as $key => $name) { - $results[$key] = self::exec($name, $tag, $params, $extra); - if (false === $results[$key]) {// 如果返回false 则中断行为执行 - break; - } elseif (!is_null($results[$key]) && $once) { - break; - } - } - return $once ? end($results) : $results; - } - - /** - * 执行某个行为 - * @param mixed $class 要执行的行为 - * @param string $tag 方法名(标签名) - * @param Mixed $params 传人的参数 - * @param mixed $extra 额外参数 - * @return mixed - */ - public static function exec($class, $tag = '', &$params = null, $extra = null) - { - App::$debug && Debug::remark('behavior_start', 'time'); - if (is_callable($class)) { - $result = call_user_func_array($class, [ & $params, $extra]); - $class = 'Closure'; - } elseif (is_object($class)) { - $result = call_user_func_array([$class, $tag], [ & $params, $extra]); - $class = get_class($class); - } else { - $obj = new $class(); - $result = ($tag && is_callable([$obj, $tag])) ? $obj->$tag($params, $extra) : $obj->run($params, $extra); - } - if (App::$debug) { - Debug::remark('behavior_end', 'time'); - Log::record('[ BEHAVIOR ] Run ' . $class . ' @' . $tag . ' [ RunTime:' . Debug::getRangeTime('behavior_start', 'behavior_end') . 's ]', 'info'); - } - return $result; - } + return $result; + } } diff --git a/library/think/Lang.php b/library/think/Lang.php index 16e7f54558de8f60e22933b7ca3bd8ed3f46108d..a50d838de907d860f5ad2c59e41521193c80efca 100644 --- a/library/think/Lang.php +++ b/library/think/Lang.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2016 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- @@ -11,114 +11,150 @@ namespace think; -use think\App; -use think\Cookie; -use think\Log; - class Lang { - // 语言数据 + /** + * @var array 语言数据 + */ private static $lang = []; - // 语言作用域 + + /** + * @var string 语言作用域 + */ private static $range = 'zh-cn'; - // 语言自动侦测的变量 + + /** + * @var string 语言自动侦测的变量 + */ protected static $langDetectVar = 'lang'; - // 语言Cookie变量 + + /** + * @var string 语言 Cookie 变量 + */ protected static $langCookieVar = 'think_var'; - // 语言Cookie的过期时间 + + /** + * @var int 语言 Cookie 的过期时间 + */ protected static $langCookieExpire = 3600; - // 允许语言列表 + + /** + * @var array 允许语言列表 + */ protected static $allowLangList = []; - // 设定当前的语言 + /** + * @var array Accept-Language 转义为对应语言包名称 系统默认配置 + */ + protected static $acceptLanguage = ['zh-hans-cn' => 'zh-cn']; + + /** + * 设定当前的语言 + * @access public + * @param string $range 语言作用域 + * @return string + */ public static function range($range = '') { - if ('' == $range) { - return self::$range; - } else { + if ($range) { self::$range = $range; } + + return self::$range; } /** * 设置语言定义(不区分大小写) - * @param string|array $name 语言变量 - * @param string $value 语言值 - * @param string $range 语言作用域 + * @access public + * @param string|array $name 语言变量 + * @param string $value 语言值 + * @param string $range 语言作用域 * @return mixed */ public static function set($name, $value = null, $range = '') { $range = $range ?: self::$range; - // 批量定义 + if (!isset(self::$lang[$range])) { self::$lang[$range] = []; } + if (is_array($name)) { return self::$lang[$range] = array_change_key_case($name) + self::$lang[$range]; - } else { - return self::$lang[$range][strtolower($name)] = $value; } + + return self::$lang[$range][strtolower($name)] = $value; } /** * 加载语言定义(不区分大小写) - * @param string $file 语言文件 - * @param string $range 语言作用域 + * @access public + * @param array|string $file 语言文件 + * @param string $range 语言作用域 * @return mixed */ public static function load($file, $range = '') { $range = $range ?: self::$range; + $file = is_string($file) ? [$file] : $file; + if (!isset(self::$lang[$range])) { self::$lang[$range] = []; } - // 批量定义 - if (is_string($file)) { - $file = [$file]; - } + $lang = []; + foreach ($file as $_file) { if (is_file($_file)) { // 记录加载信息 App::$debug && Log::record('[ LANG ] ' . $_file, 'info'); + $_lang = include $_file; - $lang = array_change_key_case($_lang) + $lang; + + if (is_array($_lang)) { + $lang = array_change_key_case($_lang) + $lang; + } } } + if (!empty($lang)) { self::$lang[$range] = $lang + self::$lang[$range]; } + return self::$lang[$range]; } /** * 获取语言定义(不区分大小写) - * @param string|null $name 语言变量 - * @param array $vars 变量替换 - * @param string $range 语言作用域 + * @access public + * @param string|null $name 语言变量 + * @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 语言作用域 + * @access public + * @param string|null $name 语言变量 + * @param array $vars 变量替换 + * @param string $range 语言作用域 * @return mixed */ public static function get($name = null, $vars = [], $range = '') { $range = $range ?: self::$range; + // 空参数返回所有定义 if (empty($name)) { return self::$lang[$range]; } + $key = strtolower($name); $value = isset(self::$lang[$range][$key]) ? self::$lang[$range][$key] : $name; @@ -143,43 +179,50 @@ class Lang } } + return $value; } /** * 自动侦测设置获取语言选择 + * @access public * @return string */ public static function detect() { - // 自动侦测设置获取语言选择 $langSet = ''; + if (isset($_GET[self::$langDetectVar])) { - // url中设置了语言变量 + // url 中设置了语言变量 $langSet = strtolower($_GET[self::$langDetectVar]); - Cookie::set(self::$langCookieVar, $langSet, self::$langCookieExpire); - } elseif (Cookie::get(self::$langCookieVar)) { - // 获取上次用户的选择 - $langSet = strtolower(Cookie::get(self::$langCookieVar)); + } elseif (isset($_COOKIE[self::$langCookieVar])) { + // Cookie 中设置了语言变量 + $langSet = strtolower($_COOKIE[self::$langCookieVar]); } elseif (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) { // 自动侦测浏览器语言 preg_match('/^([a-z\d\-]+)/i', $_SERVER['HTTP_ACCEPT_LANGUAGE'], $matches); - $langSet = strtolower($matches[1]); - Cookie::set(self::$langCookieVar, $langSet, self::$langCookieExpire); + $langSet = strtolower($matches[1]); + $acceptLangs = Config::get('header_accept_lang'); + + if (isset($acceptLangs[$langSet])) { + $langSet = $acceptLangs[$langSet]; + } elseif (isset(self::$acceptLanguage[$langSet])) { + $langSet = self::$acceptLanguage[$langSet]; + } } + + // 合法的语言 if (empty(self::$allowLangList) || in_array($langSet, self::$allowLangList)) { - // 合法的语言 self::$range = $langSet ?: self::$range; } - if ('zh-hans-cn' == self::$range) { - self::$range = 'zh-cn'; - } + return self::$range; } /** * 设置语言自动侦测的变量 - * @param string $var 变量名称 + * @access public + * @param string $var 变量名称 * @return void */ public static function setLangDetectVar($var) @@ -188,8 +231,9 @@ class Lang } /** - * 设置语言的cookie保存变量 - * @param string $var 变量名称 + * 设置语言的 cookie 保存变量 + * @access public + * @param string $var 变量名称 * @return void */ public static function setLangCookieVar($var) @@ -198,8 +242,9 @@ class Lang } /** - * 设置语言的cookie的过期时间 - * @param string $expire 过期时间 + * 设置语言的 cookie 的过期时间 + * @access public + * @param string $expire 过期时间 * @return void */ public static function setLangCookieExpire($expire) @@ -209,7 +254,8 @@ class Lang /** * 设置允许的语言列表 - * @param array $list 语言列表 + * @access public + * @param array $list 语言列表 * @return void */ public static function setAllowLangList($list) diff --git a/library/think/Loader.php b/library/think/Loader.php index cfa0a2178a29e1fc96e236a476ef44e7138c4788..d813a5d7bd1519eadf54de4150aa3030da4c9328 100644 --- a/library/think/Loader.php +++ b/library/think/Loader.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2016 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- @@ -15,26 +15,57 @@ use think\exception\ClassNotFoundException; class Loader { + /** + * @var array 实例数组 + */ protected static $instance = []; - // 类名映射 - protected static $map = []; - // 命名空间别名 + /** + * @var array 类名映射 + */ + protected static $classMap = []; + + /** + * @var array 命名空间别名 + */ protected static $namespaceAlias = []; - // PSR-4 + /** + * @var array PSR-4 命名空间前缀长度映射 + */ private static $prefixLengthsPsr4 = []; - private static $prefixDirsPsr4 = []; - private static $fallbackDirsPsr4 = []; - // PSR-0 - private static $prefixesPsr0 = []; + /** + * @var array PSR-4 的加载目录 + */ + private static $prefixDirsPsr4 = []; + + /** + * @var array PSR-4 加载失败的回退目录 + */ + private static $fallbackDirsPsr4 = []; + + /** + * @var array PSR-0 命名空间前缀映射 + */ + private static $prefixesPsr0 = []; + + /** + * @var array PSR-0 加载失败的回退目录 + */ private static $fallbackDirsPsr0 = []; - // 自动加载的文件 - private static $autoloadFiles = []; + /** + * @var array 需要加载的文件 + */ + private static $files = []; - // 自动加载 + /** + * 自动加载 + * @access public + * @param string $class 类名 + * @return bool + */ public static function autoload($class) { // 检测命名空间别名 @@ -49,33 +80,33 @@ class Loader } if ($file = self::findFile($class)) { - - // Win环境严格区分大小写 - if (IS_WIN && pathinfo($file, PATHINFO_FILENAME) != pathinfo(realpath($file), PATHINFO_FILENAME)) { - return false; + // 非 Win 环境不严格区分大小写 + if (!IS_WIN || pathinfo($file, PATHINFO_FILENAME) == pathinfo(realpath($file), PATHINFO_FILENAME)) { + __include_file($file); + return true; } - - __include_file($file); - return true; } + + return false; } /** * 查找文件 - * @param $class - * @return bool + * @access private + * @param string $class 类名 + * @return bool|string */ private static function findFile($class) { - if (!empty(self::$map[$class])) { - // 类库映射 - return self::$map[$class]; + // 类库映射 + if (!empty(self::$classMap[$class])) { + return self::$classMap[$class]; } // 查找 PSR-4 $logicalPathPsr4 = strtr($class, '\\', DS) . EXT; + $first = $class[0]; - $first = $class[0]; if (isset(self::$prefixLengthsPsr4[$first])) { foreach (self::$prefixLengthsPsr4[$first] as $prefix => $length) { if (0 === strpos($class, $prefix)) { @@ -97,7 +128,7 @@ class Loader // 查找 PSR-0 if (false !== $pos = strrpos($class, '\\')) { - // namespaced class name + // namespace class name $logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1) . strtr(substr($logicalPathPsr4, $pos + 1), '_', DS); } else { @@ -124,20 +155,33 @@ class Loader } } - return self::$map[$class] = false; + // 找不到则设置映射为 false 并返回 + return self::$classMap[$class] = false; } - // 注册classmap + /** + * 注册 classmap + * @access public + * @param string|array $class 类名 + * @param string $map 映射 + * @return void + */ public static function addClassMap($class, $map = '') { if (is_array($class)) { - self::$map = array_merge(self::$map, $class); + self::$classMap = array_merge(self::$classMap, $class); } else { - self::$map[$class] = $map; + self::$classMap[$class] = $map; } } - // 注册命名空间 + /** + * 注册命名空间 + * @access public + * @param string|array $namespace 命名空间 + * @param string $path 路径 + * @return void + */ public static function addNamespace($namespace, $path = '') { if (is_array($namespace)) { @@ -149,84 +193,77 @@ class Loader } } - // 添加Ps0空间 + /** + * 添加 PSR-0 命名空间 + * @access private + * @param array|string $prefix 空间前缀 + * @param array $paths 路径 + * @param bool $prepend 预先设置的优先级更高 + * @return void + */ private static function addPsr0($prefix, $paths, $prepend = false) { if (!$prefix) { - if ($prepend) { - self::$fallbackDirsPsr0 = array_merge( - (array) $paths, - self::$fallbackDirsPsr0 - ); + self::$fallbackDirsPsr0 = $prepend ? + array_merge((array) $paths, self::$fallbackDirsPsr0) : + array_merge(self::$fallbackDirsPsr0, (array) $paths); + } else { + $first = $prefix[0]; + + if (!isset(self::$prefixesPsr0[$first][$prefix])) { + self::$prefixesPsr0[$first][$prefix] = (array) $paths; } else { - self::$fallbackDirsPsr0 = array_merge( - self::$fallbackDirsPsr0, - (array) $paths - ); + self::$prefixesPsr0[$first][$prefix] = $prepend ? + array_merge((array) $paths, self::$prefixesPsr0[$first][$prefix]) : + array_merge(self::$prefixesPsr0[$first][$prefix], (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空间 + /** + * 添加 PSR-4 空间 + * @access private + * @param array|string $prefix 空间前缀 + * @param string $paths 路径 + * @param bool $prepend 预先设置的优先级更高 + * @return void + */ 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 - ); - } + self::$fallbackDirsPsr4 = $prepend ? + array_merge((array) $paths, 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."); + 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 { + self::$prefixDirsPsr4[$prefix] = $prepend ? + // Prepend directories for an already registered namespace. + array_merge((array) $paths, self::$prefixDirsPsr4[$prefix]) : // Append directories for an already registered namespace. - self::$prefixDirsPsr4[$prefix] = array_merge( - self::$prefixDirsPsr4[$prefix], - (array) $paths - ); + array_merge(self::$prefixDirsPsr4[$prefix], (array) $paths); } } - // 注册命名空间别名 + /** + * 注册命名空间别名 + * @access public + * @param array|string $namespace 命名空间 + * @param string $original 源文件 + * @return void + */ public static function addNamespaceAlias($namespace, $original = '') { if (is_array($namespace)) { @@ -236,32 +273,58 @@ class Loader } } - // 注册自动加载机制 - public static function register($autoload = '') + /** + * 注册自动加载机制 + * @access public + * @param callable $autoload 自动加载处理方法 + * @return void + */ + public static function register($autoload = null) { // 注册系统自动加载 spl_autoload_register($autoload ?: 'think\\Loader::autoload', true, true); + + // Composer 自动加载支持 + if (is_dir(VENDOR_PATH . 'composer')) { + if (PHP_VERSION_ID >= 50600 && is_file(VENDOR_PATH . 'composer' . DS . 'autoload_static.php')) { + require VENDOR_PATH . 'composer' . DS . 'autoload_static.php'; + + $declaredClass = get_declared_classes(); + $composerClass = array_pop($declaredClass); + + foreach (['prefixLengthsPsr4', 'prefixDirsPsr4', 'fallbackDirsPsr4', 'prefixesPsr0', 'fallbackDirsPsr0', 'classMap', 'files'] as $attr) { + if (property_exists($composerClass, $attr)) { + self::${$attr} = $composerClass::${$attr}; + } + } + } else { + self::registerComposerLoader(); + } + } + // 注册命名空间定义 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(); - } + self::loadComposerAutoloadFiles(); - // 自动加载extend目录 + // 自动加载 extend 目录 self::$fallbackDirsPsr4[] = rtrim(EXTEND_PATH, DS); } - // 注册composer自动加载 + /** + * 注册 composer 自动加载 + * @access private + * @return void + */ private static function registerComposerLoader() { if (is_file(VENDOR_PATH . 'composer/autoload_namespaces.php')) { @@ -286,28 +349,36 @@ class Loader } if (is_file(VENDOR_PATH . 'composer/autoload_files.php')) { - $includeFiles = require VENDOR_PATH . 'composer/autoload_files.php'; - foreach ($includeFiles as $fileIdentifier => $file) { - if (empty(self::$autoloadFiles[$fileIdentifier])) { - __require_file($file); - self::$autoloadFiles[$fileIdentifier] = true; - } + self::$files = require VENDOR_PATH . 'composer/autoload_files.php'; + } + } + + // 加载composer autofile文件 + public static function loadComposerAutoloadFiles() + { + foreach (self::$files as $fileIdentifier => $file) { + if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) { + __require_file($file); + + $GLOBALS['__composer_autoload_files'][$fileIdentifier] = true; } } } /** - * 导入所需的类库 同java的Import 本函数有缓存功能 - * @param string $class 类库命名空间字符串 - * @param string $baseUrl 起始路径 - * @param string $ext 导入的文件扩展名 - * @return boolean + * 导入所需的类库 同 Java 的 Import 本函数有缓存功能 + * @access public + * @param string $class 类库命名空间字符串 + * @param string $baseUrl 起始路径 + * @param string $ext 导入的文件扩展名 + * @return bool */ public static function import($class, $baseUrl = '', $ext = EXT) { static $_file = []; $key = $class . $baseUrl; $class = str_replace(['.', '#'], [DS, '.'], $class); + if (isset($_file[$key])) { return true; } @@ -319,7 +390,7 @@ class Loader // 注册的命名空间 $baseUrl = self::$prefixDirsPsr4[$name . '\\']; } elseif ('@' == $name) { - //加载当前模块应用类库 + // 加载当前模块应用类库 $baseUrl = App::$modulePath; } elseif (is_dir(EXTEND_PATH . $name)) { $baseUrl = EXTEND_PATH . $name . DS; @@ -330,11 +401,11 @@ class Loader } elseif (substr($baseUrl, -1) != DS) { $baseUrl .= DS; } - // 如果类存在 则导入类库文件 + + // 如果类存在则导入类库文件 if (is_array($baseUrl)) { foreach ($baseUrl as $path) { - $filename = $path . DS . $class . $ext; - if (is_file($filename)) { + if (is_file($filename = $path . DS . $class . $ext)) { break; } } @@ -342,119 +413,154 @@ class Loader $filename = $baseUrl . $class . $ext; } - if (!empty($filename) && is_file($filename)) { - // 开启调试模式Win环境严格区分大小写 - if (IS_WIN && pathinfo($filename, PATHINFO_FILENAME) != pathinfo(realpath($filename), PATHINFO_FILENAME)) { - return false; - } + if (!empty($filename) && + is_file($filename) && + (!IS_WIN || pathinfo($filename, PATHINFO_FILENAME) == pathinfo(realpath($filename), PATHINFO_FILENAME)) + ) { __include_file($filename); $_file[$key] = true; + return true; } + return false; } /** * 实例化(分层)模型 - * @param string $name Model名称 - * @param string $layer 业务层名称 - * @param bool $appendSuffix 是否添加类名后缀 - * @param string $common 公共模块名 - * @return Object + * @access public + * @param string $name Model名称 + * @param string $layer 业务层名称 + * @param bool $appendSuffix 是否添加类名后缀 + * @param string $common 公共模块名 + * @return object * @throws ClassNotFoundException */ public static function model($name = '', $layer = 'model', $appendSuffix = false, $common = 'common') { - if (isset(self::$instance[$name . $layer])) { - return self::$instance[$name . $layer]; - } - if (strpos($name, '/')) { - list($module, $name) = explode('/', $name, 2); - } else { - $module = Request::instance()->module(); + $uid = $name . $layer; + + if (isset(self::$instance[$uid])) { + return self::$instance[$uid]; } - $class = self::parseClass($module, $layer, $name, $appendSuffix); + + list($module, $class) = self::getModuleAndClass($name, $layer, $appendSuffix); + if (class_exists($class)) { $model = new $class(); } else { $class = str_replace('\\' . $module . '\\', '\\' . $common . '\\', $class); + if (class_exists($class)) { $model = new $class(); } else { throw new ClassNotFoundException('class not exists:' . $class, $class); } } - self::$instance[$name . $layer] = $model; - return $model; + + return self::$instance[$uid] = $model; } /** * 实例化(分层)控制器 格式:[模块名/]控制器名 - * @param string $name 资源地址 - * @param string $layer 控制层名称 - * @param bool $appendSuffix 是否添加类名后缀 - * @param string $empty 空控制器名称 - * @return Object|false + * @access public + * @param string $name 资源地址 + * @param string $layer 控制层名称 + * @param bool $appendSuffix 是否添加类名后缀 + * @param string $empty 空控制器名称 + * @return object * @throws ClassNotFoundException */ public static function controller($name, $layer = 'controller', $appendSuffix = false, $empty = '') { - if (strpos($name, '/')) { - list($module, $name) = explode('/', $name); - } else { - $module = Request::instance()->module(); - } - $class = self::parseClass($module, $layer, $name, $appendSuffix); + list($module, $class) = self::getModuleAndClass($name, $layer, $appendSuffix); + if (class_exists($class)) { return App::invokeClass($class); - } elseif ($empty && class_exists($emptyClass = self::parseClass($module, $layer, $empty, $appendSuffix))) { - return new $emptyClass(Request::instance()); } + + if ($empty) { + $emptyClass = self::parseClass($module, $layer, $empty, $appendSuffix); + + if (class_exists($emptyClass)) { + return new $emptyClass(Request::instance()); + } + } + + throw new ClassNotFoundException('class not exists:' . $class, $class); } /** * 实例化验证类 格式:[模块名/]验证器名 - * @param string $name 资源地址 - * @param string $layer 验证层名称 - * @param bool $appendSuffix 是否添加类名后缀 - * @param string $common 公共模块名 - * @return Object|false + * @access public + * @param string $name 资源地址 + * @param string $layer 验证层名称 + * @param bool $appendSuffix 是否添加类名后缀 + * @param string $common 公共模块名 + * @return object|false * @throws ClassNotFoundException */ public static function validate($name = '', $layer = 'validate', $appendSuffix = false, $common = 'common') { $name = $name ?: Config::get('default_validate'); + if (empty($name)) { return new Validate; } - if (isset(self::$instance[$name . $layer])) { - return self::$instance[$name . $layer]; + $uid = $name . $layer; + if (isset(self::$instance[$uid])) { + return self::$instance[$uid]; } - if (strpos($name, '/')) { - list($module, $name) = explode('/', $name); - } else { - $module = Request::instance()->module(); - } - $class = self::parseClass($module, $layer, $name, $appendSuffix); + + list($module, $class) = self::getModuleAndClass($name, $layer, $appendSuffix); + if (class_exists($class)) { $validate = new $class; } else { $class = str_replace('\\' . $module . '\\', '\\' . $common . '\\', $class); + if (class_exists($class)) { $validate = new $class; } else { throw new ClassNotFoundException('class not exists:' . $class, $class); } } - self::$instance[$name . $layer] = $validate; - return $validate; + + return self::$instance[$uid] = $validate; + } + + /** + * 解析模块和类名 + * @access protected + * @param string $name 资源地址 + * @param string $layer 验证层名称 + * @param bool $appendSuffix 是否添加类名后缀 + * @return array + */ + protected static function getModuleAndClass($name, $layer, $appendSuffix) + { + if (false !== strpos($name, '\\')) { + $module = Request::instance()->module(); + $class = $name; + } else { + if (strpos($name, '/')) { + list($module, $name) = explode('/', $name, 2); + } else { + $module = Request::instance()->module(); + } + + $class = self::parseClass($module, $layer, $name, $appendSuffix); + } + + return [$module, $class]; } /** * 数据库初始化 并取得数据库类实例 - * @param mixed $config 数据库配置 - * @param bool|string $name 连接标识 true 强制重新连接 + * @access public + * @param mixed $config 数据库配置 + * @param bool|string $name 连接标识 true 强制重新连接 * @return \think\db\Connection */ public static function db($config = [], $name = false) @@ -464,10 +570,11 @@ class Loader /** * 远程调用模块的操作方法 参数格式 [模块/控制器/]操作 - * @param string $url 调用地址 - * @param string|array $vars 调用参数 支持字符串和数组 - * @param string $layer 要调用的控制层名称 - * @param bool $appendSuffix 是否添加类名后缀 + * @access public + * @param string $url 调用地址 + * @param string|array $vars 调用参数 支持字符串和数组 + * @param string $layer 要调用的控制层名称 + * @param bool $appendSuffix 是否添加类名后缀 * @return mixed */ public static function action($url, $vars = [], $layer = 'controller', $appendSuffix = false) @@ -476,6 +583,7 @@ class Loader $action = $info['basename']; $module = '.' != $info['dirname'] ? $info['dirname'] : Request::instance()->controller(); $class = self::controller($module, $layer, $appendSuffix); + if ($class) { if (is_scalar($vars)) { if (strpos($vars, '=')) { @@ -484,47 +592,60 @@ class Loader $vars = [$vars]; } } + return App::invokeMethod([$class, $action . Config::get('action_suffix')], $vars); } + + return false; } /** * 字符串命名风格转换 - * type 0 将Java风格转换为C的风格 1 将C风格转换为Java的风格 - * @param string $name 字符串 - * @param integer $type 转换类型 + * type 0 将 Java 风格转换为 C 的风格 1 将 C 风格转换为 Java 的风格 + * @access public + * @param string $name 字符串 + * @param integer $type 转换类型 + * @param bool $ucfirst 首字母是否大写(驼峰规则) * @return string */ - public static function parseName($name, $type = 0) + public static function parseName($name, $type = 0, $ucfirst = true) { if ($type) { - return ucfirst(preg_replace_callback('/_([a-zA-Z])/', function ($match) { + $name = preg_replace_callback('/_([a-zA-Z])/', function ($match) { return strtoupper($match[1]); - }, $name)); - } else { - return strtolower(trim(preg_replace("/[A-Z]/", "_\\0", $name), "_")); + }, $name); + + return $ucfirst ? ucfirst($name) : lcfirst($name); } + + return strtolower(trim(preg_replace("/[A-Z]/", "_\\0", $name), "_")); } /** * 解析应用类的类名 - * @param string $module 模块名 - * @param string $layer 层名 controller model ... - * @param string $name 类名 - * @param bool $appendSuffix + * @access public + * @param string $module 模块名 + * @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) . (App::$suffix || $appendSuffix ? ucfirst($layer) : ''); + + $array = explode('\\', str_replace(['/', '.'], '\\', $name)); + $class = self::parseName(array_pop($array), 1); + $class = $class . (App::$suffix || $appendSuffix ? ucfirst($layer) : ''); $path = $array ? implode('\\', $array) . '\\' : ''; - return App::$namespace . '\\' . ($module ? $module . '\\' : '') . $layer . '\\' . $path . $class; + + return App::$namespace . '\\' . + ($module ? $module . '\\' : '') . + $layer . '\\' . $path . $class; } /** * 初始化类的实例 + * @access public * @return void */ public static function clearInstance() @@ -533,10 +654,11 @@ class Loader } } +// 作用范围隔离 + /** - * 作用范围隔离 - * - * @param $file + * include + * @param string $file 文件路径 * @return mixed */ function __include_file($file) @@ -544,6 +666,11 @@ function __include_file($file) return include $file; } +/** + * require + * @param string $file 文件路径 + * @return mixed + */ function __require_file($file) { return require $file; diff --git a/library/think/Log.php b/library/think/Log.php index 5fd1631a55f2b04eb6c853071cd303a003fc9cc7..c064306c0004f3b4b70f3447378f0b6cba47704d 100644 --- a/library/think/Log.php +++ b/library/think/Log.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2016 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- @@ -17,12 +17,12 @@ 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 + * @method void log($msg) static 记录一般日志 + * @method void error($msg) static 记录错误日志 + * @method void info($msg) static 记录一般信息日志 + * @method void sql($msg) static 记录 SQL 查询日志 + * @method void notice($msg) static 记录提示日志 + * @method void alert($msg) static 记录报警日志 */ class Log { @@ -32,42 +32,62 @@ class Log const SQL = 'sql'; const NOTICE = 'notice'; const ALERT = 'alert'; + const DEBUG = 'debug'; - // 日志信息 + /** + * @var array 日志信息 + */ protected static $log = []; - // 配置参数 + + /** + * @var array 配置参数 + */ protected static $config = []; - // 日志类型 - protected static $type = ['log', 'error', 'info', 'sql', 'notice', 'alert']; - // 日志写入驱动 + + /** + * @var array 日志类型 + */ + protected static $type = ['log', 'error', 'info', 'sql', 'notice', 'alert', 'debug']; + + /** + * @var log\driver\File|log\driver\Test|log\driver\Socket 日志写入驱动 + */ protected static $driver; - // 当前日志授权key + /** + * @var string 当前日志授权 key + */ protected static $key; /** * 日志初始化 - * @param array $config + * @access public + * @param array $config 配置参数 + * @return void */ public static function init($config = []) { - $type = isset($config['type']) ? $config['type'] : 'File'; - $class = false !== strpos($type, '\\') ? $type : '\\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']); + 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, 'info'); } /** * 获取日志信息 - * @param string $type 信息类型 - * @return array + * @access public + * @param string $type 信息类型 + * @return array|string */ public static function getLog($type = '') { @@ -76,17 +96,22 @@ class Log /** * 记录调试信息 - * @param mixed $msg 调试信息 - * @param string $type 信息类型 + * @access public + * @param mixed $msg 调试信息 + * @param string $type 信息类型 * @return void */ public static function record($msg, $type = 'log') { self::$log[$type][] = $msg; + + // 命令行下面日志写入改进 + IS_CLI && self::save(); } /** * 清空日志信息 + * @access public * @return void */ public static function clear() @@ -95,8 +120,9 @@ class Log } /** - * 当前日志记录的授权key - * @param string $key 授权key + * 设置当前日志记录的授权 key + * @access public + * @param string $key 授权 key * @return void */ public static function key($key) @@ -106,94 +132,105 @@ class Log /** * 检查日志写入权限 - * @param array $config 当前日志配置参数 + * @access public + * @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 !self::$key || empty($config['allow_key']) || in_array(self::$key, $config['allow_key']); } /** * 保存调试信息 + * @access public * @return bool */ public static function save() { - if (!empty(self::$log)) { - if (is_null(self::$driver)) { - self::init(Config::get('log')); - } + // 没有需要保存的记录则直接返回 + if (empty(self::$log)) { + return true; + } - if (!self::check(self::$config)) { - // 检测日志写入权限 - return false; - } + is_null(self::$driver) && self::init(Config::get('log')); - 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]; - } - } - } + // 检测日志写入权限 + if (!self::check(self::$config)) { + return false; + } - $result = self::$driver->save($log); - if ($result) { - self::$log = []; + if (empty(self::$config['level'])) { + // 获取全部日志 + $log = self::$log; + if (!App::$debug && isset($log['debug'])) { + unset($log['debug']); } + } else { + // 记录允许级别 + $log = []; + foreach (self::$config['level'] as $level) { + if (isset(self::$log[$level])) { + $log[$level] = self::$log[$level]; + } + } + } - return $result; + if ($result = self::$driver->save($log, true)) { + self::$log = []; } - return true; + + Hook::listen('log_write_done', $log); + + return $result; } /** * 实时写入日志信息 并支持行为 - * @param mixed $msg 调试信息 - * @param string $type 信息类型 - * @param bool $force 是否强制写入 + * @access public + * @param mixed $msg 调试信息 + * @param string $type 信息类型 + * @param bool $force 是否强制写入 * @return bool */ public static function write($msg, $type = 'log', $force = false) { - // 封装日志信息 - if (true === $force || empty(self::$config['level'])) { - $log[$type][] = $msg; - } elseif (in_array($type, self::$config['level'])) { - $log[$type][] = $msg; - } else { + $log = self::$log; + + // 如果不是强制写入,而且信息类型不在可记录的类别中则直接返回 false 不做记录 + if (true !== $force && !empty(self::$config['level']) && !in_array($type, self::$config['level'])) { return false; } - // 监听log_write + // 封装日志信息 + $log[$type][] = $msg; + + // 监听 log_write Hook::listen('log_write', $log); - if (is_null(self::$driver)) { - self::init(Config::get('log')); - } + + is_null(self::$driver) && self::init(Config::get('log')); + // 写入日志 - return self::$driver->save($log); + if ($result = self::$driver->save($log, false)) { + self::$log = []; + } + + return $result; } /** - * 静态调用 - * @param $method - * @param $args - * @return mixed + * 静态方法调用 + * @access public + * @param string $method 调用方法 + * @param mixed $args 参数 + * @return void */ public static function __callStatic($method, $args) { if (in_array($method, self::$type)) { array_push($args, $method); - return call_user_func_array('\\think\\Log::record', $args); + + call_user_func_array('\\think\\Log::record', $args); } } diff --git a/library/think/Model.php b/library/think/Model.php index f062e3c5c7089643969088f9665ef6591bdecddc..2dc27b48ab0d42a4755d3473ab2a696f641c2efc 100644 --- a/library/think/Model.php +++ b/library/think/Model.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2016 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- @@ -11,39 +11,34 @@ namespace think; +use BadMethodCallException; use InvalidArgumentException; -use think\Cache; -use think\Config; -use think\Db; use think\db\Query; -use think\Exception; -use think\Exception\ValidateException; -use think\Loader; +use think\exception\ValidateException; +use think\model\Collection as ModelCollection; use think\model\Relation; -use think\paginator\Collection as PaginatorCollection; +use think\model\relation\BelongsTo; +use think\model\relation\BelongsToMany; +use think\model\relation\HasMany; +use think\model\relation\HasManyThrough; +use think\model\relation\HasOne; +use think\model\relation\MorphMany; +use think\model\relation\MorphOne; +use think\model\relation\MorphTo; /** * Class Model * @package think - * @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) 查找单条记录 如果不存在则抛出异常 - * + * @mixin Query */ abstract class Model implements \JsonSerializable, \ArrayAccess { - // 数据库对象池 + // 数据库查询对象池 protected static $links = []; // 数据库配置 protected $connection = []; + // 父关联模型对象 + protected $parent; // 数据库查询对象 protected $query; // 当前模型名称 @@ -62,6 +57,10 @@ abstract class Model implements \JsonSerializable, \ArrayAccess protected $pk; // 数据表字段信息 留空则自动获取 protected $field = []; + // 数据排除字段 + protected $except = []; + // 数据废弃字段 + protected $disuse = []; // 只读字段 protected $readonly = []; // 显示属性 @@ -72,8 +71,10 @@ abstract class Model implements \JsonSerializable, \ArrayAccess protected $append = []; // 数据信息 protected $data = []; - // 记录改变字段 - protected $change = []; + // 原始数据 + protected $origin = []; + // 关联模型 + protected $relation = []; // 保存自动完成列表 protected $auto = []; @@ -88,19 +89,27 @@ abstract class Model implements \JsonSerializable, \ArrayAccess // 更新时间字段 protected $updateTime = 'update_time'; // 时间字段取出后的默认时间格式 - protected $dateFormat = 'Y-m-d H:i:s'; + protected $dateFormat; // 字段类型或者格式转换 protected $type = []; // 是否为更新数据 protected $isUpdate = false; + // 是否使用Replace + protected $replace = false; + // 是否强制更新所有数据 + protected $force = false; // 更新条件 protected $updateWhere; - // 当前执行的关联对象 - protected $relation; // 验证失败是否抛出异常 protected $failException = false; // 全局查询范围 protected $useGlobalScope = true; + // 是否采用批量验证 + protected $batchValidate = false; + // 查询数据集对象 + protected $resultSetType; + // 关联自动写入 + protected $relationWrite; /** * 初始化过的模型. @@ -110,7 +119,13 @@ abstract class Model implements \JsonSerializable, \ArrayAccess protected static $initialized = []; /** - * 架构函数 + * 是否从主库读取(主从分布式有效) + * @var array + */ + protected static $readMaster; + + /** + * 构造方法 * @access public * @param array|object $data 数据 */ @@ -122,8 +137,20 @@ abstract class Model implements \JsonSerializable, \ArrayAccess $this->data = $data; } + if ($this->disuse) { + // 废弃字段 + foreach ((array) $this->disuse as $key) { + if (array_key_exists($key, $this->data)) { + unset($this->data[$key]); + } + } + } + + // 记录原始数据 + $this->origin = $this->data; + // 当前类名 - $this->class = get_class($this); + $this->class = get_called_class(); if (empty($this->name)) { // 当前模型名 @@ -137,65 +164,125 @@ abstract class Model implements \JsonSerializable, \ArrayAccess if (is_null($this->autoWriteTimestamp)) { // 自动写入时间戳 - $this->autoWriteTimestamp = $this->db()->getConfig('auto_timestamp'); + $this->autoWriteTimestamp = $this->getQuery()->getConfig('auto_timestamp'); + } + + if (is_null($this->dateFormat)) { + // 设置时间戳格式 + $this->dateFormat = $this->getQuery()->getConfig('datetime_format'); } + if (is_null($this->resultSetType)) { + $this->resultSetType = $this->getQuery()->getConfig('resultset_type'); + } // 执行初始化操作 $this->initialize(); } /** - * 获取当前模型的数据库查询对象 + * 是否从主库读取数据(主从分布有效) * @access public - * @param bool $baseQuery 是否调用全局查询范围 - * @return Query + * @param bool $all 是否所有模型生效 + * @return $this */ - public function db($baseQuery = true) + public function readMaster($all = false) { - $model = $this->class; - if (!isset(self::$links[$model])) { - // 设置当前模型 确保查询返回模型对象 - $query = Db::connect($this->connection)->model($model, $this->query); + $model = $all ? '*' : $this->class; + + static::$readMaster[$model] = true; + return $this; + } - // 设置当前数据表和模型名 - if (!empty($this->table)) { - $query->setTable($this->table); + /** + * 创建模型的查询对象 + * @access protected + * @return Query + */ + protected function buildQuery() + { + // 合并数据库配置 + if (!empty($this->connection)) { + if (is_array($this->connection)) { + $connection = array_merge(Config::get('database'), $this->connection); } else { - $query->name($this->name); + $connection = $this->connection; } + } else { + $connection = []; + } - if (!empty($this->pk)) { - $query->pk($this->pk); - } + $con = Db::connect($connection); + // 设置当前模型 确保查询返回模型对象 + $queryClass = $this->query ?: $con->getConfig('query'); + $query = new $queryClass($con, $this); - self::$links[$model] = $query; + if (isset(static::$readMaster['*']) || isset(static::$readMaster[$this->class])) { + $query->master(true); } - // 全局作用域 - if ($baseQuery && method_exists($this, 'base')) { - call_user_func_array([$this, 'base'], [ & self::$links[$model]]); + + // 设置当前数据表和模型名 + if (!empty($this->table)) { + $query->setTable($this->table); + } else { + $query->name($this->name); } - // 返回当前模型的数据库查询对象 - return self::$links[$model]; + + if (!empty($this->pk)) { + $query->pk($this->pk); + } + + return $query; } /** - * 获取关联模型实例 - * @access protected - * @param string|array $relation 关联查询 - * @return Relation|Query + * 创建新的模型实例 + * @access public + * @param array|object $data 数据 + * @param bool $isUpdate 是否为更新 + * @param mixed $where 更新条件 + * @return Model + */ + public function newInstance($data = [], $isUpdate = false, $where = null) + { + return (new static($data))->isUpdate($isUpdate, $where); + } + + /** + * 获取当前模型的查询对象 + * @access public + * @param bool $buildNewQuery 创建新的查询对象 + * @return Query */ - protected function relation($relation = null) + public function getQuery($buildNewQuery = false) { - if (!is_null($relation)) { - // 执行关联查询 - return $this->db()->relation($relation); + if ($buildNewQuery) { + return $this->buildQuery(); + } elseif (!isset(self::$links[$this->class])) { + // 创建模型查询对象 + self::$links[$this->class] = $this->buildQuery(); } - // 获取关联对象实例 - if (is_null($this->relation)) { - $this->relation = new Relation($this); + return self::$links[$this->class]; + } + + /** + * 获取当前模型的数据库查询对象 + * @access public + * @param bool $useBaseQuery 是否调用全局查询范围 + * @param bool $buildNewQuery 创建新的查询对象 + * @return Query + */ + public function db($useBaseQuery = true, $buildNewQuery = true) + { + $query = $this->getQuery($buildNewQuery); + + // 全局作用域 + if ($useBaseQuery && method_exists($this, 'base')) { + call_user_func_array([$this, 'base'], [ & $query]); } - return $this->relation; + + // 返回当前模型的数据库查询对象 + return $query; } /** @@ -218,12 +305,35 @@ abstract class Model implements \JsonSerializable, \ArrayAccess * @return void */ protected static function init() - {} + { + } + + /** + * 设置父关联对象 + * @access public + * @param Model $model 模型对象 + * @return $this + */ + public function setParent($model) + { + $this->parent = $model; + return $this; + } + + /** + * 获取父关联对象 + * @access public + * @return Model + */ + public function getParent() + { + return $this->parent; + } /** * 设置数据对象值 * @access public - * @param mixed $data 数据或者属性名 + * @param mixed $data 数据或者属性名 * @param mixed $value 值 * @return $this */ @@ -262,17 +372,43 @@ abstract class Model implements \JsonSerializable, \ArrayAccess return $this->data; } elseif (array_key_exists($name, $this->data)) { return $this->data[$name]; + } elseif (array_key_exists($name, $this->relation)) { + return $this->relation[$name]; } else { throw new InvalidArgumentException('property not exists:' . $this->class . '->' . $name); } } + /** + * 是否需要自动写入时间字段 + * @access public + * @param bool $auto + * @return $this + */ + public function isAutoWriteTimestamp($auto) + { + $this->autoWriteTimestamp = $auto; + return $this; + } + + /** + * 更新是否强制写入数据 而不做比较 + * @access public + * @param bool $force + * @return $this + */ + public function force($force = true) + { + $this->force = $force; + return $this; + } + /** * 修改器 设置数据对象值 * @access public - * @param string $name 属性名 - * @param mixed $value 属性值 - * @param array $data 数据 + * @param string $name 属性名 + * @param mixed $value 属性值 + * @param array $data 数据 * @return $this */ public function setAttr($name, $value, $data = []) @@ -284,26 +420,52 @@ abstract class Model implements \JsonSerializable, \ArrayAccess // 检测修改器 $method = 'set' . Loader::parseName($name, 1) . 'Attr'; if (method_exists($this, $method)) { - $value = $this->$method($value, array_merge($data, $this->data)); + $value = $this->$method($value, array_merge($this->data, $data), $this->relation); } 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 string $name 关联方法名 + * @return mixed + */ + public function getRelation($name = null) + { + if (is_null($name)) { + return $this->relation; + } elseif (array_key_exists($name, $this->relation)) { + return $this->relation[$name]; + } else { + return; + } + } + + /** + * 设置关联数据对象值 + * @access public + * @param string $name 属性名 + * @param mixed $value 属性值 + * @return $this + */ + public function setRelation($name, $value) + { + $this->relation[$name] = $value; + return $this; + } + /** * 自动写入时间戳 * @access public - * @param string $name 时间戳字段 + * @param string $name 时间戳字段 * @return mixed */ protected function autoWriteTimestamp($name) @@ -317,30 +479,58 @@ abstract class Model implements \JsonSerializable, \ArrayAccess case 'datetime': case 'date': $format = !empty($param) ? $param : $this->dateFormat; - $value = date($format, $_SERVER['REQUEST_TIME']); + $value = $this->formatDateTime(time(), $format); break; case 'timestamp': - case 'int': - $value = $_SERVER['REQUEST_TIME']; + case 'integer': + default: + $value = time(); break; } - } elseif (is_string($this->autoWriteTimestamp) && in_array(strtolower($this->autoWriteTimestamp), ['datetime', 'date', 'timestamp'])) { - $value = date($this->dateFormat, $_SERVER['REQUEST_TIME']); + } elseif (is_string($this->autoWriteTimestamp) && in_array(strtolower($this->autoWriteTimestamp), [ + 'datetime', + 'date', + 'timestamp', + ]) + ) { + $value = $this->formatDateTime(time(), $this->dateFormat); } else { - $value = $_SERVER['REQUEST_TIME']; + $value = $this->formatDateTime(time(), $this->dateFormat, true); } return $value; } + /** + * 时间日期字段格式化处理 + * @access public + * @param mixed $time 时间日期表达式 + * @param mixed $format 日期格式 + * @param bool $timestamp 是否进行时间戳转换 + * @return mixed + */ + protected function formatDateTime($time, $format, $timestamp = false) + { + if (false !== strpos($format, '\\')) { + $time = new $format($time); + } elseif (!$timestamp && false !== $format) { + $time = date($format, $time); + } + return $time; + } + /** * 数据写入 类型转换 * @access public - * @param mixed $value 值 - * @param string|array $type 要转换的类型 + * @param mixed $value 值 + * @param string|array $type 要转换的类型 * @return mixed */ protected function writeTransform($value, $type) { + if (is_null($value)) { + return; + } + if (is_array($type)) { list($type, $param) = $type; } elseif (strpos($type, ':')) { @@ -354,7 +544,7 @@ abstract class Model implements \JsonSerializable, \ArrayAccess if (empty($param)) { $value = (float) $value; } else { - $value = (float) number_format($value, $param); + $value = (float) number_format($value, $param, '.', ''); } break; case 'boolean': @@ -367,7 +557,8 @@ abstract class Model implements \JsonSerializable, \ArrayAccess break; case 'datetime': $format = !empty($param) ? $param : $this->dateFormat; - $value = date($format, is_numeric($value) ? $value : strtotime($value)); + $value = is_numeric($value) ? $value : strtotime($value); + $value = $this->formatDateTime($value, $format); break; case 'object': if (is_object($value)) { @@ -383,6 +574,7 @@ abstract class Model implements \JsonSerializable, \ArrayAccess case 'serialize': $value = serialize($value); break; + } return $value; } @@ -407,17 +599,29 @@ abstract class Model implements \JsonSerializable, \ArrayAccess // 检测属性获取器 $method = 'get' . Loader::parseName($name, 1) . 'Attr'; if (method_exists($this, $method)) { - $value = $this->$method($value, $this->data); + $value = $this->$method($value, $this->data, $this->relation); } elseif (isset($this->type[$name])) { // 类型转换 $value = $this->readTransform($value, $this->type[$name]); + } elseif (in_array($name, [$this->createTime, $this->updateTime])) { + if (is_string($this->autoWriteTimestamp) && in_array(strtolower($this->autoWriteTimestamp), [ + 'datetime', + 'date', + 'timestamp', + ]) + ) { + $value = $this->formatDateTime(strtotime($value), $this->dateFormat); + } else { + $value = $this->formatDateTime($value, $this->dateFormat); + } } elseif ($notFound) { - $method = Loader::parseName($name, 1); - if (method_exists($this, $method) && !method_exists('\think\Model', $method)) { + $relation = Loader::parseName($name, 1, false); + if (method_exists($this, $relation)) { + $modelRelation = $this->$relation(); // 不存在该字段 获取关联数据 - $value = $this->relation()->getRelation($method); + $value = $this->getRelationData($modelRelation); // 保存关联对象值 - $this->data[$name] = $value; + $this->relation[$name] = $value; } else { throw new InvalidArgumentException('property not exists:' . $this->class . '->' . $name); } @@ -425,15 +629,41 @@ abstract class Model implements \JsonSerializable, \ArrayAccess return $value; } + /** + * 获取关联模型数据 + * @access public + * @param Relation $modelRelation 模型关联对象 + * @return mixed + * @throws BadMethodCallException + */ + protected function getRelationData(Relation $modelRelation) + { + if ($this->parent && !$modelRelation->isSelfRelation() && get_class($modelRelation->getModel()) == get_class($this->parent)) { + $value = $this->parent; + } else { + // 首先获取关联数据 + if (method_exists($modelRelation, 'getRelation')) { + $value = $modelRelation->getRelation(); + } else { + throw new BadMethodCallException('method not exists:' . get_class($modelRelation) . '-> getRelation'); + } + } + return $value; + } + /** * 数据读取 类型转换 * @access public - * @param mixed $value 值 - * @param string|array $type 要转换的类型 + * @param mixed $value 值 + * @param string|array $type 要转换的类型 * @return mixed */ protected function readTransform($value, $type) { + if (is_null($value)) { + return; + } + if (is_array($type)) { list($type, $param) = $type; } elseif (strpos($type, ':')) { @@ -447,7 +677,7 @@ abstract class Model implements \JsonSerializable, \ArrayAccess if (empty($param)) { $value = (float) $value; } else { - $value = (float) number_format($value, $param); + $value = (float) number_format($value, $param, '.', ''); } break; case 'boolean': @@ -456,27 +686,36 @@ abstract class Model implements \JsonSerializable, \ArrayAccess case 'timestamp': if (!is_null($value)) { $format = !empty($param) ? $param : $this->dateFormat; - $value = date($format, $value); + $value = $this->formatDateTime($value, $format); } break; case 'datetime': if (!is_null($value)) { $format = !empty($param) ? $param : $this->dateFormat; - $value = date($format, strtotime($value)); + $value = $this->formatDateTime(strtotime($value), $format); } break; case 'json': $value = json_decode($value, true); break; case 'array': - $value = is_null($value) ? [] : json_decode($value, true); + $value = empty($value) ? [] : json_decode($value, true); break; case 'object': $value = empty($value) ? new \stdClass() : json_decode($value); break; case 'serialize': - $value = unserialize($value); + try { + $value = unserialize($value); + } catch (\Exception $e) { + $value = null; + } break; + default: + if (false !== strpos($type, '\\')) { + // 对象类型 + $value = new $type($value); + } } return $value; } @@ -484,38 +723,128 @@ abstract class Model implements \JsonSerializable, \ArrayAccess /** * 设置需要追加的输出属性 * @access public - * @param array $append 属性列表 + * @param array $append 属性列表 + * @param bool $override 是否覆盖 * @return $this */ - public function append($append = []) + public function append($append = [], $override = false) { - $this->append = $append; + $this->append = $override ? $append : array_merge($this->append, $append); + return $this; + } + + /** + * 设置附加关联对象的属性 + * @access public + * @param string $relation 关联方法 + * @param string|array $append 追加属性名 + * @return $this + * @throws Exception + */ + public function appendRelationAttr($relation, $append) + { + if (is_string($append)) { + $append = explode(',', $append); + } + + $relation = Loader::parseName($relation, 1, false); + + // 获取关联数据 + if (isset($this->relation[$relation])) { + $model = $this->relation[$relation]; + } else { + $model = $this->getRelationData($this->$relation()); + } + + if ($model instanceof Model) { + foreach ($append as $key => $attr) { + $key = is_numeric($key) ? $attr : $key; + if (isset($this->data[$key])) { + throw new Exception('bind attr has exists:' . $key); + } else { + $this->data[$key] = $model->getAttr($attr); + } + } + } return $this; } /** * 设置需要隐藏的输出属性 * @access public - * @param array $hidden 属性列表 + * @param array $hidden 属性列表 + * @param bool $override 是否覆盖 * @return $this */ - public function hidden($hidden = []) + public function hidden($hidden = [], $override = false) { - $this->hidden = $hidden; + $this->hidden = $override ? $hidden : array_merge($this->hidden, $hidden); return $this; } /** * 设置需要输出的属性 + * @access public * @param array $visible + * @param bool $override 是否覆盖 * @return $this */ - public function visible($visible = []) + public function visible($visible = [], $override = false) { - $this->visible = $visible; + $this->visible = $override ? $visible : array_merge($this->visible, $visible); return $this; } + /** + * 解析隐藏及显示属性 + * @access protected + * @param array $attrs 属性 + * @param array $result 结果集 + * @param bool $visible + * @return array + */ + protected function parseAttr($attrs, &$result, $visible = true) + { + $array = []; + foreach ($attrs as $key => $val) { + if (is_array($val)) { + if ($visible) { + $array[] = $key; + } + $result[$key] = $val; + } elseif (strpos($val, '.')) { + list($key, $name) = explode('.', $val); + if ($visible) { + $array[] = $key; + } + $result[$key][] = $name; + } else { + $array[] = $val; + } + } + return $array; + } + + /** + * 转换子模型对象 + * @access protected + * @param Model|ModelCollection $model + * @param $visible + * @param $hidden + * @param $key + * @return array + */ + protected function subToArray($model, $visible, $hidden, $key) + { + // 关联模型对象 + if (isset($visible[$key])) { + $model->visible($visible[$key]); + } elseif (isset($hidden[$key])) { + $model->hidden($hidden[$key]); + } + return $model->toArray(); + } + /** * 转换当前模型对象为数组 * @access public @@ -523,26 +852,30 @@ abstract class Model implements \JsonSerializable, \ArrayAccess */ public function toArray() { - $item = []; + $item = []; + $visible = []; + $hidden = []; - //过滤属性 + $data = array_merge($this->data, $this->relation); + + // 过滤属性 if (!empty($this->visible)) { - $data = array_intersect_key($this->data, array_flip($this->visible)); + $array = $this->parseAttr($this->visible, $visible); + $data = array_intersect_key($data, array_flip($array)); } elseif (!empty($this->hidden)) { - $data = array_diff_key($this->data, array_flip($this->hidden)); - } else { - $data = $this->data; + $array = $this->parseAttr($this->hidden, $hidden, false); + $data = array_diff_key($data, array_flip($array)); } foreach ($data as $key => $val) { - if ($val instanceof Model || $val instanceof Collection) { + if ($val instanceof Model || $val instanceof ModelCollection) { // 关联模型对象 - $item[$key] = $val->toArray(); + $item[$key] = $this->subToArray($val, $visible, $hidden, $key); } elseif (is_array($val) && reset($val) instanceof Model) { // 关联模型数据集 $arr = []; foreach ($val as $k => $value) { - $arr[$k] = $value->toArray(); + $arr[$k] = $this->subToArray($value, $visible, $hidden, $key); } $item[$key] = $arr; } else { @@ -552,8 +885,41 @@ abstract class Model implements \JsonSerializable, \ArrayAccess } // 追加属性(必须定义获取器) if (!empty($this->append)) { - foreach ($this->append as $name) { - $item[$name] = $this->getAttr($name); + foreach ($this->append as $key => $name) { + if (is_array($name)) { + // 追加关联对象属性 + $relation = $this->getAttr($key); + $item[$key] = $relation->append($name)->toArray(); + } elseif (strpos($name, '.')) { + list($key, $attr) = explode('.', $name); + // 追加关联对象属性 + $relation = $this->getAttr($key); + $item[$key] = $relation->append([$attr])->toArray(); + } else { + $relation = Loader::parseName($name, 1, false); + if (method_exists($this, $relation)) { + $modelRelation = $this->$relation(); + $value = $this->getRelationData($modelRelation); + + if (method_exists($modelRelation, 'getBindAttr')) { + $bindAttr = $modelRelation->getBindAttr(); + if ($bindAttr) { + foreach ($bindAttr as $key => $attr) { + $key = is_numeric($key) ? $attr : $key; + if (isset($this->data[$key])) { + throw new Exception('bind attr has exists:' . $key); + } else { + $item[$key] = $value ? $value->getAttr($attr) : null; + } + } + continue; + } + } + $item[$name] = $value; + } else { + $item[$name] = $this->getAttr($name); + } + } } } return !empty($item) ? $item : []; @@ -562,7 +928,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) @@ -570,6 +936,51 @@ abstract class Model implements \JsonSerializable, \ArrayAccess return json_encode($this->toArray(), $options); } + /** + * 移除当前模型的关联属性 + * @access public + * @return $this + */ + public function removeRelation() + { + $this->relation = []; + return $this; + } + + /** + * 转换当前模型数据集为数据集对象 + * @access public + * @param array|\think\Collection $collection 数据集 + * @return \think\Collection + */ + public function toCollection($collection) + { + if ($this->resultSetType) { + if ('collection' == $this->resultSetType) { + $collection = new ModelCollection($collection); + } elseif (false !== strpos($this->resultSetType, '\\')) { + $class = $this->resultSetType; + $collection = new $class($collection); + } + } + return $collection; + } + + /** + * 关联数据一起更新 + * @access public + * @param mixed $relation 关联 + * @return $this + */ + public function together($relation) + { + if (is_string($relation)) { + $relation = explode(',', $relation); + } + $this->relationWrite = $relation; + return $this; + } + /** * 获取模型对象的主键 * @access public @@ -579,10 +990,10 @@ abstract class Model implements \JsonSerializable, \ArrayAccess public function getPk($name = '') { if (!empty($name)) { - $table = $this->db()->getTable($name); - return $this->db()->getPk($table); + $table = $this->getQuery()->getTable($name); + return $this->getQuery()->getPk($table); } elseif (empty($this->pk)) { - $this->pk = $this->db()->getPk(); + $this->pk = $this->getQuery()->getPk(); } return $this->pk; } @@ -604,35 +1015,71 @@ abstract class Model implements \JsonSerializable, \ArrayAccess return false; } + /** + * 新增数据是否使用Replace + * @access public + * @param bool $replace + * @return $this + */ + public function replace($replace = true) + { + $this->replace = $replace; + return $this; + } + /** * 保存当前数据对象 * @access public - * @param array $data 数据 - * @param array $where 更新条件 - * @param string $sequence 自增序列名 + * @param array $data 数据 + * @param array $where 更新条件 + * @param string $sequence 自增序列名 * @return integer|false */ public function save($data = [], $where = [], $sequence = null) { + if (is_string($data)) { + $sequence = $data; + $data = []; + } + + // 数据自动验证 if (!empty($data)) { - // 数据自动验证 if (!$this->validateData($data)) { return false; } + // 数据对象赋值 foreach ($data as $key => $value) { $this->setAttr($key, $value, $data); } - if (!empty($where)) { - $this->isUpdate = true; - } } - // 检测字段 - if (!empty($this->field)) { - foreach ($this->data as $key => $val) { - if (!in_array($key, $this->field)) { - unset($this->data[$key]); + if (!empty($where)) { + $this->isUpdate = true; + $this->updateWhere = $where; + } + + // 自动关联写入 + if (!empty($this->relationWrite)) { + $relation = []; + foreach ($this->relationWrite as $key => $name) { + if (is_array($name)) { + if (key($name) === 0) { + $relation[$key] = []; + foreach ($name as $val) { + if (isset($this->data[$val])) { + $relation[$key][$val] = $this->data[$val]; + unset($this->data[$val]); + } + } + } else { + $relation[$key] = $name; + } + } elseif (isset($this->relation[$name])) { + $relation[$name] = $this->relation[$name]; + } elseif (isset($this->data[$name])) { + $relation[$name] = $this->data[$name]; + unset($this->data[$name]); } } } @@ -640,16 +1087,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; } - + $pk = $this->getPk(); if ($this->isUpdate) { // 自动更新 $this->autoCompleteData($this->update); @@ -659,85 +1101,261 @@ abstract class Model implements \JsonSerializable, \ArrayAccess return false; } - // 去除没有更新的字段 - $data = []; + // 获取有更新的数据 + $data = $this->getChangedData(); + + if (empty($data) || (count($data) == 1 && is_string($pk) && isset($data[$pk]))) { + // 关联更新 + if (isset($relation)) { + $this->autoRelationUpdate($relation); + } + return 0; + } elseif ($this->autoWriteTimestamp && $this->updateTime && !isset($data[$this->updateTime])) { + // 自动写入更新时间 + $data[$this->updateTime] = $this->autoWriteTimestamp($this->updateTime); + $this->data[$this->updateTime] = $data[$this->updateTime]; + } + + if (empty($where) && !empty($this->updateWhere)) { + $where = $this->updateWhere; + } + + // 保留主键数据 foreach ($this->data as $key => $val) { - if (in_array($key, $this->change) || $this->isPk($key)) { + if ($this->isPk($key)) { $data[$key] = $val; } } - if (!empty($this->readonly)) { - // 只读字段不允许更新 - foreach ($this->readonly as $key => $field) { - if (isset($data[$field])) { - unset($data[$field]); - } + $array = []; + + foreach ((array) $pk as $key) { + if (isset($data[$key])) { + $array[$key] = $data[$key]; + unset($data[$key]); } } - if (empty($where) && !empty($this->updateWhere)) { - $where = $this->updateWhere; + if (!empty($array)) { + $where = $array; } - if (!empty($where)) { - $pk = $this->getPk(); - if (is_string($pk) && isset($data[$pk])) { - if (!isset($where[$pk])) { - unset($where); - $where[$pk] = $data[$pk]; - } - unset($data[$pk]); - } + // 检测字段 + $allowFields = $this->checkAllowField(array_merge($this->auto, $this->update)); + + // 模型更新 + if (!empty($allowFields)) { + $result = $this->getQuery()->where($where)->strict(false)->field($allowFields)->update($data); + } else { + $result = $this->getQuery()->where($where)->update($data); + } + + // 关联更新 + if (isset($relation)) { + $this->autoRelationUpdate($relation); } - $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 ($this->autoWriteTimestamp) { + if ($this->createTime && !isset($this->data[$this->createTime])) { + $this->data[$this->createTime] = $this->autoWriteTimestamp($this->createTime); + } + if ($this->updateTime && !isset($this->data[$this->updateTime])) { + $this->data[$this->updateTime] = $this->autoWriteTimestamp($this->updateTime); + } } if (false === $this->trigger('before_insert', $this)) { return false; } - $result = $this->db()->insert($this->data); + // 检测字段 + $allowFields = $this->checkAllowField(array_merge($this->auto, $this->insert)); + if (!empty($allowFields)) { + $result = $this->getQuery()->strict(false)->field($allowFields)->insert($this->data, $this->replace, false, $sequence); + } else { + $result = $this->getQuery()->insert($this->data, $this->replace, false, $sequence); + } // 获取自动增长主键 - if ($result) { - $insertId = $this->db()->getLastInsID($sequence); - $pk = $this->getPk(); - if (is_string($pk) && $insertId) { - $this->data[$pk] = $insertId; + if ($result && $insertId = $this->getQuery()->getLastInsID($sequence)) { + foreach ((array) $pk as $key) { + if (!isset($this->data[$key]) || '' == $this->data[$key]) { + $this->data[$key] = $insertId; + } + } + } + + // 关联写入 + if (isset($relation)) { + foreach ($relation as $name => $val) { + $method = Loader::parseName($name, 1, false); + $this->$method()->save($val); } } + // 标记为更新 $this->isUpdate = true; - // 清空change - $this->change = []; + // 新增回调 $this->trigger('after_insert', $this); } // 写入回调 $this->trigger('after_write', $this); + // 重新记录原始数据 + $this->origin = $this->data; + + return $result; + } + + protected function checkAllowField($auto = []) + { + if (true === $this->field) { + $this->field = $this->getQuery()->getTableInfo('', 'fields'); + $field = $this->field; + } elseif (!empty($this->field)) { + $field = array_merge($this->field, $auto); + if ($this->autoWriteTimestamp) { + array_push($field, $this->createTime, $this->updateTime); + } + } elseif (!empty($this->except)) { + $fields = $this->getQuery()->getTableInfo('', 'fields'); + $field = array_diff($fields, (array) $this->except); + $this->field = $field; + } else { + $field = []; + } + + if ($this->disuse) { + // 废弃字段 + $field = array_diff($field, (array) $this->disuse); + } + return $field; + } + + protected function autoRelationUpdate($relation) + { + foreach ($relation as $name => $val) { + if ($val instanceof Model) { + $val->save(); + } else { + unset($this->data[$name]); + $model = $this->getAttr($name); + if ($model instanceof Model) { + $model->save($val); + } + } + } + } + + /** + * 获取变化的数据 并排除只读数据 + * @access public + * @return array + */ + public function getChangedData() + { + if ($this->force) { + $data = $this->data; + } else { + $data = array_udiff_assoc($this->data, $this->origin, function ($a, $b) { + if ((empty($a) || empty($b)) && $a !== $b) { + return 1; + } + return is_object($a) || $a != $b ? 1 : 0; + }); + } + + if (!empty($this->readonly)) { + // 只读字段不允许更新 + foreach ($this->readonly as $key => $field) { + if (isset($data[$field])) { + unset($data[$field]); + } + } + } + + return $data; + } + + /** + * 字段值(延迟)增长 + * @access public + * @param string $field 字段名 + * @param integer $step 增长值 + * @param integer $lazyTime 延时时间(s) + * @return integer|true + * @throws Exception + */ + public function setInc($field, $step = 1, $lazyTime = 0) + { + // 更新条件 + $where = $this->getWhere(); + + $result = $this->getQuery()->where($where)->setInc($field, $step, $lazyTime); + if (true !== $result) { + $this->data[$field] += $step; + } + return $result; } + /** + * 字段值(延迟)增长 + * @access public + * @param string $field 字段名 + * @param integer $step 增长值 + * @param integer $lazyTime 延时时间(s) + * @return integer|true + * @throws Exception + */ + public function setDec($field, $step = 1, $lazyTime = 0) + { + // 更新条件 + $where = $this->getWhere(); + $result = $this->getQuery()->where($where)->setDec($field, $step, $lazyTime); + if (true !== $result) { + $this->data[$field] -= $step; + } + + return $result; + } + + /** + * 获取更新条件 + * @access protected + * @return mixed + */ + protected function getWhere() + { + // 删除条件 + $pk = $this->getPk(); + + if (is_string($pk) && isset($this->data[$pk])) { + $where = [$pk => $this->data[$pk]]; + } elseif (!empty($this->updateWhere)) { + $where = $this->updateWhere; + } else { + $where = null; + } + return $where; + } + /** * 保存多个数据到当前数据对象 * @access public - * @param array $dataSet 数据 - * @param boolean $replace 是否自动识别更新和写入 + * @param array $dataSet 数据 + * @param boolean $replace 是否自动识别更新和写入 * @return array|false + * @throws \Exception */ public function saveAll($dataSet, $replace = true) { @@ -745,14 +1363,14 @@ abstract class Model implements \JsonSerializable, \ArrayAccess // 数据批量验证 $validate = $this->validate; foreach ($dataSet as $data) { - if (!$this->validate($validate)->validateData($data)) { + if (!$this->validateData($data, $validate)) { return false; } } } $result = []; - $db = $this->db(); + $db = $this->getQuery(); $db->startTrans(); try { $pk = $this->getPk(); @@ -760,14 +1378,14 @@ abstract class Model implements \JsonSerializable, \ArrayAccess $auto = true; } foreach ($dataSet as $key => $data) { - if (!empty($auto) && isset($data[$pk])) { - $result[$key] = self::update($data); + if ($this->isUpdate || (!empty($auto) && isset($data[$pk]))) { + $result[$key] = self::update($data, [], $this->field); } else { - $result[$key] = self::create($data); + $result[$key] = self::create($data, $this->field); } } $db->commit(); - return $result; + return $this->toCollection($result); } catch (\Exception $e) { $db->rollback(); throw $e; @@ -777,18 +1395,48 @@ abstract class Model implements \JsonSerializable, \ArrayAccess /** * 设置允许写入的字段 * @access public - * @param bool|array $field 允许写入的字段 如果为true只允许写入数据表字段 + * @param string|array $field 允许写入的字段 如果为true只允许写入数据表字段 * @return $this */ public function allowField($field) { - if (true === $field) { - $field = $this->db()->getTableInfo('', 'fields'); + if (is_string($field)) { + $field = explode(',', $field); } $this->field = $field; return $this; } + /** + * 设置排除写入的字段 + * @access public + * @param string|array $field 排除允许写入的字段 + * @return $this + */ + public function except($field) + { + if (is_string($field)) { + $field = explode(',', $field); + } + $this->except = $field; + return $this; + } + + /** + * 设置只读字段 + * @access public + * @param mixed $field 只读字段 + * @return $this + */ + public function readonly($field) + { + if (is_string($field)) { + $field = explode(',', $field); + } + $this->readonly = $field; + return $this; + } + /** * 是否为更新数据 * @access public @@ -818,9 +1466,14 @@ abstract class Model implements \JsonSerializable, \ArrayAccess $field = $value; $value = null; } - if (!in_array($field, $this->change)) { - $this->setAttr($field, !is_null($value) ? $value : (isset($this->data[$field]) ? $this->data[$field] : $value)); + + if (!isset($this->data[$field])) { + $default = null; + } else { + $default = $this->data[$field]; } + + $this->setAttr($field, !is_null($value) ? $value : $default); } } @@ -835,9 +1488,27 @@ abstract class Model implements \JsonSerializable, \ArrayAccess return false; } - $result = $this->db()->delete($this->data); + // 删除条件 + $where = $this->getWhere(); + + // 删除当前模型数据 + $result = $this->getQuery()->where($where)->delete(); + + // 关联删除 + if (!empty($this->relationWrite)) { + foreach ($this->relationWrite as $key => $name) { + $name = is_numeric($key) ? $name : $key; + $model = $this->getAttr($name); + if ($model instanceof Model) { + $model->delete(); + } + } + } $this->trigger('after_delete', $this); + // 清空原始数据 + $this->origin = []; + return $result; } @@ -856,11 +1527,12 @@ abstract class Model implements \JsonSerializable, \ArrayAccess /** * 设置字段验证 * @access public - * @param array|string|bool $rule 验证规则 true表示自动读取验证器类 - * @param array $msg 提示信息 + * @param array|string|bool $rule 验证规则 true表示自动读取验证器类 + * @param array $msg 提示信息 + * @param bool $batch 批量验证 * @return $this */ - public function validate($rule = true, $msg = []) + public function validate($rule = true, $msg = [], $batch = false) { if (is_array($rule)) { $this->validate = [ @@ -870,6 +1542,7 @@ abstract class Model implements \JsonSerializable, \ArrayAccess } else { $this->validate = true === $rule ? $this->name : $rule; } + $this->batchValidate = $batch; return $this; } @@ -888,13 +1561,16 @@ abstract class Model implements \JsonSerializable, \ArrayAccess /** * 自动验证数据 * @access protected - * @param array $data 验证数据 + * @param array $data 验证数据 + * @param mixed $rule 验证规则 + * @param bool $batch 批量验证 * @return bool */ - protected function validateData($data) + protected function validateData($data, $rule = null, $batch = null) { - if (!empty($this->validate)) { - $info = $this->validate; + $info = is_null($rule) ? $this->validate : $rule; + + if (!empty($info)) { if (is_array($info)) { $validate = Loader::validate(); $validate->rule($info['rule']); @@ -909,7 +1585,9 @@ abstract class Model implements \JsonSerializable, \ArrayAccess $validate->scene($scene); } } - if (!$validate->check($data)) { + $batch = is_null($batch) ? $this->batchValidate : $batch; + + if (!$validate->batch($batch)->check($data)) { $this->error = $validate->getError(); if ($this->failException) { throw new ValidateException($this->error); @@ -925,7 +1603,7 @@ abstract class Model implements \JsonSerializable, \ArrayAccess /** * 返回模型的错误信息 * @access public - * @return string + * @return string|array */ public function getError() { @@ -935,9 +1613,9 @@ 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) @@ -952,8 +1630,8 @@ abstract class Model implements \JsonSerializable, \ArrayAccess /** * 触发事件 * @access protected - * @param string $event 事件名 - * @param mixed $params 传入参数(引用) + * @param string $event 事件名 + * @param mixed $params 传入参数(引用) * @return bool */ protected function trigger($event, &$params) @@ -974,12 +1652,16 @@ abstract class Model implements \JsonSerializable, \ArrayAccess /** * 写入数据 * @access public - * @param array $data 数据数组 + * @param array $data 数据数组 + * @param array|true $field 允许字段 * @return $this */ - public static function create($data = []) + public static function create($data = [], $field = null) { $model = new static(); + if (!empty($field)) { + $model->allowField($field); + } $model->isUpdate(false)->save($data, []); return $model; } @@ -987,13 +1669,17 @@ abstract class Model implements \JsonSerializable, \ArrayAccess /** * 更新数据 * @access public - * @param array $data 数据数组 - * @param array $where 更新条件 + * @param array $data 数据数组 + * @param array $where 更新条件 + * @param array|true $field 允许字段 * @return $this */ - public static function update($data = [], $where = []) + public static function update($data = [], $where = [], $field = null) { - $model = new static(); + $model = new static(); + if (!empty($field)) { + $model->allowField($field); + } $result = $model->isUpdate(true)->save($data, $where); return $model; } @@ -1004,11 +1690,19 @@ abstract class Model implements \JsonSerializable, \ArrayAccess * @param mixed $data 主键值或者查询条件(闭包) * @param array|string $with 关联预查询 * @param bool $cache 是否缓存 - * @return static + * @return static|null * @throws exception\DbException */ - public static function get($data = null, $with = [], $cache = false) + public static function get($data, $with = [], $cache = false) { + if (is_null($data)) { + return; + } + + if (true === $with || is_int($with)) { + $cache = $with; + $with = []; + } $query = static::parseQuery($data, $with, $cache); return $query->find($data); } @@ -1024,6 +1718,10 @@ abstract class Model implements \JsonSerializable, \ArrayAccess */ public static function all($data = null, $with = [], $cache = false) { + if (true === $with || is_int($with)) { + $cache = $with; + $with = []; + } $query = static::parseQuery($data, $with, $cache); return $query->select($data); } @@ -1031,9 +1729,9 @@ abstract class Model implements \JsonSerializable, \ArrayAccess /** * 分析查询表达式 * @access public - * @param mixed $data 主键列表或者查询条件(闭包) - * @param string $with 关联预查询 - * @param bool $cache 是否缓存 + * @param mixed $data 主键列表或者查询条件(闭包) + * @param string $with 关联预查询 + * @param bool $cache 是否缓存 * @return Query */ protected static function parseQuery(&$data, $with, $cache) @@ -1062,14 +1760,14 @@ abstract class Model implements \JsonSerializable, \ArrayAccess { $model = new static(); $query = $model->db(); - if (is_array($data) && key($data) !== 0) { + if (empty($data) && 0 !== $data) { + return 0; + } elseif (is_array($data) && key($data) !== 0) { $query->where($data); $data = null; } elseif ($data instanceof \Closure) { call_user_func_array($data, [ & $query]); $data = null; - } elseif (is_null($data)) { - return 0; } $resultSet = $query->select($data); $count = 0; @@ -1085,18 +1783,17 @@ abstract class Model implements \JsonSerializable, \ArrayAccess /** * 命名范围 * @access public - * @param string|array|Closure $name 命名范围名称 逗号分隔 - * @param mixed ...$params 参数调用 - * @return Model + * @param string|array|\Closure $name 命名范围名称 逗号分隔 + * @internal mixed ...$params 参数调用 + * @return Query */ public static function scope($name) { - if ($name instanceof Query) { - return $name; - } - $model = new static(); - $params = func_get_args(); - $params[0] = $model->db(); + $model = new static(); + $query = $model->db(); + $params = func_get_args(); + array_shift($params); + array_unshift($params, $query); if ($name instanceof \Closure) { call_user_func_array($name, $params); } elseif (is_string($name)) { @@ -1110,77 +1807,50 @@ abstract class Model implements \JsonSerializable, \ArrayAccess } } } - return $model; + return $query; } /** * 设置是否使用全局查询范围 - * @param bool $use 是否启用全局查询范围 + * @param bool $use 是否启用全局查询范围 * @access public - * @return Model + * @return Query */ public static function useGlobalScope($use) { - $model = new static(); - $model->useGlobalScope = $use; - return $model; + $model = new static(); + return $model->db($use); } /** * 根据关联条件查询当前模型 * @access public - * @param string $relation 关联方法名 - * @param string $operator 比较操作符 - * @param integer $count 个数 - * @param string $id 关联表的统计字段 - * @return Model + * @param string $relation 关联方法名 + * @param mixed $operator 比较操作符 + * @param integer $count 个数 + * @param string $id 关联表的统计字段 + * @return Relation|Query */ public static function has($relation, $operator = '>=', $count = 1, $id = '*') { - $model = new static(); - $info = $model->$relation()->getRelationInfo(); - $table = $info['model']::getTable(); - switch ($info['type']) { - case Relation::HAS_MANY: - return $model->db()->alias('a') - ->join($table . ' b', 'a.' . $info['localKey'] . '=b.' . $info['foreignKey'], $info['joinType']) - ->group('b.' . $info['foreignKey']) - ->having('count(' . $id . ')' . $operator . $count); - case Relation::HAS_MANY_THROUGH: - // TODO + $relation = (new static())->$relation(); + if (is_array($operator) || $operator instanceof \Closure) { + return $relation->hasWhere($operator); } + return $relation->has($operator, $count, $id); } /** * 根据关联条件查询当前模型 * @access public - * @param string $relation 关联方法名 - * @param mixed $where 查询条件(数组或者闭包) - * @return Model + * @param string $relation 关联方法名 + * @param mixed $where 查询条件(数组或者闭包) + * @param mixed $fields 字段 + * @return Relation|Query */ - public static function hasWhere($relation, $where = []) + public static function hasWhere($relation, $where = [], $fields = null) { - $model = new static(); - $info = $model->$relation()->getRelationInfo(); - switch ($info['type']) { - case Relation::HAS_ONE: - 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'], $info['joinType']) - ->where($where); - case Relation::HAS_MANY_THROUGH: - // TODO - } + return (new static())->$relation()->hasWhere($where, $fields); } /** @@ -1211,9 +1881,23 @@ 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); + + foreach ($relations as $key => $relation) { + $subRelation = ''; + $closure = null; + if ($relation instanceof \Closure) { + // 支持闭包查询过滤关联条件 + $closure = $relation; + $relation = $key; + } + if (is_array($relation)) { + $subRelation = $relation; + $relation = $key; + } elseif (strpos($relation, '.')) { + list($relation, $subRelation) = explode('.', $relation, 2); + } + $method = Loader::parseName($relation, 1, false); + $this->data[$relation] = $this->$method()->getRelation($subRelation, $closure); } return $this; } @@ -1221,130 +1905,281 @@ abstract class Model implements \JsonSerializable, \ArrayAccess /** * 预载入关联查询 返回数据集 * @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); + public function eagerlyResultSet(&$resultSet, $relation) + { + $relations = is_string($relation) ? explode(',', $relation) : $relation; + foreach ($relations as $key => $relation) { + $subRelation = ''; + $closure = false; + if ($relation instanceof \Closure) { + $closure = $relation; + $relation = $key; + } + if (is_array($relation)) { + $subRelation = $relation; + $relation = $key; + } elseif (strpos($relation, '.')) { + list($relation, $subRelation) = explode('.', $relation, 2); + } + $relation = Loader::parseName($relation, 1, false); + $this->$relation()->eagerlyResultSet($resultSet, $relation, $subRelation, $closure); + } } /** * 预载入关联查询 返回模型对象 * @access public - * @param Model $result 数据对象 - * @param string $relation 关联名 + * @param Model $result 数据对象 + * @param string $relation 关联名 * @return Model */ - public function eagerlyResult($result, $relation) + public function eagerlyResult(&$result, $relation) + { + $relations = is_string($relation) ? explode(',', $relation) : $relation; + + foreach ($relations as $key => $relation) { + $subRelation = ''; + $closure = false; + if ($relation instanceof \Closure) { + $closure = $relation; + $relation = $key; + } + if (is_array($relation)) { + $subRelation = $relation; + $relation = $key; + } elseif (strpos($relation, '.')) { + list($relation, $subRelation) = explode('.', $relation, 2); + } + $relation = Loader::parseName($relation, 1, false); + $this->$relation()->eagerlyResult($result, $relation, $subRelation, $closure); + } + } + + /** + * 关联统计 + * @access public + * @param Model $result 数据对象 + * @param string|array $relation 关联名 + * @return void + */ + public function relationCount(&$result, $relation) + { + $relations = is_string($relation) ? explode(',', $relation) : $relation; + + foreach ($relations as $key => $relation) { + $closure = false; + if ($relation instanceof \Closure) { + $closure = $relation; + $relation = $key; + } elseif (is_string($key)) { + $name = $relation; + $relation = $key; + } + $relation = Loader::parseName($relation, 1, false); + $count = $this->$relation()->relationCount($result, $closure); + if (!isset($name)) { + $name = Loader::parseName($relation) . '_count'; + } + $result->setAttr($name, $count); + } + } + + /** + * 获取模型的默认外键名 + * @access public + * @param string $name 模型名 + * @return string + */ + protected function getForeignKey($name) { - return $this->relation()->eagerlyResult($result, $relation); + if (strpos($name, '\\')) { + $name = basename(str_replace('\\', '/', $name)); + } + return Loader::parseName($name) . '_id'; } /** * HAS ONE 关联定义 * @access public - * @param string $model 模型名 + * @param string $model 模型名 * @param string $foreignKey 关联外键 - * @param string $localKey 关联主键 - * @param array $alias 别名定义 - * @param string $joinType JOIN类型 - * @return Relation + * @param string $localKey 当前模型主键 + * @param array $alias 别名定义(已经废弃) + * @param string $joinType JOIN类型 + * @return HasOne */ 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, $alias, $joinType); + $foreignKey = $foreignKey ?: $this->getForeignKey($this->name); + return new HasOne($this, $model, $foreignKey, $localKey, $joinType); } /** * BELONGS TO 关联定义 * @access public - * @param string $model 模型名 + * @param string $model 模型名 * @param string $foreignKey 关联外键 - * @param string $otherKey 关联主键 - * @param array $alias 别名定义 - * @param string $joinType JOIN类型 - * @return Relation + * @param string $localKey 关联主键 + * @param array $alias 别名定义(已经废弃) + * @param string $joinType JOIN类型 + * @return BelongsTo */ - public function belongsTo($model, $foreignKey = '', $otherKey = '', $alias = [], $joinType = 'INNER') + public function belongsTo($model, $foreignKey = '', $localKey = '', $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, $alias, $joinType); + $foreignKey = $foreignKey ?: $this->getForeignKey($model); + $localKey = $localKey ?: (new $model)->getPk(); + $trace = debug_backtrace(false, 2); + $relation = Loader::parseName($trace[1]['function']); + return new BelongsTo($this, $model, $foreignKey, $localKey, $joinType, $relation); } /** * HAS MANY 关联定义 * @access public - * @param string $model 模型名 + * @param string $model 模型名 * @param string $foreignKey 关联外键 - * @param string $localKey 关联主键 - * @param array $alias 别名定义 - * @return Relation + * @param string $localKey 当前模型主键 + * @return HasMany */ - public function hasMany($model, $foreignKey = '', $localKey = '', $alias = []) + public function hasMany($model, $foreignKey = '', $localKey = '') { // 记录当前关联信息 $model = $this->parseModel($model); $localKey = $localKey ?: $this->getPk(); - $foreignKey = $foreignKey ?: Loader::parseName($this->name) . '_id'; - return $this->relation()->hasMany($model, $foreignKey, $localKey, $alias); + $foreignKey = $foreignKey ?: $this->getForeignKey($this->name); + return new HasMany($this, $model, $foreignKey, $localKey); } /** * HAS MANY 远程关联定义 * @access public - * @param string $model 模型名 - * @param string $through 中间模型名 + * @param string $model 模型名 + * @param string $through 中间模型名 * @param string $foreignKey 关联外键 * @param string $throughKey 关联外键 - * @param string $localKey 关联主键 - * @param array $alias 别名定义 - * @return Relation + * @param string $localKey 当前模型主键 + * @return HasManyThrough */ - public function hasManyThrough($model, $through, $foreignKey = '', $throughKey = '', $localKey = '', $alias = []) + public function hasManyThrough($model, $through, $foreignKey = '', $throughKey = '', $localKey = '') { // 记录当前关联信息 $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); + $foreignKey = $foreignKey ?: $this->getForeignKey($this->name); + $throughKey = $throughKey ?: $this->getForeignKey($through); + return new HasManyThrough($this, $model, $through, $foreignKey, $throughKey, $localKey); } /** * BELONGS TO MANY 关联定义 * @access public - * @param string $model 模型名 - * @param string $table 中间表名 + * @param string $model 模型名 + * @param string $table 中间表名 * @param string $foreignKey 关联外键 - * @param string $localKey 当前模型关联键 - * @param array $alias 别名定义 - * @return Relation + * @param string $localKey 当前模型关联键 + * @return BelongsToMany */ - public function belongsToMany($model, $table = '', $foreignKey = '', $localKey = '', $alias = []) + public function belongsToMany($model, $table = '', $foreignKey = '', $localKey = '') { // 记录当前关联信息 $model = $this->parseModel($model); $name = Loader::parseName(basename(str_replace('\\', '/', $model))); - $table = $table ?: $this->db()->getTable(Loader::parseName($this->name) . '_' . $name); + $table = $table ?: Loader::parseName($this->name) . '_' . $name; $foreignKey = $foreignKey ?: $name . '_id'; - $localKey = $localKey ?: Loader::parseName($this->name) . '_id'; - return $this->relation()->belongsToMany($model, $table, $foreignKey, $localKey, $alias); + $localKey = $localKey ?: $this->getForeignKey($this->name); + return new BelongsToMany($this, $model, $table, $foreignKey, $localKey); + } + + /** + * MORPH MANY 关联定义 + * @access public + * @param string $model 模型名 + * @param string|array $morph 多态字段信息 + * @param string $type 多态类型 + * @return MorphMany + */ + public function morphMany($model, $morph = null, $type = '') + { + // 记录当前关联信息 + $model = $this->parseModel($model); + if (is_null($morph)) { + $trace = debug_backtrace(false, 2); + $morph = Loader::parseName($trace[1]['function']); + } + $type = $type ?: get_class($this); + if (is_array($morph)) { + list($morphType, $foreignKey) = $morph; + } else { + $morphType = $morph . '_type'; + $foreignKey = $morph . '_id'; + } + return new MorphMany($this, $model, $foreignKey, $morphType, $type); + } + + /** + * MORPH One 关联定义 + * @access public + * @param string $model 模型名 + * @param string|array $morph 多态字段信息 + * @param string $type 多态类型 + * @return MorphOne + */ + public function morphOne($model, $morph = null, $type = '') + { + // 记录当前关联信息 + $model = $this->parseModel($model); + if (is_null($morph)) { + $trace = debug_backtrace(false, 2); + $morph = Loader::parseName($trace[1]['function']); + } + $type = $type ?: get_class($this); + if (is_array($morph)) { + list($morphType, $foreignKey) = $morph; + } else { + $morphType = $morph . '_type'; + $foreignKey = $morph . '_id'; + } + return new MorphOne($this, $model, $foreignKey, $morphType, $type); + } + + /** + * MORPH TO 关联定义 + * @access public + * @param string|array $morph 多态字段信息 + * @param array $alias 多态别名定义 + * @return MorphTo + */ + public function morphTo($morph = null, $alias = []) + { + $trace = debug_backtrace(false, 2); + $relation = Loader::parseName($trace[1]['function']); + + if (is_null($morph)) { + $morph = $relation; + } + // 记录当前关联信息 + if (is_array($morph)) { + list($morphType, $foreignKey) = $morph; + } else { + $morphType = $morph . '_type'; + $foreignKey = $morph . '_id'; + } + return new MorphTo($this, $morphType, $foreignKey, $alias, $relation); } public function __call($method, $args) { - $query = $this->db(); + $query = $this->db(true, false); if (method_exists($this, 'scope' . $method)) { // 动态调用命名范围 $method = 'scope' . $method; @@ -1356,26 +2191,27 @@ abstract class Model implements \JsonSerializable, \ArrayAccess } } - public static function __callStatic($method, $params) + public static function __callStatic($method, $args) { - $query = self::getDb(); - return call_user_func_array([$query, $method], $params); - } + $model = new static(); + $query = $model->db(); + if (method_exists($model, 'scope' . $method)) { + // 动态调用命名范围 + $method = 'scope' . $method; + array_unshift($args, $query); - protected static function getDb() - { - $model = get_called_class(); - if (!isset(self::$links[$model])) { - self::$links[$model] = (new static())->db(); + call_user_func_array([$model, $method], $args); + return $query; + } else { + return call_user_func_array([$query, $method], $args); } - return self::$links[$model]; } /** * 修改器 设置数据对象的值 * @access public - * @param string $name 名称 - * @param mixed $value 值 + * @param string $name 名称 + * @param mixed $value 值 * @return void */ public function __set($name, $value) @@ -1403,7 +2239,7 @@ abstract class Model implements \JsonSerializable, \ArrayAccess public function __isset($name) { try { - if (array_key_exists($name, $this->data)) { + if (array_key_exists($name, $this->data) || array_key_exists($name, $this->relation)) { return true; } else { $this->getAttr($name); @@ -1423,7 +2259,7 @@ abstract class Model implements \JsonSerializable, \ArrayAccess */ public function __unset($name) { - unset($this->data[$name]); + unset($this->data[$name], $this->relation[$name]); } public function __toString() @@ -1466,4 +2302,49 @@ abstract class Model implements \JsonSerializable, \ArrayAccess $this->initialize(); } + /** + * 模型事件快捷方法 + * @param $callback + * @param bool $override + */ + protected static function beforeInsert($callback, $override = false) + { + self::event('before_insert', $callback, $override); + } + + protected static function afterInsert($callback, $override = false) + { + self::event('after_insert', $callback, $override); + } + + protected static function beforeUpdate($callback, $override = false) + { + self::event('before_update', $callback, $override); + } + + protected static function afterUpdate($callback, $override = false) + { + self::event('after_update', $callback, $override); + } + + protected static function beforeWrite($callback, $override = false) + { + self::event('before_write', $callback, $override); + } + + protected static function afterWrite($callback, $override = false) + { + self::event('after_write', $callback, $override); + } + + protected static function beforeDelete($callback, $override = false) + { + self::event('before_delete', $callback, $override); + } + + protected static function afterDelete($callback, $override = false) + { + self::event('after_delete', $callback, $override); + } + } diff --git a/library/think/Paginator.php b/library/think/Paginator.php index 7385ebb0aacf52dfb56f5d5b2bc2d9d757c5ee72..365556784a850d3074f1b07aab8fecb984939570 100644 --- a/library/think/Paginator.php +++ b/library/think/Paginator.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2016 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- @@ -11,15 +11,19 @@ namespace think; -use think\paginator\Collection as PaginatorCollection; -use think\Request; +use ArrayAccess; +use ArrayIterator; +use Countable; +use IteratorAggregate; +use JsonSerializable; +use Traversable; -abstract class Paginator +abstract class Paginator implements ArrayAccess, Countable, IteratorAggregate, JsonSerializable { /** @var bool 是否为简洁模式 */ protected $simple = false; - /** @var PaginatorCollection 数据集 */ + /** @var Collection 数据集 */ protected $items; /** @var integer 当前页 */ @@ -42,33 +46,39 @@ abstract class Paginator 'var_page' => 'page', 'path' => '/', 'query' => [], - 'fragment' => '' + 'fragment' => '', ]; - protected function __construct($items, $listRows, $currentPage = null, $total = null, $simple = false, $options = []) + /** @var mixed simple模式下的下个元素 */ + protected $nextItem; + + public function __construct($items, $listRows, $currentPage = null, $total = null, $simple = false, $options = []) { $this->options = array_merge($this->options, $options); - $this->options['path'] = $this->options['path'] != '/' ? rtrim($this->options['path'], '/') : $this->options['path']; + $this->options['path'] = '/' != $this->options['path'] ? rtrim($this->options['path'], '/') : $this->options['path']; $this->simple = $simple; $this->listRows = $listRows; + if (!$items instanceof Collection) { + $items = Collection::make($items); + } + if ($simple) { - if (!$items instanceof Collection) { - $items = Collection::make($items); - } $this->currentPage = $this->setCurrentPage($currentPage); $this->hasMore = count($items) > ($this->listRows); - $items = $items->slice(0, $this->listRows); + if ($this->hasMore) { + $this->nextItem = $items->slice($this->listRows, 1); + } + $items = $items->slice(0, $this->listRows); } else { $this->total = $total; - $this->lastPage = (int)ceil($total / $listRows); + $this->lastPage = (int) ceil($total / $listRows); $this->currentPage = $this->setCurrentPage($currentPage); $this->hasMore = $this->currentPage < $this->lastPage; } - - $this->items = PaginatorCollection::make($items, $this); + $this->items = $items; } /** @@ -78,12 +88,11 @@ abstract class Paginator * @param bool $simple * @param null $total * @param array $options - * @return PaginatorCollection + * @return Paginator */ public static function make($items, $listRows, $currentPage = null, $total = null, $simple = false, $options = []) { - $paginator = new static($items, $listRows, $currentPage, $total, $simple, $options); - return $paginator->items; + return new static($items, $listRows, $currentPage, $total, $simple, $options); } protected function setCurrentPage($currentPage) @@ -119,7 +128,7 @@ abstract class Paginator } $url = $path; if (!empty($parameters)) { - $url .= '?' . urldecode(http_build_query($parameters, null, '&')); + $url .= '?' . http_build_query($parameters, null, '&'); } return $url . $this->buildFragment(); } @@ -132,9 +141,9 @@ abstract class Paginator */ public static function getCurrentPage($varPage = 'page', $default = 1) { - $page = Request::instance()->request($varPage); + $page = (int) Request::instance()->param($varPage); - if (filter_var($page, FILTER_VALIDATE_INT) !== false && (int)$page >= 1) { + if (filter_var($page, FILTER_VALIDATE_INT) !== false && $page >= 1) { return $page; } @@ -182,7 +191,7 @@ abstract class Paginator */ public function hasPages() { - return !($this->currentPage == 1 && !$this->hasMore); + return !(1 == $this->currentPage && !$this->hasMore); } /** @@ -239,7 +248,6 @@ abstract class Paginator return $this; } - /** * 构造锚点字符串 * @@ -255,4 +263,147 @@ abstract class Paginator * @return mixed */ abstract public function render(); -} \ No newline at end of file + + public function items() + { + return $this->items->all(); + } + + public function getCollection() + { + return $this->items; + } + + public function isEmpty() + { + return $this->items->isEmpty(); + } + + /** + * 给每个元素执行个回调 + * + * @param callable $callback + * @return $this + */ + public function each(callable $callback) + { + foreach ($this->items as $key => $item) { + $result = $callback($item, $key); + if (false === $result) { + break; + } elseif (!is_object($item)) { + $this->items[$key] = $result; + } + } + + return $this; + } + + /** + * Retrieve an external iterator + * @return Traversable An instance of an object implementing Iterator or + * Traversable + */ + public function getIterator() + { + return new ArrayIterator($this->items->all()); + } + + /** + * Whether a offset exists + * @param mixed $offset + * @return bool + */ + public function offsetExists($offset) + { + return $this->items->offsetExists($offset); + } + + /** + * Offset to retrieve + * @param mixed $offset + * @return mixed + */ + public function offsetGet($offset) + { + return $this->items->offsetGet($offset); + } + + /** + * Offset to set + * @param mixed $offset + * @param mixed $value + */ + public function offsetSet($offset, $value) + { + $this->items->offsetSet($offset, $value); + } + + /** + * Offset to unset + * @param mixed $offset + * @return void + * @since 5.0.0 + */ + public function offsetUnset($offset) + { + $this->items->offsetUnset($offset); + } + + /** + * Count elements of an object + */ + public function count() + { + return $this->items->count(); + } + + public function __toString() + { + return (string) $this->render(); + } + + public function toArray() + { + if ($this->simple) { + return [ + 'per_page' => $this->listRows, + 'current_page' => $this->currentPage, + 'has_more' => $this->hasMore, + 'next_item' => $this->nextItem, + 'data' => $this->items->toArray(), + ]; + } else { + return [ + 'total' => $this->total, + 'per_page' => $this->listRows, + 'current_page' => $this->currentPage, + 'last_page' => $this->lastPage, + 'data' => $this->items->toArray(), + ]; + } + + } + + /** + * Specify data which should be serialized to JSON + */ + public function jsonSerialize() + { + return $this->toArray(); + } + + public function __call($name, $arguments) + { + $collection = $this->getCollection(); + + $result = call_user_func_array([$collection, $name], $arguments); + + if ($result === $collection) { + return $this; + } + + return $result; + } + +} diff --git a/library/think/Process.php b/library/think/Process.php index 1982de2447f7f68093e43de58fe256658d1f763c..6f3faa315e3f25e0647850d68cdfd5deb07323f5 100644 --- a/library/think/Process.php +++ b/library/think/Process.php @@ -14,9 +14,9 @@ namespace think; use think\process\exception\Failed as ProcessFailedException; use think\process\exception\Timeout as ProcessTimeoutException; use think\process\pipes\Pipes; -use think\process\Utils; use think\process\pipes\Unix as UnixPipes; use think\process\pipes\Windows as WindowsPipes; +use think\process\Utils; class Process { @@ -47,10 +47,10 @@ class Process private $exitcode; private $fallbackExitcode; private $processInformation; - private $outputDisabled = false; + private $outputDisabled = false; private $stdout; private $stderr; - private $enhanceWindowsCompatibility = true; + private $enhanceWindowsCompatibility = true; private $enhanceSigchildCompatibility; private $process; private $status = self::STATUS_READY; @@ -147,7 +147,7 @@ class Process $this->enhanceSigchildCompatibility = '\\' !== DS && $this->isSigchildEnabled(); $this->options = array_replace([ 'suppress_errors' => true, - 'binary_pipes' => true + 'binary_pipes' => true, ], $options); } @@ -490,7 +490,7 @@ class Process public function getExitCodeText() { if (null === $exitcode = $this->getExitCode()) { - return null; + return; } return isset(self::$exitCodes[$exitcode]) ? self::$exitCodes[$exitcode] : 'Unknown error'; @@ -586,7 +586,7 @@ class Process */ public function isStarted() { - return $this->status != self::STATUS_READY; + return self::STATUS_READY != $this->status; } /** @@ -597,7 +597,7 @@ class Process { $this->updateStatus(false); - return $this->status == self::STATUS_TERMINATED; + return self::STATUS_TERMINATED == $this->status; } /** @@ -645,7 +645,7 @@ class Process * @param string $line */ public function addOutput($line) - { +{ $this->lastOutputTime = microtime(true); $this->stdout .= $line; } @@ -655,7 +655,7 @@ class Process * @param string $line */ public function addErrorOutput($line) - { +{ $this->lastOutputTime = microtime(true); $this->stderr .= $line; } @@ -665,7 +665,7 @@ class Process * @return string */ public function getCommandLine() - { +{ return $this->commandline; } @@ -675,7 +675,7 @@ class Process * @return self */ public function setCommandLine($commandline) - { +{ $this->commandline = $commandline; return $this; @@ -686,7 +686,7 @@ class Process * @return float|null */ public function getTimeout() - { +{ return $this->timeout; } @@ -695,7 +695,7 @@ class Process * @return float|null */ public function getIdleTimeout() - { +{ return $this->idleTimeout; } @@ -705,7 +705,7 @@ class Process * @return self */ public function setTimeout($timeout) - { +{ $this->timeout = $this->validateTimeout($timeout); return $this; @@ -717,7 +717,7 @@ class Process * @return self */ public function setIdleTimeout($timeout) - { +{ if (null !== $timeout && $this->outputDisabled) { throw new \LogicException('Idle timeout can not be set while the output is disabled.'); } @@ -733,7 +733,7 @@ class Process * @return self */ public function setTty($tty) - { +{ if ('\\' === DS && $tty) { throw new \RuntimeException('TTY mode is not supported on Windows platform.'); } @@ -741,7 +741,7 @@ class Process throw new \RuntimeException('TTY mode requires /dev/tty to be readable.'); } - $this->tty = (bool)$tty; + $this->tty = (bool) $tty; return $this; } @@ -751,7 +751,7 @@ class Process * @return bool */ public function isTty() - { +{ return $this->tty; } @@ -761,8 +761,8 @@ class Process * @return self */ public function setPty($bool) - { - $this->pty = (bool)$bool; +{ + $this->pty = (bool) $bool; return $this; } @@ -772,7 +772,7 @@ class Process * @return bool */ public function isPty() - { +{ return $this->pty; } @@ -781,7 +781,7 @@ class Process * @return string|null */ public function getWorkingDirectory() - { +{ if (null === $this->cwd) { return getcwd() ?: null; } @@ -795,7 +795,7 @@ class Process * @return self */ public function setWorkingDirectory($cwd) - { +{ $this->cwd = $cwd; return $this; @@ -806,7 +806,7 @@ class Process * @return array */ public function getEnv() - { +{ return $this->env; } @@ -816,14 +816,14 @@ class Process * @return self */ public function setEnv(array $env) - { +{ $env = array_filter($env, function ($value) { return !is_array($value); }); $this->env = []; foreach ($env as $key => $value) { - $this->env[(binary)$key] = (binary)$value; + $this->env[(binary) $key] = (binary) $value; } return $this; @@ -834,7 +834,7 @@ class Process * @return null|string */ public function getInput() - { +{ return $this->input; } @@ -844,7 +844,7 @@ class Process * @return self */ public function setInput($input) - { +{ if ($this->isRunning()) { throw new \LogicException('Input can not be set while the process is running.'); } @@ -859,7 +859,7 @@ class Process * @return array */ public function getOptions() - { +{ return $this->options; } @@ -869,7 +869,7 @@ class Process * @return self */ public function setOptions(array $options) - { +{ $this->options = $options; return $this; @@ -880,7 +880,7 @@ class Process * @return bool */ public function getEnhanceWindowsCompatibility() - { +{ return $this->enhanceWindowsCompatibility; } @@ -890,8 +890,8 @@ class Process * @return self */ public function setEnhanceWindowsCompatibility($enhance) - { - $this->enhanceWindowsCompatibility = (bool)$enhance; +{ + $this->enhanceWindowsCompatibility = (bool) $enhance; return $this; } @@ -901,7 +901,7 @@ class Process * @return bool */ public function getEnhanceSigchildCompatibility() - { +{ return $this->enhanceSigchildCompatibility; } @@ -911,8 +911,8 @@ class Process * @return self */ public function setEnhanceSigchildCompatibility($enhance) - { - $this->enhanceSigchildCompatibility = (bool)$enhance; +{ + $this->enhanceSigchildCompatibility = (bool) $enhance; return $this; } @@ -921,8 +921,8 @@ class Process * 是否超时 */ public function checkTimeout() - { - if ($this->status !== self::STATUS_STARTED) { +{ + if (self::STATUS_STARTED !== $this->status) { return; } @@ -944,7 +944,7 @@ class Process * @return bool */ public static function isPtySupported() - { +{ static $result; if (null !== $result) { @@ -970,7 +970,7 @@ class Process * @return array */ private function getDescriptors() - { +{ if ('\\' === DS) { $this->processPipes = WindowsPipes::create($this, $this->input); } else { @@ -994,7 +994,7 @@ class Process * @return callable */ protected function buildCallback($callback) - { +{ $out = self::OUT; $callback = function ($type, $data) use ($callback, $out) { if ($out == $type) { @@ -1016,7 +1016,7 @@ class Process * @param bool $blocking */ protected function updateStatus($blocking) - { +{ if (self::STATUS_STARTED !== $this->status) { return; } @@ -1036,7 +1036,7 @@ class Process * @return bool */ protected function isSigchildEnabled() - { +{ if (null !== self::$sigchild) { return self::$sigchild; } @@ -1057,8 +1057,8 @@ class Process * @return float|null */ private function validateTimeout($timeout) - { - $timeout = (float)$timeout; +{ + $timeout = (float) $timeout; if (0.0 === $timeout) { $timeout = null; @@ -1075,15 +1075,15 @@ class Process * @param bool $close */ private function readPipes($blocking, $close) - { +{ $result = $this->processPipes->readAndWrite($blocking, $close); $callback = $this->callback; foreach ($result as $type => $data) { if (3 == $type) { - $this->fallbackExitcode = (int)$data; + $this->fallbackExitcode = (int) $data; } else { - $callback($type === self::STDOUT ? self::OUT : self::ERR, $data); + $callback(self::STDOUT === $type ? self::OUT : self::ERR, $data); } } } @@ -1092,7 +1092,7 @@ class Process * 捕获退出码 */ private function captureExitCode() - { +{ if (isset($this->processInformation['exitcode']) && -1 != $this->processInformation['exitcode']) { $this->exitcode = $this->processInformation['exitcode']; } @@ -1103,7 +1103,7 @@ class Process * @return int 退出码 */ private function close() - { +{ $this->processPipes->close(); if (is_resource($this->process)) { $exitcode = proc_close($this->process); @@ -1117,7 +1117,7 @@ class Process if (-1 === $this->exitcode && null !== $this->fallbackExitcode) { $this->exitcode = $this->fallbackExitcode; } elseif (-1 === $this->exitcode && $this->processInformation['signaled'] - && 0 < $this->processInformation['termsig'] + && 0 < $this->processInformation['termsig'] ) { $this->exitcode = 128 + $this->processInformation['termsig']; } @@ -1129,7 +1129,7 @@ class Process * 重置数据 */ private function resetProcessData() - { +{ $this->starttime = null; $this->callback = null; $this->exitcode = null; @@ -1151,7 +1151,7 @@ class Process * @return bool */ private function doSignal($signal, $throwException) - { +{ if (!$this->isRunning()) { if ($throwException) { throw new \LogicException('Can not send signal on a non running process.'); @@ -1186,7 +1186,7 @@ class Process * @param string $functionName */ private function requireProcessIsStarted($functionName) - { +{ if (!$this->isStarted()) { throw new \LogicException(sprintf('Process must be started before calling %s.', $functionName)); } @@ -1197,9 +1197,9 @@ class Process * @param string $functionName */ private function requireProcessIsTerminated($functionName) - { +{ if (!$this->isTerminated()) { throw new \LogicException(sprintf('Process must be terminated before calling %s.', $functionName)); } } -} \ No newline at end of file +} diff --git a/library/think/Request.php b/library/think/Request.php index 077e190df002d42ea706a517cc3198d464bc18ff..5997a763a1d72b39c613943e7fdecd00d60c761c 100644 --- a/library/think/Request.php +++ b/library/think/Request.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2016 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- @@ -11,11 +11,6 @@ namespace think; -use think\Config; -use think\Exception; -use think\File; -use think\Session; - class Request { /** @@ -25,7 +20,7 @@ class Request protected $method; /** - * @var string 域名 + * @var string 域名(含协议和端口) */ protected $domain; @@ -64,6 +59,11 @@ class Request */ protected $routeInfo = []; + /** + * @var array 环境变量 + */ + protected $env; + /** * @var array 当前调度信息 */ @@ -93,20 +93,18 @@ class Request * @var array 资源类型 */ protected $mimeType = [ - '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', - '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', + 'image' => 'image/png,image/jpg,image/jpeg,image/pjpeg,image/gif,image/webp,image/*', + 'csv' => 'text/csv', + 'html' => 'text/html,application/xhtml+xml,*/*', ]; protected $content; @@ -121,13 +119,20 @@ class Request protected $input; // 请求缓存 protected $cache; + // 缓存是否检查 + protected $isCheckCache; + /** + * 是否合并Param + * @var bool + */ + protected $mergeParam = false; /** - * 架构函数 - * @access public + * 构造函数 + * @access protected * @param array $options 参数 */ - public function __construct($options = []) + protected function __construct($options = []) { foreach ($options as $name => $item) { if (property_exists($this, $name)) { @@ -137,6 +142,7 @@ class Request if (is_null($this->filter)) { $this->filter = Config::get('default_filter'); } + // 保存 php://input $this->input = file_get_contents('php://input'); } @@ -154,8 +160,8 @@ class Request /** * Hook 方法注入 * @access public - * @param string|array $method 方法名 - * @param mixed $callback callable + * @param string|array $method 方法名 + * @param mixed $callback callable * @return void */ public static function hook($method, $callback = null) @@ -181,16 +187,28 @@ class Request return self::$instance; } + /** + * 销毁当前请求对象 + * @access public + * @return void + */ + public static function destroy() + { + if (!is_null(self::$instance)) { + self::$instance = null; + } + } + /** * 创建一个URL请求 * @access public - * @param string $uri URL地址 - * @param string $method 请求类型 - * @param array $params 请求参数 - * @param array $cookie - * @param array $files - * @param array $server - * @param string $content + * @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 = [], $content = null) @@ -224,13 +242,14 @@ class Request if (!isset($info['path'])) { $info['path'] = '/'; } - $options = []; - $queryString = ''; + $options = []; + $options[strtolower($method)] = $params; + $queryString = ''; if (isset($info['query'])) { parse_str(html_entity_decode($info['query']), $query); if (!empty($params)) { $params = array_replace($query, $params); - $queryString = http_build_query($query, '', '&'); + $queryString = http_build_query($params, '', '&'); } else { $params = $query; $queryString = $info['query']; @@ -238,6 +257,11 @@ class Request } elseif (!empty($params)) { $queryString = http_build_query($params, '', '&'); } + if ($queryString) { + parse_str($queryString, $get); + $options['get'] = isset($options['get']) ? array_merge($get, $options['get']) : $get; + } + $server['REQUEST_URI'] = $info['path'] . ('' !== $queryString ? '?' . $queryString : ''); $server['QUERY_STRING'] = $queryString; $options['cookie'] = $cookie; @@ -248,14 +272,14 @@ class Request $options['baseUrl'] = $info['path']; $options['pathinfo'] = '/' == $info['path'] ? '/' : ltrim($info['path'], '/'); $options['method'] = $server['REQUEST_METHOD']; - $options['domain'] = $info['scheme'] . '://' . $server['HTTP_HOST']; + $options['domain'] = isset($info['scheme']) ? $info['scheme'] . '://' . $server['HTTP_HOST'] : ''; $options['content'] = $content; self::$instance = new self($options); return self::$instance; } /** - * 获取当前包含协议的域名 + * 设置或获取当前包含协议的域名 * @access public * @param string $domain 域名 * @return string @@ -272,7 +296,7 @@ class Request } /** - * 获取当前完整URL 包括QUERY_STRING + * 设置或获取当前完整URL 包括QUERY_STRING * @access public * @param string|true $url URL地址 true 带域名获取 * @return string @@ -299,7 +323,7 @@ class Request } /** - * 获取当前URL 不含QUERY_STRING + * 设置或获取当前URL 不含QUERY_STRING * @access public * @param string $url URL地址 * @return string @@ -317,7 +341,7 @@ class Request } /** - * 获取当前执行的文件 SCRIPT_NAME + * 设置或获取当前执行的文件 SCRIPT_NAME * @access public * @param string $file 当前执行的文件 * @return string @@ -349,7 +373,7 @@ class Request } /** - * 获取URL访问根地址 + * 设置或获取URL访问根地址 * @access public * @param string $url URL地址 * @return string @@ -453,7 +477,7 @@ class Request */ public function type() { - $accept = isset($this->server['HTTP_ACCEPT']) ? $this->server['HTTP_ACCEPT'] : $_SERVER['HTTP_ACCEPT']; + $accept = $this->server('HTTP_ACCEPT'); if (empty($accept)) { return false; } @@ -472,8 +496,8 @@ 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 = '') @@ -488,22 +512,28 @@ class Request /** * 当前的请求类型 * @access public - * @param bool $method true 获取原始请求类型 + * @param bool $method true 获取原始请求类型 * @return string */ public function method($method = false) { if (true === $method) { // 获取原始请求类型 - return IS_CLI ? 'GET' : (isset($this->server['REQUEST_METHOD']) ? $this->server['REQUEST_METHOD'] : $_SERVER['REQUEST_METHOD']); + return $this->server('REQUEST_METHOD') ?: 'GET'; } elseif (!$this->method) { if (isset($_POST[Config::get('var_method')])) { - $this->method = strtoupper($_POST[Config::get('var_method')]); - $this->{$this->method}($_POST); + $method = strtoupper($_POST[Config::get('var_method')]); + if (in_array($method, ['GET', 'POST', 'DELETE', 'PUT', 'PATCH'])) { + $this->method = $method; + $this->{$this->method}($_POST); + } else { + $this->method = 'POST'; + } + unset($_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']); + $this->method = $this->server('REQUEST_METHOD') ?: 'GET'; } } return $this->method; @@ -600,16 +630,16 @@ class Request } /** - * 设置获取获取当前请求的参数 + * 获取当前请求的参数 * @access public - * @param string|array $name 变量名 - * @param mixed $default 默认值 - * @param string|array $filter 过滤方法 + * @param string|array $name 变量名 + * @param mixed $default 默认值 + * @param string|array $filter 过滤方法 * @return mixed */ - public function param($name = '', $default = null, $filter = null) + public function param($name = '', $default = null, $filter = '') { - if (empty($this->param)) { + if (empty($this->mergeParam)) { $method = $this->method(true); // 自动获取请求变量 switch ($method) { @@ -625,94 +655,104 @@ class Request $vars = []; } // 当前请求参数和URL地址中的参数合并 - $this->param = array_merge($this->get(false), $vars, $this->route(false)); + $this->param = array_merge($this->param, $this->get(false), $vars, $this->route(false)); + $this->mergeParam = true; } if (true === $name) { // 获取包含文件上传信息的数组 $file = $this->file(); - $data = array_merge($this->param, $file); + $data = is_array($file) ? array_merge($this->param, $file) : $this->param; return $this->input($data, '', $default, $filter); } return $this->input($this->param, $name, $default, $filter); } /** - * 设置获取获取路由参数 + * 设置获取路由参数 * @access public - * @param string|array $name 变量名 - * @param mixed $default 默认值 - * @param string|array $filter 过滤方法 + * @param string|array $name 变量名 + * @param mixed $default 默认值 + * @param string|array $filter 过滤方法 * @return mixed */ - public function route($name = '', $default = null, $filter = null) + public function route($name = '', $default = null, $filter = '') { if (is_array($name)) { $this->param = []; + $this->mergeParam = false; return $this->route = array_merge($this->route, $name); } return $this->input($this->route, $name, $default, $filter); } /** - * 设置获取获取GET参数 + * 设置获取GET参数 * @access public - * @param string|array $name 变量名 - * @param mixed $default 默认值 - * @param string|array $filter 过滤方法 + * @param string|array $name 变量名 + * @param mixed $default 默认值 + * @param string|array $filter 过滤方法 * @return mixed */ - public function get($name = '', $default = null, $filter = null) + public function get($name = '', $default = null, $filter = '') { if (empty($this->get)) { $this->get = $_GET; } if (is_array($name)) { $this->param = []; + $this->mergeParam = false; return $this->get = array_merge($this->get, $name); } return $this->input($this->get, $name, $default, $filter); } /** - * 设置获取获取POST参数 + * 设置获取POST参数 * @access public - * @param string $name 变量名 - * @param mixed $default 默认值 - * @param string|array $filter 过滤方法 + * @param string $name 变量名 + * @param mixed $default 默认值 + * @param string|array $filter 过滤方法 * @return mixed */ - public function post($name = '', $default = null, $filter = null) + public function post($name = '', $default = null, $filter = '') { if (empty($this->post)) { - $this->post = $_POST; + $content = $this->input; + if (empty($_POST) && false !== strpos($this->contentType(), 'application/json')) { + $this->post = (array) json_decode($content, true); + } else { + $this->post = $_POST; + } } if (is_array($name)) { $this->param = []; + $this->mergeParam = false; return $this->post = array_merge($this->post, $name); } return $this->input($this->post, $name, $default, $filter); } /** - * 设置获取获取PUT参数 + * 设置获取PUT参数 * @access public - * @param string|array $name 变量名 - * @param mixed $default 默认值 - * @param string|array $filter 过滤方法 + * @param string|array $name 变量名 + * @param mixed $default 默认值 + * @param string|array $filter 过滤方法 * @return mixed */ - public function put($name = '', $default = null, $filter = null) + public function put($name = '', $default = null, $filter = '') { if (is_null($this->put)) { $content = $this->input; - if (strpos($content, '":')) { - $this->put = json_decode($content, true); + if (false !== strpos($this->contentType(), 'application/json')) { + $this->put = (array) json_decode($content, true); } else { parse_str($content, $this->put); } } if (is_array($name)) { $this->param = []; + $this->mergeParam = false; return $this->put = is_null($this->put) ? $name : array_merge($this->put, $name); } @@ -720,45 +760,46 @@ class Request } /** - * 设置获取获取DELETE参数 + * 设置获取DELETE参数 * @access public - * @param string|array $name 变量名 - * @param mixed $default 默认值 - * @param string|array $filter 过滤方法 + * @param string|array $name 变量名 + * @param mixed $default 默认值 + * @param string|array $filter 过滤方法 * @return mixed */ - public function delete($name = '', $default = null, $filter = null) + public function delete($name = '', $default = null, $filter = '') { return $this->put($name, $default, $filter); } /** - * 设置获取获取PATCH参数 + * 设置获取PATCH参数 * @access public - * @param string|array $name 变量名 - * @param mixed $default 默认值 - * @param string|array $filter 过滤方法 + * @param string|array $name 变量名 + * @param mixed $default 默认值 + * @param string|array $filter 过滤方法 * @return mixed */ - public function patch($name = '', $default = null, $filter = null) + public function patch($name = '', $default = null, $filter = '') { return $this->put($name, $default, $filter); } /** * 获取request变量 - * @param string $name 数据名称 - * @param string $default 默认值 - * @param string|array $filter 过滤方法 + * @param string $name 数据名称 + * @param string $default 默认值 + * @param string|array $filter 过滤方法 * @return mixed */ - public function request($name = '', $default = null, $filter = null) + public function request($name = '', $default = null, $filter = '') { if (empty($this->request)) { $this->request = $_REQUEST; } if (is_array($name)) { $this->param = []; + $this->mergeParam = false; return $this->request = array_merge($this->request, $name); } return $this->input($this->request, $name, $default, $filter); @@ -767,12 +808,12 @@ class Request /** * 获取session数据 * @access public - * @param string|array $name 数据名称 - * @param string $default 默认值 - * @param string|array $filter 过滤方法 + * @param string|array $name 数据名称 + * @param string $default 默认值 + * @param string|array $filter 过滤方法 * @return mixed */ - public function session($name = '', $default = null, $filter = null) + public function session($name = '', $default = null, $filter = '') { if (empty($this->session)) { $this->session = Session::get(); @@ -786,31 +827,45 @@ class Request /** * 获取cookie参数 * @access public - * @param string|array $name 数据名称 - * @param string $default 默认值 - * @param string|array $filter 过滤方法 + * @param string|array $name 数据名称 + * @param string $default 默认值 + * @param string|array $filter 过滤方法 * @return mixed */ - public function cookie($name = '', $default = null, $filter = null) + public function cookie($name = '', $default = null, $filter = '') { if (empty($this->cookie)) { - $this->cookie = $_COOKIE; + $this->cookie = Cookie::get(); } if (is_array($name)) { return $this->cookie = array_merge($this->cookie, $name); + } elseif (!empty($name)) { + $data = Cookie::has($name) ? Cookie::get($name) : $default; + } else { + $data = $this->cookie; } - return $this->input($this->cookie, $name, $default, $filter); + + // 解析过滤器 + $filter = $this->getFilter($filter, $default); + + if (is_array($data)) { + array_walk_recursive($data, [$this, 'filterValue'], $filter); + reset($data); + } else { + $this->filterValue($data, $name, $filter); + } + return $data; } /** * 获取server参数 * @access public - * @param string|array $name 数据名称 - * @param string $default 默认值 - * @param string|array $filter 过滤方法 + * @param string|array $name 数据名称 + * @param string $default 默认值 + * @param string|array $filter 过滤方法 * @return mixed */ - public function server($name = '', $default = null, $filter = null) + public function server($name = '', $default = null, $filter = '') { if (empty($this->server)) { $this->server = $_SERVER; @@ -845,7 +900,7 @@ class Request $keys = array_keys($file); $count = count($file['name']); for ($i = 0; $i < $count; $i++) { - if (empty($file['tmp_name'][$i])) { + if (empty($file['tmp_name'][$i]) || !is_file($file['tmp_name'][$i])) { continue; } $temp['key'] = $key; @@ -859,7 +914,7 @@ class Request if ($file instanceof File) { $array[$key] = $file; } else { - if (empty($file['tmp_name'])) { + if (empty($file['tmp_name']) || !is_file($file['tmp_name'])) { continue; } $array[$key] = (new File($file['tmp_name']))->setUploadInfo($file); @@ -878,17 +933,17 @@ class Request return $array[$name]; } } - return null; + return; } /** * 获取环境变量 - * @param string|array $name 数据名称 - * @param string $default 默认值 - * @param string|array $filter 过滤方法 + * @param string|array $name 数据名称 + * @param string $default 默认值 + * @param string|array $filter 过滤方法 * @return mixed */ - public function env($name = '', $default = null, $filter = null) + public function env($name = '', $default = null, $filter = '') { if (empty($this->env)) { $this->env = $_ENV; @@ -902,26 +957,30 @@ class Request /** * 设置或者获取当前的Header * @access public - * @param string|array $name header名称 - * @param string $default 默认值 + * @param string|array $name header名称 + * @param string $default 默认值 * @return string */ public function header($name = '', $default = null) { if (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 (function_exists('apache_request_headers') && $result = apache_request_headers()) { + $header = $result; + } else { + $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']; } - } - 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); } @@ -937,13 +996,13 @@ class Request /** * 获取变量 支持过滤和默认值 - * @param array $data 数据源 - * @param string|false $name 字段名 - * @param mixed $default 默认值 - * @param string|array $filter 过滤函数 + * @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) + public function input($data = [], $name = '', $default = null, $filter = '') { if (false === $name) { // 获取原始数据 @@ -972,14 +1031,8 @@ class Request } // 解析过滤器 - $filter = $filter ?: $this->filter; + $filter = $this->getFilter($filter, $default); - if (is_string($filter)) { - $filter = explode(',', $filter); - } else { - $filter = (array) $filter; - } - $filter[] = $default; if (is_array($data)) { array_walk_recursive($data, [$this, 'filterValue'], $filter); reset($data); @@ -1008,11 +1061,28 @@ class Request } } + protected function getFilter($filter, $default) + { + if (is_null($filter)) { + $filter = []; + } else { + $filter = $filter ?: $this->filter; + if (is_string($filter) && false === strpos($filter, '/')) { + $filter = explode(',', $filter); + } else { + $filter = (array) $filter; + } + } + + $filter[] = $default; + return $filter; + } + /** * 递归过滤给定的值 - * @param mixed $value 键值 - * @param mixed $key 键名 - * @param array $filters 过滤方法+默认值 + * @param mixed $value 键值 + * @param mixed $key 键名 + * @param array $filters 过滤方法+默认值 * @return mixed */ private function filterValue(&$value, $key, $filters) @@ -1023,7 +1093,7 @@ class Request // 调用函数或者方法过滤 $value = call_user_func($filter, $value); } elseif (is_scalar($value)) { - if (strpos($filter, '/')) { + if (false !== strpos($filter, '/')) { // 正则过滤 if (!preg_match($filter, $value)) { // 匹配不成功返回默认值 @@ -1052,7 +1122,7 @@ class Request 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)) { + if (is_string($value) && preg_match('/^(EXP|NEQ|GT|EGT|LT|ELT|OR|XOR|LIKE|NOTLIKE|NOT LIKE|NOT BETWEEN|NOTBETWEEN|BETWEEN|NOT EXISTS|NOTEXISTS|EXISTS|NOT NULL|NOTNULL|NULL|BETWEEN TIME|NOT BETWEEN TIME|NOTBETWEEN TIME|NOTIN|NOT IN|IN)$/i', $value)) { $value .= ' '; } // TODO 其他安全过滤 @@ -1097,9 +1167,9 @@ class Request /** * 是否存在某个请求参数 * @access public - * @param string $name 变量名 - * @param string $type 变量类型 - * @param bool $checkEmpty 是否检测空值 + * @param string $name 变量名 + * @param string $type 变量类型 + * @param bool $checkEmpty 是否检测空值 * @return mixed */ public function has($name, $type = 'param', $checkEmpty = false) @@ -1123,8 +1193,8 @@ class Request /** * 获取指定的参数 * @access public - * @param string|array $name 变量名 - * @param string $type 变量类型 + * @param string|array $name 变量名 + * @param string $type 变量类型 * @return mixed */ public function only($name, $type = 'param') @@ -1145,8 +1215,8 @@ class Request /** * 排除指定参数获取 * @access public - * @param string|array $name 变量名 - * @param string $type 变量类型 + * @param string|array $name 变量名 + * @param string $type 变量类型 * @return mixed */ public function except($name, $type = 'param') @@ -1179,6 +1249,8 @@ class Request return true; } elseif (isset($server['HTTP_X_FORWARDED_PROTO']) && 'https' == $server['HTTP_X_FORWARDED_PROTO']) { return true; + } elseif (Config::get('https_agent_name') && isset($server[Config::get('https_agent_name')])) { + return true; } return false; } @@ -1186,7 +1258,7 @@ class Request /** * 当前是否Ajax请求 * @access public - * @param bool $ajax true 获取原始ajax请求 + * @param bool $ajax true 获取原始ajax请求 * @return bool */ public function isAjax($ajax = false) @@ -1196,14 +1268,16 @@ class Request if (true === $ajax) { return $result; } else { - return $this->param(Config::get('var_ajax')) ? true : $result; + $result = $this->param(Config::get('var_ajax')) ? true : $result; + $this->mergeParam = false; + return $result; } } /** * 当前是否Pjax请求 * @access public - * @param bool $pjax true 获取原始pjax请求 + * @param bool $pjax true 获取原始pjax请求 * @return bool */ public function isPjax($pjax = false) @@ -1212,17 +1286,19 @@ class Request if (true === $pjax) { return $result; } else { - return $this->param(Config::get('var_pjax')) ? true : $result; + $result = $this->param(Config::get('var_pjax')) ? true : $result; + $this->mergeParam = false; + return $result; } } /** * 获取客户端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) + public function ip($type = 0, $adv = true) { $type = $type ? 1 : 0; static $ip = null; @@ -1230,15 +1306,18 @@ class Request return $ip[$type]; } - if ($adv) { + $httpAgentIp = Config::get('http_agent_ip'); + + if ($httpAgentIp && isset($_SERVER[$httpAgentIp])) { + $ip = $_SERVER[$httpAgentIp]; + } elseif ($adv) { if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) { $arr = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']); $pos = array_search('unknown', $arr); if (false !== $pos) { unset($arr[$pos]); } - - $ip = trim($arr[0]); + $ip = trim(current($arr)); } elseif (isset($_SERVER['HTTP_CLIENT_IP'])) { $ip = $_SERVER['HTTP_CLIENT_IP']; } elseif (isset($_SERVER['REMOTE_ADDR'])) { @@ -1249,7 +1328,7 @@ class Request } // IP地址合法验证 $long = sprintf("%u", ip2long($ip)); - $ip = $long ? array($ip, $long) : array('0.0.0.0', 0); + $ip = $long ? [$ip, $long] : ['0.0.0.0', 0]; return $ip[$type]; } @@ -1296,11 +1375,18 @@ class Request /** * 当前请求的host * @access public + * @param bool $strict true 仅仅获取HOST * @return string */ - public function host() + public function host($strict = false) { - return $this->server('HTTP_HOST'); + if (isset($_SERVER['HTTP_X_REAL_HOST'])) { + $host = $_SERVER['HTTP_X_REAL_HOST']; + } else { + $host = $this->server('HTTP_HOST'); + } + + return true === $strict && strpos($host, ':') ? strstr($host, ':', true) : $host; } /** @@ -1333,6 +1419,25 @@ class Request return $this->server('REMOTE_PORT'); } + /** + * 当前请求 HTTP_CONTENT_TYPE + * @access public + * @return string + */ + public function contentType() + { + $contentType = $this->server('CONTENT_TYPE'); + if ($contentType) { + if (strpos($contentType, ';')) { + list($type) = explode(';', $contentType); + } else { + $type = $contentType; + } + return trim($type); + } + return ''; + } + /** * 获取当前请求的路由信息 * @access public @@ -1351,7 +1456,7 @@ class Request /** * 设置或者获取当前请求的调度信息 * @access public - * @param array $dispatch 调度信息 + * @param array $dispatch 调度信息 * @return array */ public function dispatch($dispatch = null) @@ -1366,7 +1471,7 @@ class Request * 设置或者获取当前的模块名 * @access public * @param string $module 模块名 - * @return string|$this + * @return string|Request */ public function module($module = null) { @@ -1382,7 +1487,7 @@ class Request * 设置或者获取当前的控制器名 * @access public * @param string $controller 控制器名 - * @return string|$this + * @return string|Request */ public function controller($controller = null) { @@ -1398,15 +1503,16 @@ class Request * 设置或者获取当前的操作名 * @access public * @param string $action 操作名 - * @return string + * @return string|Request */ public function action($action = null) { - if (!is_null($action)) { + if (!is_null($action) && !is_bool($action)) { $this->action = $action; return $this; } else { - return $this->action ?: ''; + $name = $this->action ?: ''; + return true === $action ? $name : strtolower($name); } } @@ -1414,7 +1520,7 @@ class Request * 设置或者获取当前的语言 * @access public * @param string $lang 语言名 - * @return string + * @return string|Request */ public function langset($lang = null) { @@ -1468,15 +1574,46 @@ class Request } /** - * 读取或者设置缓存 + * 设置当前地址的请求缓存 * @access public - * @param string $key 缓存标识,支持变量规则 ,例如 item/:name/:id + * @param string $key 缓存标识,支持变量规则 ,例如 item/:name/:id * @param mixed $expire 缓存有效期 - * @return mixed + * @param array $except 缓存排除 + * @param string $tag 缓存标签 + * @return void */ - public function cache($key, $expire = null) + public function cache($key, $expire = null, $except = [], $tag = null) { - if ($this->isGet()) { + if (!is_array($except)) { + $tag = $except; + $except = []; + } + + if (false !== $key && $this->isGet() && !$this->isCheckCache) { + // 标记请求缓存检查 + $this->isCheckCache = true; + if (false === $expire) { + // 关闭当前缓存 + return; + } + if ($key instanceof \Closure) { + $key = call_user_func_array($key, [$this]); + } elseif (true === $key) { + foreach ($except as $rule) { + if (0 === stripos($this->url(), $rule)) { + return; + } + } + // 自动缓存功能 + $key = '__URL__'; + } elseif (strpos($key, '|')) { + list($key, $fun) = explode('|', $key); + } + // 特殊规则替换 + if (false !== strpos($key, '__')) { + $key = str_replace(['__MODULE__', '__CONTROLLER__', '__ACTION__', '__URL__', ''], [$this->module, $this->controller, $this->action, md5($this->url(true))], $key); + } + if (false !== strpos($key, ':')) { $param = $this->param(); foreach ($param as $item => $val) { @@ -1484,9 +1621,6 @@ class Request $key = str_replace(':' . $item, $val, $key); } } - } elseif ('__URL__' == $key) { - // 当前URL地址作为缓存标识 - $key = md5($this->url()); } elseif (strpos($key, ']')) { if ('[' . $this->ext() . ']' == $key) { // 缓存某个后缀的请求 @@ -1495,6 +1629,9 @@ class Request return; } } + if (isset($fun)) { + $key = $fun($key); + } if (strtotime($this->server('HTTP_IF_MODIFIED_SINCE')) + $expire > $_SERVER['REQUEST_TIME']) { // 读取缓存 @@ -1505,13 +1642,13 @@ class Request $response = Response::create($content)->header($header); throw new \think\exception\HttpResponseException($response); } else { - $this->cache = [$key, $expire]; + $this->cache = [$key, $expire, $tag]; } } } /** - * 读取缓存设置 + * 读取请求缓存设置 * @access public * @return array */ @@ -1523,8 +1660,8 @@ class Request /** * 设置当前请求绑定的对象实例 * @access public - * @param string $name 绑定的对象标识 - * @param mixed $obj 绑定的对象实例 + * @param string|array $name 绑定的对象标识 + * @param mixed $obj 绑定的对象实例 * @return mixed */ public function bind($name, $obj = null) diff --git a/library/think/Response.php b/library/think/Response.php index 8b62e4199adf3a493fd110c96cd70deb9ce3bc6d..c5c152093a2101388104bd5a91e3fcf45dedb775 100644 --- a/library/think/Response.php +++ b/library/think/Response.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2016 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- @@ -11,11 +11,6 @@ namespace think; -use think\Cache; -use think\Config; -use think\Debug; -use think\Env; -use think\Request; use think\response\Json as JsonResponse; use think\response\Jsonp as JsonpResponse; use think\response\Redirect as RedirectResponse; @@ -24,7 +19,6 @@ use think\response\Xml as XmlResponse; class Response { - // 原始数据 protected $data; @@ -45,7 +39,7 @@ class Response protected $content = null; /** - * 架构函数 + * 构造函数 * @access public * @param mixed $data 输出数据 * @param int $code @@ -55,12 +49,12 @@ class Response 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); + $this->header = array_merge($this->header, $header); + $this->code = $code; } /** @@ -75,9 +69,7 @@ class Response */ 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); + $class = false !== strpos($type, '\\') ? $type : '\\think\\response\\' . ucfirst(strtolower($type)); if (class_exists($class)) { $response = new $class($data, $code, $header, $options); } else { @@ -95,6 +87,9 @@ class Response */ public function send() { + // 监听response_send + Hook::listen('response_send', $this); + // 处理输出数据 $data = $this->getContent(); @@ -103,24 +98,29 @@ class Response Debug::inject($this, $data); } + if (200 == $this->code) { + $cache = Request::instance()->getCache(); + if ($cache) { + $this->header['Cache-Control'] = 'max-age=' . $cache[1] . ',must-revalidate'; + $this->header['Last-Modified'] = gmdate('D, d M Y H:i:s') . ' GMT'; + $this->header['Expires'] = gmdate('D, d M Y H:i:s', $_SERVER['REQUEST_TIME'] + $cache[1]) . ' GMT'; + Cache::tag($cache[2])->set($cache[0], [$data, $this->header], $cache[1]); + } + } + if (!headers_sent() && !empty($this->header)) { // 发送状态码 http_response_code($this->code); // 发送头部信息 foreach ($this->header as $name => $val) { - header($name . ':' . $val); - } - } - if (200 == $this->code) { - $cache = Request::instance()->getCache(); - if ($cache) { - header('Cache-Control: max-age=' . $cache[1] . ',must-revalidate'); - header('Last-Modified:' . gmdate('D, d M Y H:i:s') . ' GMT'); - header('Expires:' . gmdate('D, d M Y H:i:s', $_SERVER['REQUEST_TIME'] + $cache[1]) . ' GMT'); - $header['Content-Type'] = $this->header['Content-Type']; - Cache::set($cache[0], [$data, $header], $cache[1]); + if (is_null($val)) { + header($name); + } else { + header($name . ':' . $val); + } } } + echo $data; if (function_exists('fastcgi_finish_request')) { @@ -130,6 +130,11 @@ class Response // 监听response_end Hook::listen('response_end', $this); + + // 清空当次请求有效的数据 + if (!($this instanceof RedirectResponse)) { + Session::flush(); + } } /** @@ -278,7 +283,11 @@ class Response */ public function getHeader($name = '') { - return !empty($name) ? $this->header[$name] : $this->header; + if (!empty($name)) { + return isset($this->header[$name]) ? $this->header[$name] : null; + } else { + return $this->header; + } } /** diff --git a/library/think/Route.php b/library/think/Route.php index c18a2d6762062285a605edbcef06535dc0ee065b..ab53aa200a564fbf39ea7f54f7b0fb25f548cf98 100644 --- a/library/think/Route.php +++ b/library/think/Route.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2016 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- @@ -11,26 +11,19 @@ namespace think; -use think\App; -use think\Config; use think\exception\HttpException; -use think\Hook; -use think\Loader; -use think\Log; -use think\Request; -use think\Response; class Route { // 路由规则 private static $rules = [ - 'GET' => [], - 'POST' => [], - 'PUT' => [], - 'DELETE' => [], - 'PATCH' => [], - 'HEAD' => [], - 'OPTIONS' => [], + 'get' => [], + 'post' => [], + 'put' => [], + 'delete' => [], + 'patch' => [], + 'head' => [], + 'options' => [], '*' => [], 'alias' => [], 'domain' => [], @@ -40,21 +33,22 @@ class Route // REST路由操作方法定义 private static $rest = [ - 'index' => ['GET', '', 'index'], - 'create' => ['GET', '/create', 'create'], - 'edit' => ['GET', '/:id/edit', 'edit'], - 'read' => ['GET', '/:id', 'read'], - 'save' => ['POST', '', 'save'], - 'update' => ['PUT', '/:id', 'update'], - 'delete' => ['DELETE', '/:id', 'delete'], + 'index' => ['get', '', 'index'], + 'create' => ['get', '/create', 'create'], + 'edit' => ['get', '/:id/edit', 'edit'], + 'read' => ['get', '/:id', 'read'], + 'save' => ['post', '', 'save'], + 'update' => ['put', '/:id', 'update'], + 'delete' => ['delete', '/:id', 'delete'], ]; // 不同请求类型的方法前缀 private static $methodPrefix = [ - 'GET' => 'get', - 'POST' => 'post', - 'PUT' => 'put', - 'DELETE' => 'delete', + 'get' => 'get', + 'post' => 'post', + 'put' => 'put', + 'delete' => 'delete', + 'patch' => 'patch', ]; // 子域名 @@ -68,12 +62,14 @@ class Route private static $domainRule; // 当前域名 private static $domain; + // 当前路由执行过程中的参数 + private static $option = []; /** * 注册变量规则 * @access public - * @param string|array $name 变量名 - * @param string $rule 变量规则 + * @param string|array $name 变量名 + * @param string $rule 变量规则 * @return void */ public static function pattern($name = null, $rule = '') @@ -88,10 +84,10 @@ class Route /** * 注册子域名部署规则 * @access public - * @param string|array $domain 子域名 - * @param mixed $rule 路由规则 - * @param array $option 路由参数 - * @param array $pattern 变量规则 + * @param string|array $domain 子域名 + * @param mixed $rule 路由规则 + * @param array $option 路由参数 + * @param array $pattern 变量规则 * @return void */ public static function domain($domain, $rule = '', $option = [], $pattern = []) @@ -125,8 +121,8 @@ class Route /** * 设置路由绑定 * @access public - * @param mixed $bind 绑定信息 - * @param string $type 绑定类型 默认为module 支持 namespace class + * @param mixed $bind 绑定信息 + * @param string $type 绑定类型 默认为module 支持 namespace class controller * @return mixed */ public static function bind($bind, $type = 'module') @@ -137,8 +133,8 @@ class Route /** * 设置或者获取路由标识 * @access public - * @param string|array $name 路由命名标识 数组表示批量设置 - * @param array $value 路由地址及变量信息 + * @param string|array $name 路由命名标识 数组表示批量设置 + * @param array $value 路由地址及变量信息 * @return array */ public static function name($name = '', $value = null) @@ -148,8 +144,9 @@ class Route } elseif ('' === $name) { return self::$rules['name']; } elseif (!is_null($value)) { - self::$rules['name'][$name][] = $value; + self::$rules['name'][strtolower($name)][] = $value; } else { + $name = strtolower($name); return isset(self::$rules['name'][$name]) ? self::$rules['name'][$name] : null; } } @@ -157,7 +154,7 @@ class Route /** * 读取路由绑定 * @access public - * @param string $type 绑定类型 + * @param string $type 绑定类型 * @return mixed */ public static function getBind($type) @@ -168,8 +165,8 @@ class Route /** * 导入配置文件的路由规则 * @access public - * @param array $rule 路由规则 - * @param string $type 请求类型 + * @param array $rule 路由规则 + * @param string $type 请求类型 * @return void */ public static function import(array $rule, $type = '*') @@ -198,7 +195,7 @@ class Route unset($rule['__rest__']); } - self::registerRules($rule, strtoupper($type)); + self::registerRules($rule, strtolower($type)); } // 批量注册路由 @@ -225,23 +222,24 @@ class Route /** * 注册路由规则 * @access public - * @param string $rule 路由规则 - * @param string $route 路由地址 - * @param string $type 请求类型 - * @param array $option 路由参数 - * @param array $pattern 变量规则 + * @param string|array $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::getGroup('name'); + if (!is_null($group)) { // 路由分组 $option = array_merge(self::getGroup('option'), $option); $pattern = array_merge(self::getGroup('pattern'), $pattern); } - $type = strtoupper($type); + $type = strtolower($type); if (strpos($type, '|')) { $option['method'] = $type; @@ -257,9 +255,11 @@ class Route $option1 = array_merge($option, $val[1]); $pattern1 = array_merge($pattern, isset($val[2]) ? $val[2] : []); } else { - $route = $val; + $option1 = null; + $pattern1 = null; + $route = $val; } - self::setRule($key, $route, $type, isset($option1) ? $option1 : $option, isset($pattern1) ? $pattern1 : $pattern, $group); + self::setRule($key, $route, $type, !is_null($option1) ? $option1 : $option, !is_null($pattern1) ? $pattern1 : $pattern, $group); } } else { self::setRule($rule, $route, $type, $option, $pattern, $group); @@ -270,12 +270,12 @@ class Route /** * 设置路由规则 * @access public - * @param string $rule 路由规则 - * @param string $route 路由地址 - * @param string $type 请求类型 - * @param array $option 路由参数 - * @param array $pattern 变量规则 - * @param string $group 所属分组 + * @param string|array $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 = '') @@ -292,20 +292,27 @@ class Route } elseif ('$' == substr($rule, -1, 1)) { // 是否完整匹配 $option['complete_match'] = true; - $rule = substr($rule, 0, -1); } } elseif (empty($option['complete_match']) && '$' == substr($rule, -1, 1)) { // 是否完整匹配 $option['complete_match'] = true; - $rule = substr($rule, 0, -1); } - if ('/' != $rule) { + if ('$' == substr($rule, -1, 1)) { + $rule = substr($rule, 0, -1); + } + + if ('/' != $rule || $group) { $rule = trim($rule, '/'); } $vars = self::parseVar($rule); if (isset($name)) { - self::name($name, [$rule, $vars, self::$domain]); + $key = $group ? $group . ($rule ? '/' . $rule : '') : $rule; + $suffix = isset($option['ext']) ? $option['ext'] : null; + self::name($name, [$key, $vars, self::$domain, $suffix]); + } + if (isset($option['modular'])) { + $route = $option['modular'] . '/' . $route; } if ($group) { if ('*' != $type) { @@ -327,10 +334,10 @@ class Route } if ('*' == $type) { // 注册路由快捷方式 - foreach (['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS'] as $method) { - if (self::$domain) { + foreach (['get', 'post', 'put', 'delete', 'patch', 'head', 'options'] as $method) { + if (self::$domain && !isset(self::$rules['domain'][self::$domain][$method][$rule])) { self::$rules['domain'][self::$domain][$method][$rule] = true; - } else { + } elseif (!self::$domain && !isset(self::$rules[$method][$rule])) { self::$rules[$method][$rule] = true; } } @@ -338,10 +345,31 @@ class Route } } + /** + * 设置当前执行的参数信息 + * @access public + * @param array $options 参数信息 + * @return mixed + */ + protected static function setOption($options = []) + { + self::$option[] = $options; + } + + /** + * 获取当前执行的所有参数信息 + * @access public + * @return array + */ + public static function getOption() + { + return self::$option; + } + /** * 获取当前的分组信息 * @access public - * @param string $type 分组信息名称 name option pattern + * @param string $type 分组信息名称 name option pattern * @return mixed */ public static function getGroup($type) @@ -356,9 +384,9 @@ class Route /** * 设置当前的路由分组 * @access public - * @param string $name 分组名称 - * @param array $option 分组路由参数 - * @param array $pattern 分组变量规则 + * @param string $name 分组名称 + * @param array $option 分组路由参数 + * @param array $pattern 分组变量规则 * @return void */ public static function setGroup($name, $option = [], $pattern = []) @@ -371,10 +399,10 @@ class Route /** * 注册路由分组 * @access public - * @param string|array $name 分组名称或者参数 - * @param array|\Closure $routes 路由地址 - * @param array $option 路由参数 - * @param array $pattern 变量规则 + * @param string|array $name 分组名称或者参数 + * @param array|\Closure $routes 路由地址 + * @param array $option 路由参数 + * @param array $pattern 变量规则 * @return void */ public static function group($name, $routes, $option = [], $pattern = []) @@ -402,7 +430,8 @@ class Route self::$rules['*'][$name]['pattern'] = $pattern; } } else { - $item = []; + $item = []; + $completeMatch = Config::get('route_complete_match'); foreach ($routes as $key => $val) { if (is_numeric($key)) { $key = array_shift($val); @@ -421,16 +450,20 @@ class Route // 是否完整匹配 $options['complete_match'] = true; $key = substr($key, 0, -1); + } elseif ($completeMatch) { + $options['complete_match'] = true; } + $key = trim($key, '/'); $vars = self::parseVar($key); $item[] = ['rule' => $key, 'route' => $route, 'var' => $vars, 'option' => $options, 'pattern' => $patterns]; // 设置路由标识 - self::name($route, [$key, $vars, self::$domain]); + $suffix = isset($options['ext']) ? $options['ext'] : null; + self::name($route, [$name . ($key ? '/' . $key : ''), $vars, self::$domain, $suffix]); } self::$rules['*'][$name] = ['rule' => $item, 'route' => '', 'var' => [], 'option' => $option, 'pattern' => $pattern]; } - foreach (['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS'] as $method) { + foreach (['get', 'post', 'put', 'delete', 'patch', 'head', 'options'] as $method) { if (!isset(self::$rules[$method][$name])) { self::$rules[$method][$name] = true; } elseif (is_array(self::$rules[$method][$name])) { @@ -454,10 +487,10 @@ class Route /** * 注册路由 * @access public - * @param string $rule 路由规则 - * @param string $route 路由地址 - * @param array $option 路由参数 - * @param array $pattern 变量规则 + * @param string|array $rule 路由规则 + * @param string $route 路由地址 + * @param array $option 路由参数 + * @param array $pattern 变量规则 * @return void */ public static function any($rule, $route = '', $option = [], $pattern = []) @@ -468,10 +501,10 @@ class Route /** * 注册GET路由 * @access public - * @param string $rule 路由规则 - * @param string $route 路由地址 - * @param array $option 路由参数 - * @param array $pattern 变量规则 + * @param string|array $rule 路由规则 + * @param string $route 路由地址 + * @param array $option 路由参数 + * @param array $pattern 变量规则 * @return void */ public static function get($rule, $route = '', $option = [], $pattern = []) @@ -482,10 +515,10 @@ class Route /** * 注册POST路由 * @access public - * @param string $rule 路由规则 - * @param string $route 路由地址 - * @param array $option 路由参数 - * @param array $pattern 变量规则 + * @param string|array $rule 路由规则 + * @param string $route 路由地址 + * @param array $option 路由参数 + * @param array $pattern 变量规则 * @return void */ public static function post($rule, $route = '', $option = [], $pattern = []) @@ -496,10 +529,10 @@ class Route /** * 注册PUT路由 * @access public - * @param string $rule 路由规则 - * @param string $route 路由地址 - * @param array $option 路由参数 - * @param array $pattern 变量规则 + * @param string|array $rule 路由规则 + * @param string $route 路由地址 + * @param array $option 路由参数 + * @param array $pattern 变量规则 * @return void */ public static function put($rule, $route = '', $option = [], $pattern = []) @@ -510,10 +543,10 @@ class Route /** * 注册DELETE路由 * @access public - * @param string $rule 路由规则 - * @param string $route 路由地址 - * @param array $option 路由参数 - * @param array $pattern 变量规则 + * @param string|array $rule 路由规则 + * @param string $route 路由地址 + * @param array $option 路由参数 + * @param array $pattern 变量规则 * @return void */ public static function delete($rule, $route = '', $option = [], $pattern = []) @@ -524,10 +557,10 @@ class Route /** * 注册PATCH路由 * @access public - * @param string $rule 路由规则 - * @param string $route 路由地址 - * @param array $option 路由参数 - * @param array $pattern 变量规则 + * @param string|array $rule 路由规则 + * @param string $route 路由地址 + * @param array $option 路由参数 + * @param array $pattern 变量规则 * @return void */ public static function patch($rule, $route = '', $option = [], $pattern = []) @@ -538,10 +571,10 @@ class Route /** * 注册资源路由 * @access public - * @param string $rule 路由规则 - * @param string $route 路由地址 - * @param array $option 路由参数 - * @param array $pattern 变量规则 + * @param string|array $rule 路由规则 + * @param string $route 路由地址 + * @param array $option 路由参数 + * @param array $pattern 变量规则 * @return void */ public static function resource($rule, $route = '', $option = [], $pattern = []) @@ -575,7 +608,8 @@ class Route } elseif (strpos($val[1], ':id') && isset($option['var'][$rule])) { $val[1] = str_replace(':id', ':' . $option['var'][$rule], $val[1]); } - $item = ltrim($rule . $val[1], '/'); + $item = ltrim($rule . $val[1], '/'); + $option['rest'] = $key; self::rule($item . '$', $route . '/' . $val[2], $val[0], $option, $pattern); } } @@ -584,10 +618,10 @@ class Route /** * 注册控制器路由 操作方法对应不同的请求后缀 * @access public - * @param string $rule 路由规则 - * @param string $route 路由地址 - * @param array $option 路由参数 - * @param array $pattern 变量规则 + * @param string $rule 路由规则 + * @param string $route 路由地址 + * @param array $option 路由参数 + * @param array $pattern 变量规则 * @return void */ public static function controller($rule, $route = '', $option = [], $pattern = []) @@ -600,9 +634,9 @@ class Route /** * 注册别名路由 * @access public - * @param string|array $rule 路由别名 - * @param string $route 路由地址 - * @param array $option 路由参数 + * @param string|array $rule 路由别名 + * @param string $route 路由地址 + * @param array $option 路由参数 * @return void */ public static function alias($rule = null, $route = '', $option = []) @@ -617,30 +651,30 @@ class Route /** * 设置不同请求类型下面的方法前缀 * @access public - * @param string $method 请求类型 - * @param string $prefix 类型前缀 + * @param string $method 请求类型 + * @param string $prefix 类型前缀 * @return void */ public static function setMethodPrefix($method, $prefix = '') { if (is_array($method)) { - self::$methodPrefix = array_merge(self::$methodPrefix, array_change_key_case($method, CASE_UPPER)); + self::$methodPrefix = array_merge(self::$methodPrefix, array_change_key_case($method)); } else { - self::$methodPrefix[strtoupper($method)] = $prefix; + self::$methodPrefix[strtolower($method)] = $prefix; } } /** * rest方法定义和修改 * @access public - * @param string $name 方法名称 - * @param array $resourece 资源 + * @param string|array $name 方法名称 + * @param array|bool $resource 资源 * @return void */ public static function rest($name, $resource = []) { if (is_array($name)) { - self::$rest = array_merge(self::$rest, $name); + self::$rest = $resource ? $name : array_merge(self::$rest, $name); } else { self::$rest[$name] = $resource; } @@ -649,9 +683,9 @@ class Route /** * 注册未匹配路由规则后的处理 * @access public - * @param string $route 路由地址 - * @param string $method 请求类型 - * @param array $option 路由参数 + * @param string $route 路由地址 + * @param string $method 请求类型 + * @param array $option 路由参数 * @return void */ public static function miss($route, $method = '*', $option = []) @@ -662,7 +696,7 @@ class Route /** * 注册一个自动解析的URL路由 * @access public - * @param string $route 路由地址 + * @param string $route 路由地址 * @return void */ public static function auto($route) @@ -681,7 +715,7 @@ class Route if (is_array($rules)) { self::$rules = $rules; } elseif ($rules) { - return true === $rules ? self::$rules : self::$rules[$rules]; + return true === $rules ? self::$rules : self::$rules[strtolower($rules)]; } else { $rules = self::$rules; unset($rules['pattern'], $rules['alias'], $rules['domain'], $rules['name']); @@ -692,18 +726,18 @@ class Route /** * 检测子域名部署 * @access public - * @param Request $request Request请求对象 - * @param array $currentRules 当前路由规则 - * @param string $method 请求类型 + * @param Request $request Request请求对象 + * @param array $currentRules 当前路由规则 + * @param string $method 请求类型 * @return void */ - public static function checkDomain($request, &$currentRules, $method = 'GET') + public static function checkDomain($request, &$currentRules, $method = 'get') { // 域名规则 $rules = self::$rules['domain']; // 开启子域名部署 支持二级和三级域名 if (!empty($rules)) { - $host = $request->host(); + $host = $request->host(true); if (isset($rules[$host])) { // 完整域名或者IP配置 $item = $rules[$host]; @@ -742,6 +776,10 @@ class Route } } if (!empty($item)) { + if (isset($panDomain)) { + // 保存当前泛域名 + $request->route(['__domain__' => $panDomain]); + } if (isset($item['[bind]'])) { // 解析子域名部署规则 list($rule, $option, $pattern) = $item['[bind]']; @@ -789,29 +827,36 @@ class Route /** * 检测URL路由 * @access public - * @param Request $request Request请求对象 - * @param string $url URL地址 - * @param string $depr URL分隔符 - * @param bool $checkDomain 是否检测域名规则 + * @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 ('/' != $depr) { - $url = str_replace($depr, '/', $url); + //检查解析缓存 + if (!App::$debug && Config::get('route_check_cache')) { + $key = self::getCheckCacheKey($request); + if (Cache::has($key)) { + list($rule, $route, $pathinfo, $option, $matches) = Cache::get($key); + return self::parseRule($rule, $route, $pathinfo, $option, $matches, true); + } } - if (strpos($url, '/') && isset(self::$rules['alias'][strstr($url, '/', true)])) { + // 分隔符替换 确保路由定义使用统一的分隔符 + $url = str_replace($depr, '|', $url); + + if (isset(self::$rules['alias'][$url]) || isset(self::$rules['alias'][strstr($url, '|', true)])) { // 检测路由别名 $result = self::checkRouteAlias($request, $url, $depr); if (false !== $result) { return $result; } } - $method = $request->method(); + $method = strtolower($request->method()); // 获取当前请求类型的路由规则 - $rules = self::$rules[$method]; + $rules = isset(self::$rules[$method]) ? self::$rules[$method] : []; // 检测域名部署 if ($checkDomain) { self::checkDomain($request, $rules, $method); @@ -821,17 +866,19 @@ class Route if (false !== $return) { return $return; } - if ('/' != $url) { - $url = rtrim($url, '/'); + if ('|' != $url) { + $url = rtrim($url, '|'); } - if (isset($rules[$url])) { + $item = str_replace('|', '/', $url); + if (isset($rules[$item])) { // 静态路由规则检测 - $rule = $rules[$url]; + $rule = $rules[$item]; if (true === $rule) { - $rule = self::getRouteExpress($url); + $rule = self::getRouteExpress($item); } - if (!empty($rule['route']) && self::checkOption($rule['option'], $url, $request)) { - return self::parseRule($url, $rule['route'], $url, $rule['option']); + if (!empty($rule['route']) && self::checkOption($rule['option'], $request)) { + self::setOption($rule['option']); + return self::parseRule($item, $rule['route'], $url, $rule['option']); } } @@ -850,12 +897,12 @@ class Route /** * 检测路由规则 * @access private - * @param Request $request - * @param array $rules 路由规则 - * @param string $url URL地址 - * @param string $depr URL分割符 - * @param string $group 路由分组名 - * @param array $options 路由参数(分组) + * @param Request $request + * @param array $rules 路由规则 + * @param string $url URL地址 + * @param string $depr URL分割符 + * @param string $group 路由分组名 + * @param array $options 路由参数(分组) * @return mixed */ private static function checkRoute($request, $rules, $url, $depr = '/', $group = '', $options = []) @@ -874,7 +921,7 @@ class Route $pattern = $item['pattern']; // 检查参数有效性 - if (!self::checkOption($option, $url, $request)) { + if (!self::checkOption($option, $request)) { continue; } @@ -891,10 +938,10 @@ class Route } else { $str = $key; } - if (is_string($str) && $str && 0 !== strpos($url, $str)) { + if (is_string($str) && $str && 0 !== stripos(str_replace('|', '/', $url), $str)) { continue; } - + self::setOption($option); $result = self::checkRoute($request, $rule, $url, $depr, $key, $option); if (false !== $result) { return $result; @@ -909,10 +956,12 @@ class Route if ($group) { $rule = $group . ($rule ? '/' . ltrim($rule, '/') : ''); } + + self::setOption($option); if (isset($options['bind_model']) && isset($option['bind_model'])) { $option['bind_model'] = array_merge($options['bind_model'], $option['bind_model']); } - $result = self::checkRule($rule, $route, $url, $pattern, $option); + $result = self::checkRule($rule, $route, $url, $pattern, $option, $depr); if (false !== $result) { return $result; } @@ -931,43 +980,56 @@ class Route /** * 检测路由别名 * @access private - * @param Request $request - * @param string $url URL地址 - * @param string $depr URL分隔符 + * @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]]; + $array = explode('|', $url); + $alias = array_shift($array); + $item = self::$rules['alias'][$alias]; if (is_array($item)) { list($rule, $option) = $item; + $action = $array[0]; + if (isset($option['allow']) && !in_array($action, explode(',', $option['allow']))) { + // 允许操作 + return false; + } elseif (isset($option['except']) && in_array($action, explode(',', $option['except']))) { + // 排除操作 + return false; + } + if (isset($option['method'][$action])) { + $option['method'] = $option['method'][$action]; + } } else { $rule = $item; } + $bind = implode('|', $array); // 参数有效性检查 - if (isset($option) && !self::checkOption($option, $url, $request)) { + if (isset($option) && !self::checkOption($option, $request)) { // 路由不匹配 return false; } elseif (0 === strpos($rule, '\\')) { // 路由到类 - return self::bindToClass($array[1], substr($rule, 1), $depr); - } elseif (0 === strpos($url, '@')) { + return self::bindToClass($bind, substr($rule, 1), $depr); + } elseif (0 === strpos($rule, '@')) { // 路由到控制器类 - return self::bindToController($array[1], substr($rule, 1), $depr); + return self::bindToController($bind, substr($rule, 1), $depr); } else { // 路由到模块/控制器 - return self::bindToModule($array[1], $rule, $depr); + return self::bindToModule($bind, $rule, $depr); } } /** * 检测URL绑定 * @access private - * @param string $url URL地址 - * @param array $rules 路由规则 - * @param string $depr URL分隔符 + * @param string $url URL地址 + * @param array $rules 路由规则 + * @param string $depr URL分隔符 * @return mixed */ private static function checkUrlBind(&$url, &$rules, $depr = '/') @@ -982,6 +1044,9 @@ class Route case 'class': // 绑定到类 return self::bindToClass($url, $bind, $depr); + case 'controller': + // 绑定到控制器类 + return self::bindToController($url, $bind, $depr); case 'namespace': // 绑定到命名空间 return self::bindToNamespace($url, $bind, $depr); @@ -993,69 +1058,73 @@ class Route /** * 绑定到类 * @access public - * @param string $url URL地址 - * @param string $class 类名(带命名空间) - * @param string $depr URL分隔符 + * @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); + $url = str_replace($depr, '|', $url); + $array = explode('|', $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]]; + return ['type' => 'method', 'method' => [$class, $action], 'var' => []]; } /** * 绑定到命名空间 * @access public - * @param string $url URL地址 - * @param string $namespace 命名空间 - * @param string $depr URL分隔符 + * @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); + $url = str_replace($depr, '|', $url); + $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' => [$namespace . '\\' . $class, $method]]; + return ['type' => 'method', 'method' => [$namespace . '\\' . Loader::parseName($class, 1), $method], 'var' => []]; } /** * 绑定到控制器类 * @access public - * @param string $url URL地址 - * @param string $controller 控制器名 (支持带模块名 index/user ) - * @param string $depr URL分隔符 + * @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); + $url = str_replace($depr, '|', $url); + $array = explode('|', $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]; + return ['type' => 'controller', 'controller' => $controller . '/' . $action, 'var' => []]; } /** * 绑定到模块/控制器 * @access public - * @param string $url URL地址 - * @param string $class 控制器类名(带命名空间) - * @param string $depr URL分隔符 + * @param string $url URL地址 + * @param string $controller 控制器类名(带命名空间) + * @param string $depr URL分隔符 * @return array */ public static function bindToModule($url, $controller, $depr = '/') { - $array = explode($depr, $url, 2); + $url = str_replace($depr, '|', $url); + $array = explode('|', $url, 2); $action = !empty($array[0]) ? $array[0] : Config::get('default_action'); if (!empty($array[1])) { self::parseUrlParams($array[1]); @@ -1066,20 +1135,23 @@ class Route /** * 路由参数有效性检查 * @access private - * @param array $option 路由参数 - * @param string $url URL地址 - * @param Request $request Request对象 + * @param array $option 路由参数 + * @param Request $request Request对象 * @return bool */ - private static function checkOption($option, $url, $request) + private static function checkOption($option, $request) { - // 请求类型检测 - if ((isset($option['method']) && false === stripos($option['method'], $request->method())) - || (isset($option['ext']) && false === stripos($option['ext'], $request->ext())) // 伪静态后缀检测 - || (isset($option['deny_ext']) && false !== stripos($option['deny_ext'], $request->ext())) + if ((isset($option['method']) && is_string($option['method']) && false === stripos($option['method'], $request->method())) + || (isset($option['ajax']) && $option['ajax'] && !$request->isAjax()) // Ajax检测 + || (isset($option['ajax']) && !$option['ajax'] && $request->isAjax()) // 非Ajax检测 + || (isset($option['pjax']) && $option['pjax'] && !$request->isPjax()) // Pjax检测 + || (isset($option['pjax']) && !$option['pjax'] && $request->isPjax()) // 非Pjax检测 + || (isset($option['ext']) && false === stripos('|' . $option['ext'] . '|', '|' . $request->ext() . '|')) // 伪静态后缀检测 + || (isset($option['deny_ext']) && false !== stripos('|' . $option['deny_ext'] . '|', '|' . $request->ext() . '|')) || (isset($option['domain']) && !in_array($option['domain'], [$_SERVER['HTTP_HOST'], self::$subDomain])) // 域名检测 - || (!empty($option['https']) && !$request->isSsl()) // https检测 - || (!empty($option['before_behavior']) && false === Hook::exec($option['before_behavior'], '', $url)) // 行为检测 + || (isset($option['https']) && $option['https'] && !$request->isSsl()) // https检测 + || (isset($option['https']) && !$option['https'] && $request->isSsl()) // https检测 + || (!empty($option['before_behavior']) && false === Hook::exec($option['before_behavior'])) // 行为检测 || (!empty($option['callback']) && is_callable($option['callback']) && false === call_user_func($option['callback'])) // 自定义检测 ) { return false; @@ -1090,29 +1162,33 @@ class Route /** * 检测路由规则 * @access private - * @param string $rule 路由规则 - * @param string $route 路由地址 - * @param string $url URL地址 - * @param array $pattern 变量规则 - * @param array $option 路由参数 + * @param string $rule 路由规则 + * @param string $route 路由地址 + * @param string $url URL地址 + * @param array $pattern 变量规则 + * @param array $option 路由参数 + * @param string $depr URL分隔符(全局) * @return array|false */ - private static function checkRule($rule, $route, $url, $pattern, $option) + private static function checkRule($rule, $route, $url, $pattern, $option, $depr) { // 检查完整规则定义 - if (isset($pattern['__url__']) && !preg_match('/^' . $pattern['__url__'] . '/', $url)) { + if (isset($pattern['__url__']) && !preg_match(0 === strpos($pattern['__url__'], '/') ? $pattern['__url__'] : '/^' . $pattern['__url__'] . '/', str_replace('|', $depr, $url))) { return false; } - // 检测是否设置了参数分隔符 - if ($depr = Config::get('url_params_depr')) { - $url = str_replace($depr, '/', $url); - $rule = str_replace($depr, '/', $rule); + // 检查路由的参数分隔符 + if (isset($option['param_depr'])) { + $url = str_replace(['|', $option['param_depr']], [$depr, '|'], $url); } - $len1 = substr_count($url, '/'); + $len1 = substr_count($url, '|'); $len2 = substr_count($rule, '/'); // 多余参数是否合并 - $merge = !empty($option['merge_extra_vars']) ? true : false; + $merge = !empty($option['merge_extra_vars']); + if ($merge && $len1 > $len2) { + $url = str_replace('|', $depr, $url); + $url = implode('|', explode($depr, $url, $len2 + 1)); + } if ($len1 >= $len2 || strpos($rule, '[')) { if (!empty($option['complete_match'])) { @@ -1122,9 +1198,9 @@ class Route } } $pattern = array_merge(self::$rules['pattern'], $pattern); - if (false !== $match = self::match($url, $rule, $pattern, $merge)) { + if (false !== $match = self::match($url, $rule, $pattern)) { // 匹配到路由规则 - return self::parseRule($rule, $route, $url, $option, $match, $merge); + return self::parseRule($rule, $route, $url, $option, $match); } } return false; @@ -1133,19 +1209,21 @@ class Route /** * 解析模块的URL地址 [模块/控制器/操作?]参数1=值1&参数2=值2... * @access public - * @param string $url URL地址 - * @param string $depr URL分隔符 - * @param bool $autoSearch 是否自动深度搜索控制器 + * @param string $url URL地址 + * @param string $depr URL分隔符 + * @param bool $autoSearch 是否自动深度搜索控制器 * @return array */ public static function parseUrl($url, $depr = '/', $autoSearch = false) { + if (isset(self::$bind['module'])) { + $bind = str_replace('/', $depr, self::$bind['module']); // 如果有模块/控制器绑定 - $url = self::$bind['module'] . '/' . ltrim($url, '/'); + $url = $bind . ('.' != substr($bind, -1) ? $depr : '') . ltrim($url, $depr); } - - list($path, $var) = self::parseUrlPath($url, $depr); + $url = str_replace($depr, '|', $url); + list($path, $var) = self::parseUrlPath($url); $route = [null, null, null]; if (isset($path)) { // 解析模块 @@ -1155,15 +1233,24 @@ class Route $dir = APP_PATH . ($module ? $module . DS : '') . Config::get('url_controller_layer'); $suffix = App::$suffix || Config::get('controller_suffix') ? ucfirst(Config::get('url_controller_layer')) : ''; $item = []; + $find = false; foreach ($path as $val) { - $item[] = array_shift($path); - if (is_file($dir . DS . $val . $suffix . EXT)) { + $item[] = $val; + $file = $dir . DS . str_replace('.', DS, $val) . $suffix . EXT; + $file = pathinfo($file, PATHINFO_DIRNAME) . DS . Loader::parseName(pathinfo($file, PATHINFO_FILENAME), 1) . EXT; + if (is_file($file)) { + $find = true; break; } else { - $dir .= DS . $val; + $dir .= DS . Loader::parseName($val); } } - $controller = implode('.', $item); + if ($find) { + $controller = implode('.', $item); + $path = array_slice($path, count($item)); + } else { + $controller = array_shift($path); + } } else { // 解析控制器 $controller = !empty($path) ? array_shift($path) : null; @@ -1171,11 +1258,18 @@ class Route // 解析操作 $action = !empty($path) ? array_shift($path) : null; // 解析额外参数 - self::parseUrlParams(empty($path) ? '' : implode('/', $path)); + self::parseUrlParams(empty($path) ? '' : implode('|', $path)); // 封装路由 $route = [$module, $controller, $action]; - if (isset(self::$rules['name'][implode($depr, $route)])) { - throw new HttpException(404, 'invalid request:' . $url); + // 检查地址是否被定义过路由 + $name = strtolower($module . '/' . Loader::parseName($controller, 1) . '/' . $action); + $name2 = ''; + if (empty($module) || isset($bind) && $module == $bind) { + $name2 = strtolower(Loader::parseName($controller, 1) . '/' . $action); + } + + if (isset(self::$rules['name'][$name]) || isset(self::$rules['name'][$name2])) { + throw new HttpException(404, 'invalid request:' . str_replace('|', $depr, $url)); } } return ['type' => 'module', 'module' => $route]; @@ -1184,16 +1278,13 @@ class Route /** * 解析URL的pathinfo参数和变量 * @access private - * @param string $url URL地址 - * @param string $depr URL分隔符 + * @param string $url URL地址 * @return array */ - private static function parseUrlPath($url, $depr = '/') + private static function parseUrlPath($url) { // 分隔符替换 确保路由定义使用统一的分隔符 - if ('/' != $depr) { - $url = str_replace($depr, '/', $url); - } + $url = str_replace('|', '/', $url); $url = trim($url, '/'); $var = []; if (false !== strpos($url, '?')) { @@ -1204,9 +1295,6 @@ class Route } elseif (strpos($url, '/')) { // [模块/控制器/操作] $path = explode('/', $url); - } elseif (false !== strpos($url, '=')) { - // 参数1=值1&参数2=值2... - parse_str($url, $var); } else { $path = [$url]; } @@ -1216,16 +1304,15 @@ class Route /** * 检测URL和规则路由是否匹配 * @access private - * @param string $url URL地址 - * @param string $rule 路由规则 - * @param array $pattern 变量规则 - * @param bool $merge 合并额外变量 + * @param string $url URL地址 + * @param string $rule 路由规则 + * @param array $pattern 变量规则 * @return array|false */ - private static function match($url, $rule, $pattern, $merge) + private static function match($url, $rule, $pattern) { $m2 = explode('/', $rule); - $m1 = $merge ? explode('/', $url, count($m2)) : explode('/', $url); + $m1 = explode('|', $url); $var = []; foreach ($m2 as $key => $val) { @@ -1269,9 +1356,16 @@ class Route if (!$optional && !isset($m1[$key])) { return false; } - if (isset($m1[$key]) && isset($pattern[$name]) && !preg_match('/^' . $pattern[$name] . '$/', $m1[$key])) { + if (isset($m1[$key]) && isset($pattern[$name])) { // 检查变量规则 - return false; + if ($pattern[$name] instanceof \Closure) { + $result = call_user_func_array($pattern[$name], [$m1[$key]]); + if (false === $result) { + return false; + } + } elseif (!preg_match(0 === strpos($pattern[$name], '/') ? $pattern[$name] : '/^' . $pattern[$name] . '$/', $m1[$key])) { + return false; + } } $var[$name] = isset($m1[$key]) ? $m1[$key] : ''; } elseif (!isset($m1[$key]) || 0 !== strcasecmp($val, $m1[$key])) { @@ -1285,22 +1379,33 @@ class Route /** * 解析规则路由 * @access private - * @param string $rule 路由规则 - * @param string $route 路由地址 - * @param string $pathinfo URL地址 - * @param array $option 路由参数 - * @param array $matches 匹配的变量 - * @param bool $merge 合并额外变量 + * @param string $rule 路由规则 + * @param string $route 路由地址 + * @param string $pathinfo URL地址 + * @param array $option 路由参数 + * @param array $matches 匹配的变量 + * @param bool $fromCache 通过缓存解析 * @return array */ - private static function parseRule($rule, $route, $pathinfo, $option = [], $matches = [], $merge = false) + private static function parseRule($rule, $route, $pathinfo, $option = [], $matches = [], $fromCache = false) { $request = Request::instance(); + + //保存解析缓存 + if (Config::get('route_check_cache') && !$fromCache) { + try { + $key = self::getCheckCacheKey($request); + Cache::tag('route_check')->set($key, [$rule, $route, $pathinfo, $option, $matches]); + } catch (\Exception $e) { + + } + } + // 解析路由规则 if ($rule) { $rule = explode('/', $rule); // 获取URL地址中的参数 - $paths = $merge ? explode('/', $pathinfo, count($rule)) : explode('/', $pathinfo); + $paths = explode('|', $pathinfo); foreach ($rule as $item) { $fun = ''; if (0 === strpos($item, '[:')) { @@ -1315,7 +1420,7 @@ class Route } } } else { - $paths = explode('/', $pathinfo); + $paths = explode('|', $pathinfo); } // 获取路由地址规则 @@ -1328,7 +1433,6 @@ class Route foreach ($matches as $key => $val) { if (false !== strpos($route, ':' . $key)) { $route = str_replace(':' . $key, $val, $route); - unset($matches[$key]); } } } @@ -1371,8 +1475,12 @@ class Route $request->bind($bind); } + if (!empty($option['response'])) { + Hook::add('response_send', $option['response']); + } + // 解析额外参数 - self::parseUrlParams(empty($paths) ? '' : implode('/', $paths), $matches); + self::parseUrlParams(empty($paths) ? '' : implode('|', $paths), $matches); // 记录匹配的路由信息 $request->routeInfo(['rule' => $rule, 'route' => $route, 'option' => $option, 'var' => $matches]); @@ -1399,31 +1507,39 @@ class Route if ($route instanceof \Closure) { // 执行闭包 $result = ['type' => 'function', 'function' => $route]; - } elseif (0 === strpos($route, '/') || 0 === strpos($route, 'http')) { + } elseif (0 === strpos($route, '/') || strpos($route, '://')) { // 路由到重定向地址 $result = ['type' => 'redirect', 'url' => $route, 'status' => isset($option['status']) ? $option['status'] : 301]; } elseif (false !== strpos($route, '\\')) { // 路由到方法 - $route = str_replace('/', '@', $route); - $method = strpos($route, '@') ? explode('@', $route) : $route; - $result = ['type' => 'method', 'method' => $method]; + list($path, $var) = self::parseUrlPath($route); + $route = str_replace('/', '@', implode('/', $path)); + $method = strpos($route, '@') ? explode('@', $route) : $route; + $result = ['type' => 'method', 'method' => $method, 'var' => $var]; } elseif (0 === strpos($route, '@')) { // 路由到控制器 - $result = ['type' => 'controller', 'controller' => substr($route, 1)]; + $route = substr($route, 1); + list($route, $var) = self::parseUrlPath($route); + $result = ['type' => 'controller', 'controller' => implode('/', $route), 'var' => $var]; + $request->action(array_pop($route)); + $request->controller($route ? array_pop($route) : Config::get('default_controller')); + $request->module($route ? array_pop($route) : Config::get('default_module')); + App::$modulePath = APP_PATH . (Config::get('app_multi_module') ? $request->module() . DS : ''); } else { // 路由到模块/控制器/操作 - $result = self::parseModule($route); + $result = self::parseModule($route, isset($option['convert']) ? $option['convert'] : false); } // 开启请求缓存 - if ($request->isGet() && !empty($option['cache'])) { + if ($request->isGet() && isset($option['cache'])) { $cache = $option['cache']; if (is_array($cache)) { - list($key, $expire) = $cache; + list($key, $expire, $tag) = array_pad($cache, 3, null); } else { - $key = $pathinfo; + $key = str_replace('|', '/', $pathinfo); $expire = $cache; + $tag = null; } - $request->cache($key, $expire); + $request->cache($key, $expire, $tag); } return $result; } @@ -1431,13 +1547,13 @@ class Route /** * 解析URL地址为 模块/控制器/操作 * @access private - * @param string $url URL地址 - * @param string $depr URL分隔符 + * @param string $url URL地址 + * @param bool $convert 是否自动转换URL地址 * @return array */ - private static function parseModule($url, $depr = '/') + private static function parseModule($url, $convert = false) { - list($path, $var) = self::parseUrlPath($url, $depr); + list($path, $var) = self::parseUrlPath($url); $action = array_pop($path); $controller = !empty($path) ? array_pop($path) : null; $module = Config::get('app_multi_module') && !empty($path) ? array_pop($path) : null; @@ -1449,23 +1565,23 @@ class Route // 设置当前请求的路由变量 Request::instance()->route($var); // 路由到模块/控制器/操作 - return ['type' => 'module', 'module' => [$module, $controller, $action], 'convert' => false]; + return ['type' => 'module', 'module' => [$module, $controller, $action], 'convert' => $convert]; } /** * 解析URL地址中的参数Request对象 * @access private - * @param string $rule 路由规则 - * @param array $var 变量 + * @param string $url 路由规则 + * @param array $var 变量 * @return void */ private static function parseUrlParams($url, &$var = []) { if ($url) { if (Config::get('url_param_type')) { - $var += explode('/', $url); + $var += explode('|', $url); } else { - preg_replace_callback('/(\w+)\/([^\/]+)/', function ($match) use (&$var) { + preg_replace_callback('/(\w+)\|([^\|]+)/', function ($match) use (&$var) { $var[$match[1]] = strip_tags($match[2]); }, $url); } @@ -1506,4 +1622,24 @@ class Route } return $var; } + + /** + * 获取路由解析缓存的key + * @param Request $request + * @return string + */ + private static function getCheckCacheKey(Request $request) + { + static $key; + + if (empty($key)) { + if ($callback = Config::get('route_check_cache_key')) { + $key = call_user_func($callback, $request); + } else { + $key = "{$request->host(true)}|{$request->method()}|{$request->path()}"; + } + } + + return $key; + } } diff --git a/library/think/Session.php b/library/think/Session.php index e8f56fd935221fe58a8d8297987af967fed83b6d..61150bcad9b1ad45cd17d95eb2c4acbfcbf54072 100644 --- a/library/think/Session.php +++ b/library/think/Session.php @@ -1,303 +1,366 @@ - -// +---------------------------------------------------------------------- - -namespace think; - -use think\App; -use think\exception\ClassNotFoundException; - -class Session -{ - protected static $prefix = ''; - protected static $init = null; - - /** - * 设置或者获取session作用域(前缀) - * @param string $prefix - * @return string|void - */ - public static function prefix($prefix = '') - { - if (empty($prefix) && null !== $prefix) { - return self::$prefix; - } else { - self::$prefix = $prefix; - } - } - - /** - * session初始化 - * @param array $config - * @return void - * @throws \think\Exception - */ - public static function init(array $config = []) - { - if (empty($config)) { - $config = Config::get('session'); - } - // 记录初始化信息 - 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); - } - - // 启动session - if (!empty($config['auto_start']) && PHP_SESSION_ACTIVE != session_status()) { - ini_set('session.auto_start', 0); - $isDoStart = true; - } - - if (isset($config['prefix'])) { - self::$prefix = $config['prefix']; - } - if (isset($config['var_session_id']) && isset($_REQUEST[$config['var_session_id']])) { - session_id($_REQUEST[$config['var_session_id']]); - } elseif (isset($config['id']) && !empty($config['id'])) { - session_id($config['id']); - } - if (isset($config['name'])) { - session_name($config['name']); - } - if (isset($config['path'])) { - session_save_path($config['path']); - } - if (isset($config['domain'])) { - ini_set('session.cookie_domain', $config['domain']); - } - if (isset($config['expire'])) { - ini_set('session.gc_maxlifetime', $config['expire']); - ini_set('session.cookie_lifetime', $config['expire']); - } - - if (isset($config['use_cookies'])) { - ini_set('session.use_cookies', $config['use_cookies'] ? 1 : 0); - } - if (isset($config['cache_limiter'])) { - session_cache_limiter($config['cache_limiter']); - } - if (isset($config['cache_expire'])) { - session_cache_expire($config['cache_expire']); - } - if (!empty($config['type'])) { - // 读取session驱动 - $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 ClassNotFoundException('error session handler:' . $class, $class); - } - } - if ($isDoStart) { - session_start(); - self::$init = true; - } else { - self::$init = false; - } - } - - /** - * session自动启动或者初始化 - * @return void - */ - public static function boot() - { - if (is_null(self::$init)) { - self::init(); - } elseif (false === self::$init) { - session_start(); - self::$init = true; - } - } - - /** - * session设置 - * @param string $name session名称 - * @param mixed $value session值 - * @param string|null $prefix 作用域(前缀) - * @return void - */ - public static function set($name, $value = '', $prefix = null) - { - empty(self::$init) && self::boot(); - - $prefix = !is_null($prefix) ? $prefix : self::$prefix; - if (strpos($name, '.')) { - // 二维数组赋值 - list($name1, $name2) = explode('.', $name); - if ($prefix) { - $_SESSION[$prefix][$name1][$name2] = $value; - } else { - $_SESSION[$name1][$name2] = $value; - } - } elseif ($prefix) { - $_SESSION[$prefix][$name] = $value; - } else { - $_SESSION[$name] = $value; - } - } - - /** - * session获取 - * @param string $name session名称 - * @param string|null $prefix 作用域(前缀) - * @return mixed - */ - public static function get($name = '', $prefix = null) - { - empty(self::$init) && self::boot(); - $prefix = !is_null($prefix) ? $prefix : self::$prefix; - if ('' == $name) { - // 获取全部的session - $value = $prefix ? (!empty($_SESSION[$prefix]) ? $_SESSION[$prefix] : []) : $_SESSION; - } elseif ($prefix) { - // 获取session - if (strpos($name, '.')) { - list($name1, $name2) = explode('.', $name); - $value = isset($_SESSION[$prefix][$name1][$name2]) ? $_SESSION[$prefix][$name1][$name2] : null; - } else { - $value = isset($_SESSION[$prefix][$name]) ? $_SESSION[$prefix][$name] : null; - } - } else { - if (strpos($name, '.')) { - list($name1, $name2) = explode('.', $name); - $value = isset($_SESSION[$name1][$name2]) ? $_SESSION[$name1][$name2] : null; - } else { - $value = isset($_SESSION[$name]) ? $_SESSION[$name] : null; - } - } - return $value; - } - - /** - * session获取并删除 - * @param string $name session名称 - * @param string|null $prefix 作用域(前缀) - * @return mixed - */ - public static function pull($name, $prefix = null) - { - $result = self::get($name, $prefix); - if ($result) { - self::delete($name, $prefix); - return $result; - } else { - return null; - } - } - - /** - * 删除session数据 - * @param string $name session名称 - * @param string|null $prefix 作用域(前缀) - * @return void - */ - public static function delete($name, $prefix = null) - { - empty(self::$init) && self::boot(); - $prefix = !is_null($prefix) ? $prefix : self::$prefix; - if (strpos($name, '.')) { - list($name1, $name2) = explode('.', $name); - if ($prefix) { - unset($_SESSION[$prefix][$name1][$name2]); - } else { - unset($_SESSION[$name1][$name2]); - } - } else { - if ($prefix) { - unset($_SESSION[$prefix][$name]); - } else { - unset($_SESSION[$name]); - } - } - } - - /** - * 清空session数据 - * @param string|null $prefix 作用域(前缀) - * @return void - */ - public static function clear($prefix = null) - { - empty(self::$init) && self::boot(); - $prefix = !is_null($prefix) ? $prefix : self::$prefix; - if ($prefix) { - unset($_SESSION[$prefix]); - } else { - $_SESSION = []; - } - } - - /** - * 判断session数据 - * @param string $name session名称 - * @param string|null $prefix - * @return bool - */ - public static function has($name, $prefix = null) - { - empty(self::$init) && self::boot(); - $prefix = !is_null($prefix) ? $prefix : self::$prefix; - if (strpos($name, '.')) { - // 支持数组 - list($name1, $name2) = explode('.', $name); - return $prefix ? isset($_SESSION[$prefix][$name1][$name2]) : isset($_SESSION[$name1][$name2]); - } else { - return $prefix ? isset($_SESSION[$prefix][$name]) : isset($_SESSION[$name]); - } - } - - /** - * 启动session - * @return void - */ - public static function start() - { - session_start(); - self::$init = true; - } - - /** - * 销毁session - * @return void - */ - public static function destroy() - { - if (!empty($_SESSION)) { - $_SESSION = []; - } - session_unset(); - session_destroy(); - self::$init = null; - } - - /** - * 重新生成session_id - * @param bool $delete 是否删除关联会话文件 - * @return void - */ - private static function regenerate($delete = false) - { - session_regenerate_id($delete); - } - - /** - * 暂停session - * @return void - */ - public static function pause() - { - // 暂停session - session_write_close(); - self::$init = false; - } -} + +// +---------------------------------------------------------------------- + +namespace think; + +use think\exception\ClassNotFoundException; + +class Session +{ + protected static $prefix = ''; + protected static $init = null; + + /** + * 设置或者获取session作用域(前缀) + * @param string $prefix + * @return string|void + */ + public static function prefix($prefix = '') + { + empty(self::$init) && self::boot(); + if (empty($prefix) && null !== $prefix) { + return self::$prefix; + } else { + self::$prefix = $prefix; + } + } + + /** + * session初始化 + * @param array $config + * @return void + * @throws \think\Exception + */ + public static function init(array $config = []) + { + if (empty($config)) { + $config = Config::get('session'); + } + // 记录初始化信息 + 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); + } + + // 启动session + if (!empty($config['auto_start']) && PHP_SESSION_ACTIVE != session_status()) { + ini_set('session.auto_start', 0); + $isDoStart = true; + } + + if (isset($config['prefix']) && ('' === self::$prefix || null === self::$prefix)) { + self::$prefix = $config['prefix']; + } + if (isset($config['var_session_id']) && isset($_REQUEST[$config['var_session_id']])) { + session_id($_REQUEST[$config['var_session_id']]); + } elseif (isset($config['id']) && !empty($config['id'])) { + session_id($config['id']); + } + if (isset($config['name'])) { + session_name($config['name']); + } + if (isset($config['path'])) { + session_save_path($config['path']); + } + if (isset($config['domain'])) { + ini_set('session.cookie_domain', $config['domain']); + } + if (isset($config['expire'])) { + ini_set('session.gc_maxlifetime', $config['expire']); + ini_set('session.cookie_lifetime', $config['expire']); + } + if (isset($config['secure'])) { + ini_set('session.cookie_secure', $config['secure']); + } + if (isset($config['httponly'])) { + ini_set('session.cookie_httponly', $config['httponly']); + } + if (isset($config['use_cookies'])) { + ini_set('session.use_cookies', $config['use_cookies'] ? 1 : 0); + } + if (isset($config['cache_limiter'])) { + session_cache_limiter($config['cache_limiter']); + } + if (isset($config['cache_expire'])) { + session_cache_expire($config['cache_expire']); + } + if (!empty($config['type'])) { + // 读取session驱动 + $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 ClassNotFoundException('error session handler:' . $class, $class); + } + } + if ($isDoStart) { + session_start(); + self::$init = true; + } else { + self::$init = false; + } + } + + /** + * session自动启动或者初始化 + * @return void + */ + public static function boot() + { + if (is_null(self::$init)) { + self::init(); + } elseif (false === self::$init) { + if (PHP_SESSION_ACTIVE != session_status()) { + session_start(); + } + self::$init = true; + } + } + + /** + * session设置 + * @param string $name session名称 + * @param mixed $value session值 + * @param string|null $prefix 作用域(前缀) + * @return void + */ + public static function set($name, $value = '', $prefix = null) + { + empty(self::$init) && self::boot(); + + $prefix = !is_null($prefix) ? $prefix : self::$prefix; + if (strpos($name, '.')) { + // 二维数组赋值 + list($name1, $name2) = explode('.', $name); + if ($prefix) { + $_SESSION[$prefix][$name1][$name2] = $value; + } else { + $_SESSION[$name1][$name2] = $value; + } + } elseif ($prefix) { + $_SESSION[$prefix][$name] = $value; + } else { + $_SESSION[$name] = $value; + } + } + + /** + * session获取 + * @param string $name session名称 + * @param string|null $prefix 作用域(前缀) + * @return mixed + */ + public static function get($name = '', $prefix = null) + { + empty(self::$init) && self::boot(); + $prefix = !is_null($prefix) ? $prefix : self::$prefix; + if ('' == $name) { + // 获取全部的session + $value = $prefix ? (!empty($_SESSION[$prefix]) ? $_SESSION[$prefix] : []) : $_SESSION; + } elseif ($prefix) { + // 获取session + if (strpos($name, '.')) { + list($name1, $name2) = explode('.', $name); + $value = isset($_SESSION[$prefix][$name1][$name2]) ? $_SESSION[$prefix][$name1][$name2] : null; + } else { + $value = isset($_SESSION[$prefix][$name]) ? $_SESSION[$prefix][$name] : null; + } + } else { + if (strpos($name, '.')) { + list($name1, $name2) = explode('.', $name); + $value = isset($_SESSION[$name1][$name2]) ? $_SESSION[$name1][$name2] : null; + } else { + $value = isset($_SESSION[$name]) ? $_SESSION[$name] : null; + } + } + return $value; + } + + /** + * session获取并删除 + * @param string $name session名称 + * @param string|null $prefix 作用域(前缀) + * @return mixed + */ + public static function pull($name, $prefix = null) + { + $result = self::get($name, $prefix); + if ($result) { + self::delete($name, $prefix); + return $result; + } else { + return; + } + } + + /** + * session设置 下一次请求有效 + * @param string $name session名称 + * @param mixed $value session值 + * @param string|null $prefix 作用域(前缀) + * @return void + */ + public static function flash($name, $value) + { + self::set($name, $value); + if (!self::has('__flash__.__time__')) { + self::set('__flash__.__time__', $_SERVER['REQUEST_TIME_FLOAT']); + } + self::push('__flash__', $name); + } + + /** + * 清空当前请求的session数据 + * @return void + */ + public static function flush() + { + if (self::$init) { + $item = self::get('__flash__'); + + if (!empty($item)) { + $time = $item['__time__']; + if ($_SERVER['REQUEST_TIME_FLOAT'] > $time) { + unset($item['__time__']); + self::delete($item); + self::set('__flash__', []); + } + } + } + } + + /** + * 删除session数据 + * @param string|array $name session名称 + * @param string|null $prefix 作用域(前缀) + * @return void + */ + public static function delete($name, $prefix = null) + { + empty(self::$init) && self::boot(); + $prefix = !is_null($prefix) ? $prefix : self::$prefix; + if (is_array($name)) { + foreach ($name as $key) { + self::delete($key, $prefix); + } + } elseif (strpos($name, '.')) { + list($name1, $name2) = explode('.', $name); + if ($prefix) { + unset($_SESSION[$prefix][$name1][$name2]); + } else { + unset($_SESSION[$name1][$name2]); + } + } else { + if ($prefix) { + unset($_SESSION[$prefix][$name]); + } else { + unset($_SESSION[$name]); + } + } + } + + /** + * 清空session数据 + * @param string|null $prefix 作用域(前缀) + * @return void + */ + public static function clear($prefix = null) + { + empty(self::$init) && self::boot(); + $prefix = !is_null($prefix) ? $prefix : self::$prefix; + if ($prefix) { + unset($_SESSION[$prefix]); + } else { + $_SESSION = []; + } + } + + /** + * 判断session数据 + * @param string $name session名称 + * @param string|null $prefix + * @return bool + */ + public static function has($name, $prefix = null) + { + empty(self::$init) && self::boot(); + $prefix = !is_null($prefix) ? $prefix : self::$prefix; + if (strpos($name, '.')) { + // 支持数组 + list($name1, $name2) = explode('.', $name); + return $prefix ? isset($_SESSION[$prefix][$name1][$name2]) : isset($_SESSION[$name1][$name2]); + } else { + return $prefix ? isset($_SESSION[$prefix][$name]) : isset($_SESSION[$name]); + } + } + + /** + * 添加数据到一个session数组 + * @param string $key + * @param mixed $value + * @return void + */ + public static function push($key, $value) + { + $array = self::get($key); + if (is_null($array)) { + $array = []; + } + $array[] = $value; + self::set($key, $array); + } + + /** + * 启动session + * @return void + */ + public static function start() + { + session_start(); + self::$init = true; + } + + /** + * 销毁session + * @return void + */ + public static function destroy() + { + if (!empty($_SESSION)) { + $_SESSION = []; + } + session_unset(); + session_destroy(); + self::$init = null; + } + + /** + * 重新生成session_id + * @param bool $delete 是否删除关联会话文件 + * @return void + */ + public static function regenerate($delete = false) + { + session_regenerate_id($delete); + } + + /** + * 暂停session + * @return void + */ + public static function pause() + { + // 暂停session + session_write_close(); + self::$init = false; + } +} diff --git a/library/think/Template.php b/library/think/Template.php index 036b367ebbd4c4188d6143a518fffa66e5e1f4fa..9ba0ff35bfe99dec914710637d41bd39613125e5 100644 --- a/library/think/Template.php +++ b/library/think/Template.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2016 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- @@ -12,6 +12,7 @@ namespace think; use think\exception\TemplateNotFoundException; +use think\template\TagLib; /** * ThinkPHP分离出来的模板引擎 @@ -25,6 +26,7 @@ class Template // 引擎配置 protected $config = [ 'view_path' => '', // 模板路径 + 'view_base' => '', 'view_suffix' => 'html', // 默认模板文件后缀 'view_depr' => DS, 'cache_suffix' => 'php', // 默认模板缓存后缀 @@ -56,17 +58,22 @@ class Template protected $storage; /** - * 架构函数 + * 构造函数 * @access public + * @param array $config */ public function __construct(array $config = []) { - $this->config['cache_path'] = TEMP_PATH; - $this->config = array_merge($this->config, $config); - $this->config['taglib_begin'] = $this->stripPreg($this->config['taglib_begin']); - $this->config['taglib_end'] = $this->stripPreg($this->config['taglib_end']); - $this->config['tpl_begin'] = $this->stripPreg($this->config['tpl_begin']); - $this->config['tpl_end'] = $this->stripPreg($this->config['tpl_end']); + $this->config['cache_path'] = TEMP_PATH; + $this->config = array_merge($this->config, $config); + + $this->config['taglib_begin_origin'] = $this->config['taglib_begin']; + $this->config['taglib_end_origin'] = $this->config['taglib_end']; + + $this->config['taglib_begin'] = preg_quote($this->config['taglib_begin'], '/'); + $this->config['taglib_end'] = preg_quote($this->config['taglib_end'], '/'); + $this->config['tpl_begin'] = preg_quote($this->config['tpl_begin'], '/'); + $this->config['tpl_end'] = preg_quote($this->config['tpl_end'], '/'); // 初始化模板编译存储器 $type = $this->config['compile_type'] ? $this->config['compile_type'] : 'File'; @@ -74,20 +81,6 @@ class Template $this->storage = new $class(); } - /** - * 字符串替换 避免正则混淆 - * @access private - * @param string $str - * @return string - */ - private function stripPreg($str) - { - return str_replace( - ['{', '}', '(', ')', '|', '[', ']', '-', '+', '*', '.', '^', '?'], - ['\{', '\}', '\(', '\)', '\|', '\[', '\]', '\-', '\+', '\*', '\.', '\^', '\?'], - $str); - } - /** * 模板变量赋值 * @access public @@ -119,7 +112,7 @@ class Template * 模板引擎配置项 * @access public * @param array|string $config - * @return void|array + * @return string|void|array */ public function config($config) { @@ -128,7 +121,7 @@ class Template } elseif (isset($this->config[$config])) { return $this->config[$config]; } else { - return null; + return; } } @@ -182,7 +175,7 @@ class Template } $template = $this->parseTemplateFile($template); if ($template) { - $cacheFile = $this->config['cache_path'] . $this->config['cache_prefix'] . md5($template) . '.' . ltrim($this->config['cache_suffix'], '.'); + $cacheFile = $this->config['cache_path'] . $this->config['cache_prefix'] . md5($this->config['layout_name'] . $template) . '.' . ltrim($this->config['cache_suffix'], '.'); if (!$this->checkCache($cacheFile)) { // 缓存无效 重新模板编译 $content = file_get_contents($template); @@ -233,7 +226,7 @@ class Template * @access public * @param mixed $name 布局模板名称 false 则关闭布局 * @param string $replace 布局模板内容替换标识 - * @return object + * @return Template */ public function layout($name, $replace = '') { @@ -667,7 +660,7 @@ class Template $content = str_replace($matches[0], '', $content); return explode(',', $matches['name']); } - return null; + return; } /** @@ -687,6 +680,7 @@ class Template } else { $className = '\\think\\template\\taglib\\' . ucwords($tagLib); } + /** @var Taglib $tLib */ $tLib = new $className($this); $tLib->parseTag($content, $hide ? '' : $tagLib); return; @@ -762,31 +756,26 @@ class Template } else { if (isset($array[1])) { $this->parseVar($array[2]); - $_name = ' && ' . $name . $array[1] . $array[2]; + $express = $name . $array[1] . $array[2]; } else { - $_name = ''; + $express = false; } // $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 = ''; - } else { - $str = ''; - } + $str = ''; } } } else { @@ -926,7 +915,7 @@ class Template if (false === strpos($name, '(')) { $name = '(isset(' . $name . ') && (' . $name . ' !== \'\')?' . $name . ':' . $args[1] . ')'; } else { - $name = '(' . $name . ' !== \'\'?' . $name . ':' . $args[1] . ')'; + $name = '(' . $name . ' ?: ' . $args[1] . ')'; } break; default: // 通用模板函数 @@ -959,78 +948,66 @@ class Template * @param array $vars 变量数组 * @return string */ - public function parseThinkVar(&$vars) + public function parseThinkVar($vars) { - $vars[0] = strtoupper(trim($vars[0])); - $parseStr = ''; - if (count($vars) >= 2) { - $vars[1] = trim($vars[1]); - switch ($vars[0]) { + $type = strtoupper(trim(array_shift($vars))); + $param = implode('.', $vars); + if ($vars) { + switch ($type) { case 'SERVER': - $parseStr = '$_SERVER[\'' . strtoupper($vars[1]) . '\']'; + $parseStr = '\\think\\Request::instance()->server(\'' . $param . '\')'; break; case 'GET': - $parseStr = '$_GET[\'' . $vars[1] . '\']'; + $parseStr = '\\think\\Request::instance()->get(\'' . $param . '\')'; break; case 'POST': - $parseStr = '$_POST[\'' . $vars[1] . '\']'; + $parseStr = '\\think\\Request::instance()->post(\'' . $param . '\')'; break; case 'COOKIE': - if (isset($vars[2])) { - $parseStr = '\\think\\Cookie::get(\'' . $vars[1] . '.' . $vars[2] . '\')'; - } else { - $parseStr = '\\think\\Cookie::get(\'' . $vars[1] . '\')'; - } + $parseStr = '\\think\\Cookie::get(\'' . $param . '\')'; break; case 'SESSION': - if (isset($vars[2])) { - $parseStr = '\\think\\Session::get(\'' . $vars[1] . '.' . $vars[2] . '\')'; - } else { - $parseStr = '\\think\\Session::get(\'' . $vars[1] . '\')'; - } + $parseStr = '\\think\\Session::get(\'' . $param . '\')'; break; case 'ENV': - $parseStr = '$_ENV[\'' . strtoupper($vars[1]) . '\']'; + $parseStr = '\\think\\Request::instance()->env(\'' . $param . '\')'; break; case 'REQUEST': - $parseStr = '$_REQUEST[\'' . $vars[1] . '\']'; + $parseStr = '\\think\\Request::instance()->request(\'' . $param . '\')'; break; case 'CONST': - $parseStr = strtoupper($vars[1]); + $parseStr = strtoupper($param); break; case 'LANG': - $parseStr = '\\think\\Lang::get(\'' . $vars[1] . '\')'; + $parseStr = '\\think\\Lang::get(\'' . $param . '\')'; break; case 'CONFIG': - if (isset($vars[2])) { - $vars[1] .= '.' . $vars[2]; - } - $parseStr = '\\think\\Config::get(\'' . $vars[1] . '\')'; + $parseStr = '\\think\\Config::get(\'' . $param . '\')'; break; default: $parseStr = '\'\''; break; } } else { - if (count($vars) == 1) { - switch ($vars[0]) { - case 'NOW': - $parseStr = "date('Y-m-d g:i a',time())"; - break; - case 'VERSION': - $parseStr = 'THINK_VERSION'; - break; - case 'LDELIM': - $parseStr = '\'' . ltrim($this->config['tpl_begin'], '\\') . '\''; - break; - case 'RDELIM': - $parseStr = '\'' . ltrim($this->config['tpl_end'], '\\') . '\''; - break; - default: - if (defined($vars[0])) { - $parseStr = $vars[0]; - } - } + switch ($type) { + case 'NOW': + $parseStr = "date('Y-m-d g:i a',time())"; + break; + case 'VERSION': + $parseStr = 'THINK_VERSION'; + break; + case 'LDELIM': + $parseStr = '\'' . ltrim($this->config['tpl_begin'], '\\') . '\''; + break; + case 'RDELIM': + $parseStr = '\'' . ltrim($this->config['tpl_end'], '\\') . '\''; + break; + default: + if (defined($type)) { + $parseStr = $type; + } else { + $parseStr = ''; + } } } return $parseStr; @@ -1073,14 +1050,20 @@ class Template { if ('' == pathinfo($template, PATHINFO_EXTENSION)) { if (strpos($template, '@')) { - // 跨模块调用模板 + list($module, $template) = explode('@', $template); + } + if (0 !== strpos($template, '/')) { $template = str_replace(['/', ':'], $this->config['view_depr'], $template); - $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; + $template = str_replace(['/', ':'], $this->config['view_depr'], substr($template, 1)); + } + if ($this->config['view_base']) { + $module = isset($module) ? $module : Request::instance()->module(); + $path = $this->config['view_base'] . ($module ? $module . DS : ''); + } else { + $path = isset($module) ? APP_PATH . $module . DS . basename($this->config['view_path']) . DS : $this->config['view_path']; } - $template .= '.' . ltrim($this->config['view_suffix'], '.'); + $template = realpath($path . $template . '.' . ltrim($this->config['view_suffix'], '.')); } if (is_file($template)) { diff --git a/library/think/Url.php b/library/think/Url.php index 4e1457026a087e09dfcd656c6e060589b39cb7bc..53a545f921365926e1091c75fc6243e23836ed57 100644 --- a/library/think/Url.php +++ b/library/think/Url.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2016 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- @@ -11,15 +11,11 @@ namespace think; -use think\Config; -use think\Loader; -use think\Request; -use think\Route; - class Url { // 生成URL地址的root protected static $root; + protected static $bindCheck; /** * URL生成 支持路由反射 @@ -31,7 +27,7 @@ class Url */ public static function build($url = '', $vars = '', $suffix = true, $domain = false) { - if (false === $domain && Config::get('url_domain_deploy')) { + if (false === $domain && Route::rules('domain')) { $domain = true; } // 解析URL @@ -40,22 +36,24 @@ class Url $name = substr($url, 1, $pos - 1); $url = 'name' . substr($url, $pos + 1); } - $info = parse_url($url); - $url = !empty($info['path']) ? $info['path'] : ''; - if (isset($info['fragment'])) { - // 解析锚点 - $anchor = $info['fragment']; - if (false !== strpos($anchor, '?')) { - // 解析参数 - list($anchor, $info['query']) = explode('?', $anchor, 2); - } - if (false !== strpos($anchor, '@')) { + if (false === strpos($url, '://') && 0 !== strpos($url, '/')) { + $info = parse_url($url); + $url = !empty($info['path']) ? $info['path'] : ''; + if (isset($info['fragment'])) { + // 解析锚点 + $anchor = $info['fragment']; + if (false !== strpos($anchor, '?')) { + // 解析参数 + list($anchor, $info['query']) = explode('?', $anchor, 2); + } + if (false !== strpos($anchor, '@')) { + // 解析域名 + list($anchor, $domain) = explode('@', $anchor, 2); + } + } elseif (strpos($url, '@') && false === strpos($url, '\\')) { // 解析域名 - list($anchor, $domain) = explode('@', $anchor, 2); + list($url, $domain) = explode('@', $url, 2); } - } elseif (strpos($url, '@')) { - // 解析域名 - list($url, $domain) = explode('@', $url, 2); } // 解析参数 @@ -77,27 +75,52 @@ class Url if (!empty($rule) && $match = self::getRuleUrl($rule, $vars)) { // 匹配路由命名标识 $url = $match[0]; + // 替换可选分隔符 + $url = preg_replace(['/(\W)\?$/', '/(\W)\?/'], ['', '\1'], $url); if (!empty($match[1])) { $domain = $match[1]; } + if (!is_null($match[2])) { + $suffix = $match[2]; + } } elseif (!empty($rule) && isset($name)) { throw new \InvalidArgumentException('route name not exists:' . $name); } else { + // 检查别名路由 + $alias = Route::rules('alias'); + $matchAlias = false; + if ($alias) { + // 别名路由解析 + foreach ($alias as $key => $val) { + if (is_array($val)) { + $val = $val[0]; + } + if (0 === strpos($url, $val)) { + $url = $key . substr($url, strlen($val)); + $matchAlias = true; + break; + } + } + } + if (!$matchAlias) { + // 路由标识不存在 直接解析 + $url = self::parseUrl($url, $domain); + } if (isset($info['query'])) { // 解析地址里面参数 合并到vars parse_str($info['query'], $params); $vars = array_merge($params, $vars); } - // 路由标识不存在 直接解析 - $url = self::parseUrl($url, $domain); } // 检测URL绑定 - $type = Route::getBind('type'); - if ($type) { - $bind = Route::getBind($type); - if (0 === strpos($url, $bind)) { - $url = substr($url, strlen($bind) + 1); + if (!self::$bindCheck) { + $type = Route::getBind('type'); + if ($type) { + $bind = Route::getBind($type); + if ($bind && 0 === strpos($url, $bind)) { + $url = substr($url, strlen($bind) + 1); + } } } // 还原URL分隔符 @@ -112,12 +135,17 @@ class Url if (!empty($vars)) { // 添加参数 if (Config::get('url_common_param')) { - $vars = urldecode(http_build_query($vars)); + $vars = http_build_query($vars); $url .= $suffix . '?' . $vars . $anchor; } else { + $paramType = Config::get('url_param_type'); foreach ($vars as $var => $val) { if ('' !== trim($val)) { - $url .= $depr . $var . $depr . urlencode($val); + if ($paramType) { + $url .= $depr . urlencode($val); + } else { + $url .= $depr . $var . $depr . urlencode($val); + } } } $url .= $suffix . $anchor; @@ -128,12 +156,14 @@ class Url // 检测域名 $domain = self::parseDomain($url, $domain); // URL组装 - $url = $domain . (self::$root ?: Request::instance()->root()) . '/' . ltrim($url, '/'); + $url = $domain . rtrim(self::$root ?: Request::instance()->root(), '/') . '/' . ltrim($url, '/'); + + self::$bindCheck = false; return $url; } // 直接解析URL地址 - protected static function parseUrl($url, $domain) + protected static function parseUrl($url, &$domain) { $request = Request::instance(); if (0 === strpos($url, '/')) { @@ -149,26 +179,52 @@ class Url // 解析到 模块/控制器/操作 $module = $request->module(); $domains = Route::rules('domain'); - if (isset($domains[$domain]['[bind]'][0])) { - $bindModule = $domains[$domain]['[bind]'][0]; - if ($bindModule && !in_array($bindModule[0], ['\\', '@'])) { - $module = ''; + if (true === $domain && 2 == substr_count($url, '/')) { + $current = $request->host(); + $match = []; + $pos = []; + foreach ($domains as $key => $item) { + if (isset($item['[bind]']) && 0 === strpos($url, $item['[bind]'][0])) { + $pos[$key] = strlen($item['[bind]'][0]) + 1; + $match[] = $key; + $module = ''; + } + } + if ($match) { + $domain = current($match); + foreach ($match as $item) { + if (0 === strpos($current, $item)) { + $domain = $item; + } + } + self::$bindCheck = true; + $url = substr($url, $pos[$domain]); + } + } elseif ($domain) { + if (isset($domains[$domain]['[bind]'][0])) { + $bindModule = $domains[$domain]['[bind]'][0]; + if ($bindModule && !in_array($bindModule[0], ['\\', '@'])) { + $module = ''; + } } - } else { - $module = $module ? $module . '/' : ''; } + $module = $module ? $module . '/' : ''; - $controller = Loader::parseName($request->controller()); + $controller = $request->controller(); if ('' == $url) { // 空字符串输出当前的 模块/控制器/操作 - $url = $module . $controller . '/' . $request->action(); + $action = $request->action(); } else { $path = explode('/', $url); - $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)); + $action = array_pop($path); + $controller = empty($path) ? $controller : array_pop($path); $module = empty($path) ? $module : array_pop($path) . '/'; - $url = $module . $controller . '/' . $action; } + if (Config::get('url_convert')) { + $action = strtolower($action); + $controller = Loader::parseName($controller); + } + $url = $module . $controller . '/' . $action; } return $url; } @@ -179,15 +235,15 @@ class Url if (!$domain) { return ''; } - $request = Request::instance(); + $request = Request::instance(); + $rootDomain = Config::get('url_domain_root'); 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); + $domain = Config::get('app_host') ?: $request->host(); + + $domains = Route::rules('domain'); + if ($domains) { + $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) { @@ -196,13 +252,13 @@ class Url $url = ltrim($url, $rule); $domain = $key; // 生成对应子域名 - if (!empty($urlDomainRoot)) { - $domain .= $urlDomainRoot; + if (!empty($rootDomain)) { + $domain .= $rootDomain; } break; - } else if (false !== strpos($key, '*')) { - if (!empty($urlDomainRoot)) { - $domain .= $urlDomainRoot; + } elseif (false !== strpos($key, '*')) { + if (!empty($rootDomain)) { + $domain .= $rootDomain; } break; } @@ -210,15 +266,22 @@ class Url } } } - } elseif (!strpos($domain, '.')) { - $rootDomain = Config::get('url_domain_root'); + + } else { if (empty($rootDomain)) { - $host = $request->host(); + $host = Config::get('app_host') ?: $request->host(); $rootDomain = substr_count($host, '.') > 1 ? substr(strstr($host, '.'), 1) : $host; } - $domain .= '.' . $rootDomain; + if (substr_count($domain, '.') < 2 && !strpos($domain, $rootDomain)) { + $domain .= '.' . $rootDomain; + } + } + if (false !== strpos($domain, '://')) { + $scheme = ''; + } else { + $scheme = $request->isSsl() || Config::get('is_https') ? 'https://' : 'http://'; } - return ($request->isSsl() ? 'https://' : 'http://') . $domain; + return $scheme . $domain; } // 解析URL后缀 @@ -237,18 +300,19 @@ class Url public static function getRuleUrl($rule, &$vars = []) { foreach ($rule as $item) { - list($url, $pattern, $domain) = $item; + list($url, $pattern, $domain, $suffix) = $item; if (empty($pattern)) { - return [$url, $domain]; + return [rtrim($url, '$'), $domain, $suffix]; } + $type = Config::get('url_common_param'); foreach ($pattern as $key => $val) { if (isset($vars[$key])) { - $url = str_replace(['[:' . $key . ']', '<' . $key . '?>', ':' . $key . '', '<' . $key . '>'], $vars[$key], $url); + $url = str_replace(['[:' . $key . ']', '<' . $key . '?>', ':' . $key . '', '<' . $key . '>'], $type ? $vars[$key] : urlencode($vars[$key]), $url); unset($vars[$key]); - $result = [$url, $domain]; + $result = [$url, $domain, $suffix]; } elseif (2 == $val) { $url = str_replace(['/[:' . $key . ']', '[:' . $key . ']', '<' . $key . '?>'], '', $url); - $result = [$url, $domain]; + $result = [$url, $domain, $suffix]; } else { break; } diff --git a/library/think/Validate.php b/library/think/Validate.php index 5fa15424ca71897fd3d21a369bd68f50f1685918..608e1e4a52aad92e1fa460756aa7fd5951dfc71b 100644 --- a/library/think/Validate.php +++ b/library/think/Validate.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2016 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- @@ -11,9 +11,7 @@ namespace think; -use think\File; -use think\Request; -use think\Session; +use think\exception\ClassNotFoundException; class Validate { @@ -33,57 +31,61 @@ class Validate // 验证提示信息 protected $message = []; + // 验证字段描述 + protected $field = []; // 验证规则默认提示信息 protected static $typeMsg = [ - 'require' => ':attribute不能为空', - 'number' => ':attribute必须是数字', - 'float' => ':attribute必须是浮点数', - 'boolean' => ':attribute必须是布尔值', - 'email' => ':attribute格式不符', - 'array' => ':attribute必须是数组', - 'accepted' => ':attribute必须是yes、on或者1', - 'date' => ':attribute格式不符合', - 'file' => ':attribute不是有效的上传文件', - 'image' => ':attribute不是有效的图像文件', - 'alpha' => ':attribute只能是字母', - 'alphaNum' => ':attribute只能是字母和数字', - 'alphaDash' => ':attribute只能是字母、数字和下划线_及破折号-', - 'activeUrl' => ':attribute不是有效的域名或者IP', - 'chs' => ':attribute只能是汉字', - 'chsAlpha' => ':attribute只能是汉字、字母', - 'chsAlphaNum' => ':attribute只能是汉字、字母和数字', - 'chsDash' => ':attribute只能是汉字、字母、数字和下划线_及破折号-', - 'url' => ':attribute不是有效的URL地址', - 'ip' => ':attribute不是有效的IP地址', - 'dateFormat' => ':attribute必须使用日期格式 :rule', - 'in' => ':attribute必须在 :rule 范围内', - 'notIn' => ':attribute不能在 :rule 范围内', - 'between' => ':attribute只能在 :1 - :2 之间', - 'notBetween' => ':attribute不能在 :1 - :2 之间', - 'length' => ':attribute长度不符合要求 :rule', - 'max' => ':attribute长度不能超过 :rule', - 'min' => ':attribute长度不能小于 :rule', - 'after' => ':attribute日期不能小于 :rule', - 'before' => ':attribute日期不能超过 :rule', - 'expire' => '不在有效期内 :rule', - 'allowIp' => '不允许的IP访问', - 'denyIp' => '禁止的IP访问', - 'confirm' => ':attribute和字段 :rule 不一致', - 'different' => ':attribute和字段 :rule 不能相同', - 'egt' => ':attribute必须大于等于 :rule', - 'gt' => ':attribute必须大于 :rule', - 'elt' => ':attribute必须小于等于 :rule', - 'lt' => ':attribute必须小于 :rule', - 'eq' => ':attribute必须等于 :rule', - 'unique' => ':attribute已存在', - 'regex' => ':attribute不符合指定规则', - 'method' => '无效的请求类型', - 'token' => '令牌数据无效', - 'fileSize' => '上传文件大小不符', - 'fileExt' => '上传文件后缀不符', - 'fileMime' => '上传文件类型不符', - + 'require' => ':attribute require', + 'number' => ':attribute must be numeric', + 'integer' => ':attribute must be integer', + 'float' => ':attribute must be float', + 'boolean' => ':attribute must be bool', + 'email' => ':attribute not a valid email address', + 'array' => ':attribute must be a array', + 'accepted' => ':attribute must be yes,on or 1', + 'date' => ':attribute not a valid datetime', + 'file' => ':attribute not a valid file', + 'image' => ':attribute not a valid image', + 'alpha' => ':attribute must be alpha', + 'alphaNum' => ':attribute must be alpha-numeric', + 'alphaDash' => ':attribute must be alpha-numeric, dash, underscore', + 'activeUrl' => ':attribute not a valid domain or ip', + 'chs' => ':attribute must be chinese', + 'chsAlpha' => ':attribute must be chinese or alpha', + 'chsAlphaNum' => ':attribute must be chinese,alpha-numeric', + 'chsDash' => ':attribute must be chinese,alpha-numeric,underscore, dash', + 'url' => ':attribute not a valid url', + 'ip' => ':attribute not a valid ip', + 'dateFormat' => ':attribute must be dateFormat of :rule', + 'in' => ':attribute must be in :rule', + 'notIn' => ':attribute be notin :rule', + 'between' => ':attribute must between :1 - :2', + 'notBetween' => ':attribute not between :1 - :2', + 'length' => 'size of :attribute must be :rule', + 'max' => 'max size of :attribute must be :rule', + 'min' => 'min size of :attribute must be :rule', + 'after' => ':attribute cannot be less than :rule', + 'before' => ':attribute cannot exceed :rule', + 'afterWith' => ':attribute cannot be less than :rule', + 'beforeWith' => ':attribute cannot exceed :rule', + 'expire' => ':attribute not within :rule', + 'allowIp' => 'access IP is not allowed', + 'denyIp' => 'access IP denied', + 'confirm' => ':attribute out of accord with :2', + 'different' => ':attribute cannot be same with :2', + 'egt' => ':attribute must greater than or equal :rule', + 'gt' => ':attribute must greater than :rule', + 'elt' => ':attribute must less than or equal :rule', + 'lt' => ':attribute must less than :rule', + 'eq' => ':attribute must equal :rule', + 'unique' => ':attribute has exists', + 'regex' => ':attribute not conform to the rules', + 'method' => 'invalid Request method', + 'token' => 'invalid token', + 'fileSize' => 'filesize not match', + 'fileExt' => 'extensions to upload is not allowed', + 'fileMime' => 'mimetype to upload is not allowed', ]; // 当前验证场景 @@ -102,15 +104,17 @@ class Validate protected $batch = false; /** - * 架构函数 + * 构造函数 * @access public * @param array $rules 验证规则 * @param array $message 验证提示信息 + * @param array $field 验证字段描述信息 */ - public function __construct(array $rules = [], $message = []) + public function __construct(array $rules = [], $message = [], $field = []) { $this->rule = array_merge($this->rule, $rules); $this->message = array_merge($this->message, $message); + $this->field = array_merge($this->field, $field); } /** @@ -118,12 +122,13 @@ class Validate * @access public * @param array $rules 验证规则 * @param array $message 验证提示信息 + * @param array $field 验证字段描述信息 * @return Validate */ - public static function make($rules = [], $message = []) + public static function make($rules = [], $message = [], $field = []) { if (is_null(self::$instance)) { - self::$instance = new self($rules, $message); + self::$instance = new self($rules, $message, $field); } return self::$instance; } @@ -162,7 +167,7 @@ class Validate } /** - * 获取验证规则的默认提示信息 + * 设置验证规则的默认提示信息 * @access protected * @param string|array $type 验证规则类型名称或者数组 * @param string $msg 验证提示信息 @@ -215,6 +220,17 @@ class Validate return $this; } + /** + * 判断是否存在某个验证场景 + * @access public + * @param string $name 场景名 + * @return bool + */ + public function hasScene($name) + { + return isset($this->scene[$name]); + } + /** * 设置批量验证 * @access public @@ -279,7 +295,7 @@ class Validate // 字段|描述 用于指定属性名称 list($key, $title) = explode('|', $key); } else { - $title = $key; + $title = isset($this->field[$key]) ? $this->field[$key] : $key; } // 场景检测 @@ -300,7 +316,12 @@ class Validate $value = $this->getDataValue($data, $key); // 字段验证 - $result = $this->checkItem($key, $value, $rule, $data, $title, $msg); + if ($rule instanceof \Closure) { + // 匿名函数验证 支持传入当前字段和所有字段两个数据 + $result = call_user_func_array($rule, [$value, $data]); + } else { + $result = $this->checkItem($key, $value, $rule, $data, $title, $msg); + } if (true !== $result) { // 没有返回true 则表示验证失败 @@ -320,6 +341,41 @@ class Validate return !empty($this->error) ? false : true; } + /** + * 根据验证规则验证数据 + * @access protected + * @param mixed $value 字段值 + * @param mixed $rules 验证规则 + * @return bool + */ + protected function checkRule($value, $rules) + { + if ($rules instanceof \Closure) { + return call_user_func_array($rules, [$value]); + } elseif (is_string($rules)) { + $rules = explode('|', $rules); + } + + foreach ($rules as $key => $rule) { + if ($rule instanceof \Closure) { + $result = call_user_func_array($rule, [$value]); + } else { + // 判断验证类型 + list($type, $rule) = $this->getValidateType($key, $rule); + + $callback = isset(self::$type[$type]) ? self::$type[$type] : [$this, $type]; + + $result = call_user_func_array($callback, [$value, $rule]); + } + + if (true !== $result) { + return $result; + } + } + + return true; + } + /** * 验证单个字段规则 * @access protected @@ -333,74 +389,105 @@ class Validate */ protected function checkItem($field, $value, $rules, $data, $title = '', $msg = []) { - if ($rules instanceof \Closure) { - // 匿名函数验证 支持传入当前字段和所有字段两个数据 - $result = call_user_func_array($rules, [$value, $data]); - } else { - // 支持多规则验证 require|in:a,b,c|... 或者 ['require','in'=>'a,b,c',...] - if (is_string($rules)) { - $rules = explode('|', $rules); - } - $i = 0; - foreach ($rules as $key => $rule) { - if ($rule instanceof \Closure) { - $result = call_user_func_array($rule, [$value, $data]); + // 支持多规则验证 require|in:a,b,c|... 或者 ['require','in'=>'a,b,c',...] + if (is_string($rules)) { + $rules = explode('|', $rules); + } + $i = 0; + foreach ($rules as $key => $rule) { + if ($rule instanceof \Closure) { + $result = call_user_func_array($rule, [$value, $data]); + $info = is_numeric($key) ? '' : $key; + } else { + // 判断验证类型 + list($type, $rule, $info) = $this->getValidateType($key, $rule); + + // 如果不是require 有数据才会行验证 + 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, $title]); } else { - // 判断验证类型 - if (is_numeric($key) && strpos($rule, ':')) { - list($type, $rule) = explode(':', $rule, 2); - if (isset($this->alias[$type])) { - // 判断别名 - $type = $this->alias[$type]; - } - $info = $type; - } elseif (is_numeric($key)) { - $type = 'is'; - $info = $rule; - } else { - $info = $type = $key; - } - - // 如果不是require 有数据才会行验证 - 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]); - } else { - $result = true; - } + $result = true; } + } - if (false === $result) { - // 验证失败 返回错误信息 - if (isset($msg[$i])) { - $message = $msg[$i]; - } else { - $message = $this->getRuleMsg($field, $title, $info, $rule); + if (false === $result) { + // 验证失败 返回错误信息 + if (isset($msg[$i])) { + $message = $msg[$i]; + if (is_string($message) && strpos($message, '{%') === 0) { + $message = Lang::get(substr($message, 2, -1)); } - return $message; - } elseif (true !== $result) { - // 返回自定义错误信息 - return $result; + } else { + $message = $this->getRuleMsg($field, $title, $info, $rule); + } + return $message; + } elseif (true !== $result) { + // 返回自定义错误信息 + if (is_string($result) && false !== strpos($result, ':')) { + $result = str_replace([':attribute', ':rule'], [$title, (string) $rule], $result); } - $i++; + return $result; + } + $i++; + } + return $result; + } + + /** + * 获取当前验证类型及规则 + * @access public + * @param mixed $key + * @param mixed $rule + * @return array + */ + protected function getValidateType($key, $rule) + { + // 判断验证类型 + if (!is_numeric($key)) { + return [$key, $rule, $key]; + } + + if (strpos($rule, ':')) { + list($type, $rule) = explode(':', $rule, 2); + if (isset($this->alias[$type])) { + // 判断别名 + $type = $this->alias[$type]; } + $info = $type; + } elseif (method_exists($this, $rule)) { + $type = $rule; + $info = $rule; + $rule = ''; + } else { + $type = 'is'; + $info = $rule; } - return true !== $result ? $result : true; + + return [$type, $rule, $info]; } /** * 验证是否和某个字段的值一致 * @access protected - * @param mixed $value 字段值 + * @param mixed $value 字段值 * @param mixed $rule 验证规则 * @param array $data 数据 + * @param string $field 字段名 * @return bool */ - protected function confirm($value, $rule, $data) + protected function confirm($value, $rule, $data, $field = '') { - return $this->getDataValue($data, $rule) == $value; + if ('' == $rule) { + if (strpos($field, '_confirm')) { + $rule = strstr($field, '_confirm', true); + } else { + $rule = $field . '_confirm'; + } + } + return $this->getDataValue($data, $rule) === $value; } /** @@ -421,11 +508,13 @@ class Validate * @access protected * @param mixed $value 字段值 * @param mixed $rule 验证规则 + * @param array $data 数据 * @return bool */ - protected function egt($value, $rule) + protected function egt($value, $rule, $data) { - return $value >= $rule; + $val = $this->getDataValue($data, $rule); + return !is_null($val) && $value >= $val; } /** @@ -433,11 +522,13 @@ class Validate * @access protected * @param mixed $value 字段值 * @param mixed $rule 验证规则 + * @param array $data 数据 * @return bool */ - protected function gt($value, $rule) + protected function gt($value, $rule, $data) { - return $value > $rule; + $val = $this->getDataValue($data, $rule); + return !is_null($val) && $value > $val; } /** @@ -445,11 +536,13 @@ class Validate * @access protected * @param mixed $value 字段值 * @param mixed $rule 验证规则 + * @param array $data 数据 * @return bool */ - protected function elt($value, $rule) + protected function elt($value, $rule, $data) { - return $value <= $rule; + $val = $this->getDataValue($data, $rule); + return !is_null($val) && $value <= $val; } /** @@ -457,11 +550,13 @@ class Validate * @access protected * @param mixed $value 字段值 * @param mixed $rule 验证规则 + * @param array $data 数据 * @return bool */ - protected function lt($value, $rule) + protected function lt($value, $rule, $data) { - return $value < $rule; + $val = $this->getDataValue($data, $rule); + return !is_null($val) && $value < $val; } /** @@ -533,7 +628,7 @@ class Validate break; case 'ip': // 是否为IP地址 - $result = $this->filter($value, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 | FILTER_FLAG_IPV6); + $result = $this->filter($value, [FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 | FILTER_FLAG_IPV6]); break; case 'url': // 是否为一个URL地址 @@ -556,7 +651,7 @@ class Validate break; case 'boolean': // 是否为布尔值 - $result = in_array($value, [0, 1, true, false]); + $result = in_array($value, [true, false, 0, 1, '0', '1'], true); break; case 'array': // 是否为数组 @@ -589,8 +684,12 @@ class Validate if (function_exists('exif_imagetype')) { return exif_imagetype($image); } else { - $info = getimagesize($image); - return $info[2]; + try { + $info = getimagesize($image); + return $info ? $info[2] : false; + } catch (\Exception $e) { + return false; + } } } @@ -603,6 +702,9 @@ class Validate */ protected function activeUrl($value, $rule) { + if (!in_array($rule, ['A', 'MX', 'NS', 'SOA', 'PTR', 'CNAME', 'AAAA', 'A6', 'SRV', 'NAPTR', 'TXT', 'ANY'])) { + $rule = 'MX'; + } return checkdnsrr($value, $rule); } @@ -618,7 +720,7 @@ class Validate if (!in_array($rule, ['ipv4', 'ipv6'])) { $rule = 'ipv4'; } - return $this->filter($value, FILTER_VALIDATE_IP, 'ipv6' == $rule ? FILTER_FLAG_IPV6 : FILTER_FLAG_IPV4); + return $this->filter($value, [FILTER_VALIDATE_IP, 'ipv6' == $rule ? FILTER_FLAG_IPV6 : FILTER_FLAG_IPV4]); } /** @@ -630,21 +732,17 @@ class Validate */ protected function fileExt($file, $rule) { - if (!($file instanceof File)) { - return false; - } - if (is_string($rule)) { - $rule = explode(',', $rule); - } if (is_array($file)) { foreach ($file as $item) { - if (!$item->checkExt($rule)) { + if (!($item instanceof File) || !$item->checkExt($rule)) { return false; } } return true; - } else { + } elseif ($file instanceof File) { return $file->checkExt($rule); + } else { + return false; } } @@ -657,21 +755,17 @@ class Validate */ protected function fileMime($file, $rule) { - if (!($file instanceof File)) { - return false; - } - if (is_string($rule)) { - $rule = explode(',', $rule); - } if (is_array($file)) { foreach ($file as $item) { - if (!$item->checkMime($rule)) { + if (!($item instanceof File) || !$item->checkMime($rule)) { return false; } } return true; - } else { + } elseif ($file instanceof File) { return $file->checkMime($rule); + } else { + return false; } } @@ -684,18 +778,17 @@ class Validate */ protected function fileSize($file, $rule) { - if (!($file instanceof File)) { - return false; - } if (is_array($file)) { foreach ($file as $item) { - if (!$item->checkSize($rule)) { + if (!($item instanceof File) || !$item->checkSize($rule)) { return false; } } return true; - } else { + } elseif ($file instanceof File) { return $file->checkSize($rule); + } else { + return false; } } @@ -711,19 +804,24 @@ class Validate if (!($file instanceof File)) { return false; } - $rule = explode(',', $rule); - list($width, $height, $type) = getimagesize($file->getRealPath()); - if (isset($rule[2])) { - $imageType = strtolower($rule[2]); - if ('jpeg' == $imageType) { - $imageType = 'jpg'; - } - if (image_type_to_extension($type, false) != $imageType) { - return false; + if ($rule) { + $rule = explode(',', $rule); + list($width, $height, $type) = getimagesize($file->getRealPath()); + if (isset($rule[2])) { + $imageType = strtolower($rule[2]); + if ('jpeg' == $imageType) { + $imageType = 'jpg'; + } + if (image_type_to_extension($type, false) != $imageType) { + return false; + } } + + list($w, $h) = $rule; + return $w == $width && $h == $height; + } else { + return in_array($this->getImageType($file->getRealPath()), [1, 2, 3, 6]); } - list($w, $h) = $rule; - return $w == $width && $h == $height; } /** @@ -766,28 +864,42 @@ class Validate if (is_string($rule)) { $rule = explode(',', $rule); } - $db = Db::name($rule[0]); + if (false !== strpos($rule[0], '\\')) { + // 指定模型类 + $db = new $rule[0]; + } else { + try { + $db = Loader::model($rule[0]); + } catch (ClassNotFoundException $e) { + $db = Db::name($rule[0]); + } + } $key = isset($rule[1]) ? $rule[1] : $field; if (strpos($key, '^')) { // 支持多个字段验证 $fields = explode('^', $key); foreach ($fields as $key) { - $map[$key] = $data[$key]; + if (isset($data[$key])) { + $map[$key] = $data[$key]; + } } } elseif (strpos($key, '=')) { parse_str($key, $map); - } else { + } elseif (isset($data[$field])) { $map[$key] = $data[$field]; + } else { + $map = []; } - $pk = strval(isset($rule[3]) ? $rule[3] : $db->getPk()); - if (isset($rule[2])) { - $map[$pk] = ['neq', $rule[2]]; - } elseif (isset($data[$pk])) { - $map[$pk] = ['neq', $data[$pk]]; + $pk = isset($rule[3]) ? $rule[3] : $db->getPk(); + if (is_string($pk)) { + if (isset($rule[2])) { + $map[$pk] = ['neq', $rule[2]]; + } elseif (isset($data[$pk])) { + $map[$pk] = ['neq', $data[$pk]]; + } } - if ($db->where($map)->field($pk)->find()) { return false; } @@ -820,6 +932,7 @@ class Validate list($rule, $param) = explode(',', $rule); } elseif (is_array($rule)) { $param = isset($rule[1]) ? $rule[1] : null; + $rule = $rule[0]; } else { $param = null; } @@ -838,7 +951,7 @@ class Validate { list($field, $val) = explode(',', $rule); if ($this->getDataValue($data, $field) == $val) { - return !empty($value); + return !empty($value) || '0' == $value; } else { return true; } @@ -856,7 +969,7 @@ class Validate { $result = call_user_func_array($rule, [$value, $data]); if ($result) { - return !empty($value); + return !empty($value) || '0' == $value; } else { return true; } @@ -874,7 +987,7 @@ class Validate { $val = $this->getDataValue($data, $rule); if (!empty($val)) { - return !empty($value); + return !empty($value) || '0' == $value; } else { return true; } @@ -1006,9 +1119,10 @@ class Validate * @access protected * @param mixed $value 字段值 * @param mixed $rule 验证规则 + * @param array $data 数据 * @return bool */ - protected function after($value, $rule) + protected function after($value, $rule, $data) { return strtotime($value) >= strtotime($rule); } @@ -1018,13 +1132,42 @@ class Validate * @access protected * @param mixed $value 字段值 * @param mixed $rule 验证规则 + * @param array $data 数据 * @return bool */ - protected function before($value, $rule) + protected function before($value, $rule, $data) { return strtotime($value) <= strtotime($rule); } + /** + * 验证日期字段 + * @access protected + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 + * @param array $data 数据 + * @return bool + */ + protected function afterWith($value, $rule, $data) + { + $rule = $this->getDataValue($data, $rule); + return !is_null($rule) && strtotime($value) >= strtotime($rule); + } + + /** + * 验证日期字段 + * @access protected + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 + * @param array $data 数据 + * @return bool + */ + protected function beforeWith($value, $rule, $data) + { + $rule = $this->getDataValue($data, $rule); + return !is_null($rule) && strtotime($value) <= strtotime($rule); + } + /** * 验证有效期 * @access protected @@ -1088,7 +1231,7 @@ class Validate // 不是正则表达式则两端补上/ $rule = '/^' . $rule . '$/'; } - return 1 === preg_match($rule, (string) $value); + return is_scalar($value) && 1 === preg_match($rule, (string) $value); } /** @@ -1127,13 +1270,15 @@ class Validate /** * 获取数据值 * @access protected - * @param array $data 数据 - * @param string $key 数据标识 支持二维 + * @param array $data 数据 + * @param string $key 数据标识 支持二维 * @return mixed */ protected function getDataValue($data, $key) { - if (strpos($key, '.')) { + if (is_numeric($key)) { + $value = $key; + } elseif (strpos($key, '.')) { // 支持二维数组验证 list($name1, $name2) = explode('.', $key); $value = isset($data[$name1][$name2]) ? $data[$name1][$name2] : null; @@ -1156,17 +1301,27 @@ class Validate { if (isset($this->message[$attribute . '.' . $type])) { $msg = $this->message[$attribute . '.' . $type]; + } elseif (isset($this->message[$attribute][$type])) { + $msg = $this->message[$attribute][$type]; } elseif (isset($this->message[$attribute])) { $msg = $this->message[$attribute]; } elseif (isset(self::$typeMsg[$type])) { $msg = self::$typeMsg[$type]; + } elseif (0 === strpos($type, 'require')) { + $msg = self::$typeMsg['require']; } else { - $msg = $title . '规则错误'; + $msg = $title . Lang::get('not conform to the rules'); + } + + if (is_string($msg) && 0 === strpos($msg, '{%')) { + $msg = Lang::get(substr($msg, 2, -1)); + } elseif (Lang::has($msg)) { + $msg = Lang::get($msg); } - // TODO 多语言支持 - if (is_string($msg) && false !== strpos($msg, ':')) { + + if (is_string($msg) && is_scalar($rule) && false !== strpos($msg, ':')) { // 变量替换 - if (strpos($rule, ',')) { + if (is_string($rule) && strpos($rule, ',')) { $array = array_pad(explode(',', $rule), 3, ''); } else { $array = array_pad([], 3, ''); @@ -1206,7 +1361,7 @@ class Validate public static function __callStatic($method, $params) { - $class = new static; + $class = self::make(); if (method_exists($class, $method)) { return call_user_func_array([$class, $method], $params); } else { diff --git a/library/think/View.php b/library/think/View.php index da087036dcd2376c785cb4be141aedc7acedc9ea..ca2dadbb6a345f7c390ee7c2d65d79647de597e4 100644 --- a/library/think/View.php +++ b/library/think/View.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2016 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- @@ -19,11 +19,13 @@ class View public $engine; // 模板变量 protected $data = []; + // 用于静态赋值的模板变量 + protected static $var = []; // 视图输出替换 protected $replace = []; /** - * 架构函数 + * 构造函数 * @access public * @param array $engine 模板引擎参数 * @param array $replace 字符串替换参数 @@ -31,8 +33,22 @@ class View public function __construct($engine = [], $replace = []) { // 初始化模板引擎 - $this->engine((array) $engine); - $this->replace = $replace; + $this->engine($engine); + // 基础替换字符串 + $request = Request::instance(); + $base = $request->root(); + $root = strpos($base, '.') ? ltrim(dirname($base), DS) : $base; + if ('' != $root) { + $root = '/' . ltrim($root, '/'); + } + $baseReplace = [ + '__ROOT__' => $root, + '__URL__' => $base . '/' . $request->module() . '/' . Loader::parseName($request->controller()), + '__STATIC__' => $root . '/static', + '__CSS__' => $root . '/static/css', + '__JS__' => $root . '/static/js', + ]; + $this->replace = array_merge($baseReplace, (array) $replace); } /** @@ -50,6 +66,22 @@ class View return self::$instance; } + /** + * 模板变量静态赋值 + * @access public + * @param mixed $name 变量名 + * @param mixed $value 变量值 + * @return void + */ + public static function share($name, $value = '') + { + if (is_array($name)) { + self::$var = array_merge(self::$var, $name); + } else { + self::$var[$name] = $value; + } + } + /** * 模板变量赋值 * @access public @@ -95,7 +127,7 @@ class View * @access private * @param string|array $name 参数名 * @param mixed $value 参数值 - * @return void + * @return $this */ public function config($name, $value = null) { @@ -116,25 +148,28 @@ class View public function fetch($template = '', $vars = [], $replace = [], $config = [], $renderContent = false) { // 模板变量 - $vars = array_merge($this->data, $vars); + $vars = array_merge(self::$var, $this->data, $vars); // 页面缓存 ob_start(); ob_implicit_flush(0); // 渲染输出 - $method = $renderContent ? 'display' : 'fetch'; - $this->engine->$method($template, $vars, $config); + try { + $method = $renderContent ? 'display' : 'fetch'; + // 允许用户自定义模板的字符串替换 + $replace = array_merge($this->replace, $replace, (array) $this->engine->config('tpl_replace_string')); + $this->engine->config('tpl_replace_string', $replace); + $this->engine->$method($template, $vars, $config); + } catch (\Exception $e) { + ob_end_clean(); + throw $e; + } // 获取并清空缓存 $content = ob_get_clean(); // 内容过滤标签 Hook::listen('view_filter', $content); - // 允许用户自定义模板的字符串替换 - $replace = array_merge($this->replace, $replace); - if (!empty($replace)) { - $content = strtr($content, $replace); - } return $content; } diff --git a/library/think/cache/Driver.php b/library/think/cache/Driver.php index e0aeb7bc2e84628c3a46acfb3ba15d7a3a57d025..07805e485e4d7565eeb2984c9527f34e2919aeb0 100644 --- a/library/think/cache/Driver.php +++ b/library/think/cache/Driver.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2016 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- @@ -105,10 +105,50 @@ abstract class Driver $this->rm($name); return $result; } else { - return null; + return; } } + /** + * 如果不存在则写入缓存 + * @access public + * @param string $name 缓存变量名 + * @param mixed $value 存储数据 + * @param int $expire 有效时间 0为永久 + * @return mixed + */ + public function remember($name, $value, $expire = null) + { + if (!$this->has($name)) { + $time = time(); + while ($time + 5 > time() && $this->has($name . '_lock')) { + // 存在锁定则等待 + usleep(200000); + } + + try { + // 锁定 + $this->set($name . '_lock', true); + if ($value instanceof \Closure) { + $value = call_user_func($value); + } + $this->set($name, $value, $expire); + // 解锁 + $this->rm($name . '_lock'); + } catch (\Exception $e) { + // 解锁 + $this->rm($name . '_lock'); + throw $e; + } catch (\throwable $e) { + $this->rm($name . '_lock'); + throw $e; + } + } else { + $value = $this->get($name); + } + return $value; + } + /** * 缓存标签 * @access public @@ -119,7 +159,9 @@ abstract class Driver */ public function tag($name, $keys = null, $overlay = false) { - if (is_null($keys)) { + if (is_null($name)) { + + } elseif (is_null($keys)) { $this->tag = $name; } else { $key = 'tag_' . md5($name); @@ -132,7 +174,7 @@ abstract class Driver } else { $value = array_unique(array_merge($this->getTagItem($name), $keys)); } - $this->set($key, implode(',', $value)); + $this->set($key, implode(',', $value), 0); } return $this; } @@ -149,12 +191,13 @@ abstract class Driver $key = 'tag_' . md5($this->tag); $this->tag = null; if ($this->has($key)) { - $value = $this->get($key); - $value .= ',' . $name; + $value = explode(',', $this->get($key)); + $value[] = $name; + $value = implode(',', array_unique($value)); } else { $value = $name; } - $this->set($key, $value); + $this->set($key, $value, 0); } } @@ -169,7 +212,7 @@ abstract class Driver $key = 'tag_' . md5($tag); $value = $this->get($key); if ($value) { - return explode(',', $value); + return array_filter(explode(',', $value)); } else { return []; } diff --git a/library/think/cache/driver/File.php b/library/think/cache/driver/File.php index 5c98d799c30309ade0b4b8c7676d4fdc2204cb52..fee64894a698a1bb8ac3233296160ac40ffa7499 100644 --- a/library/think/cache/driver/File.php +++ b/library/think/cache/driver/File.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2016 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- @@ -21,14 +21,16 @@ class File extends Driver { protected $options = [ 'expire' => 0, - 'cache_subdir' => false, + 'cache_subdir' => true, 'prefix' => '', 'path' => CACHE_PATH, 'data_compress' => false, ]; + protected $expire; + /** - * 架构函数 + * 构造函数 * @param array $options */ public function __construct($options = []) @@ -61,10 +63,11 @@ class File extends Driver /** * 取得变量的存储文件名 * @access protected - * @param string $name 缓存变量名 + * @param string $name 缓存变量名 + * @param bool $auto 是否自动创建目录 * @return string */ - protected function getCacheKey($name) + protected function getCacheKey($name, $auto = false) { $name = md5($name); if ($this->options['cache_subdir']) { @@ -76,7 +79,8 @@ class File extends Driver } $filename = $this->options['path'] . $name . '.php'; $dir = dirname($filename); - if (!is_dir($dir)) { + + if ($auto && !is_dir($dir)) { mkdir($dir, 0755, true); } return $filename; @@ -106,15 +110,15 @@ class File extends Driver if (!is_file($filename)) { return $default; } - $content = file_get_contents($filename); + $content = file_get_contents($filename); + $this->expire = null; if (false !== $content) { $expire = (int) substr($content, 8, 12); - if (0 != $expire && $_SERVER['REQUEST_TIME'] > filemtime($filename) + $expire) { - //缓存过期删除缓存文件 - $this->unlink($filename); + if (0 != $expire && time() > filemtime($filename) + $expire) { return $default; } - $content = substr($content, 20, -3); + $this->expire = $expire; + $content = substr($content, 32); if ($this->options['data_compress'] && function_exists('gzcompress')) { //启用数据压缩 $content = gzuncompress($content); @@ -129,9 +133,9 @@ class File extends Driver /** * 写入缓存 * @access public - * @param string $name 缓存变量名 - * @param mixed $value 存储数据 - * @param int $expire 有效时间 0为永久 + * @param string $name 缓存变量名 + * @param mixed $value 存储数据 + * @param integer|\DateTime $expire 有效时间(秒) * @return boolean */ public function set($name, $value, $expire = null) @@ -139,7 +143,10 @@ class File extends Driver if (is_null($expire)) { $expire = $this->options['expire']; } - $filename = $this->getCacheKey($name); + if ($expire instanceof \DateTime) { + $expire = $expire->getTimestamp() - time(); + } + $filename = $this->getCacheKey($name, true); if ($this->tag && !is_file($filename)) { $first = true; } @@ -148,7 +155,7 @@ class File extends Driver //数据压缩 $data = gzcompress($data, 3); } - $data = ""; + $data = "\n" . $data; $result = file_put_contents($filename, $data); if ($result) { isset($first) && $this->setTagItem($filename); @@ -169,11 +176,14 @@ class File extends Driver public function inc($name, $step = 1) { if ($this->has($name)) { - $value = $this->get($name) + $step; + $value = $this->get($name) + $step; + $expire = $this->expire; } else { - $value = $step; + $value = $step; + $expire = 0; } - return $this->set($name, $value, 0) ? $value : false; + + return $this->set($name, $value, $expire) ? $value : false; } /** @@ -186,11 +196,14 @@ class File extends Driver public function dec($name, $step = 1) { if ($this->has($name)) { - $value = $this->get($name) - $step; + $value = $this->get($name) - $step; + $expire = $this->expire; } else { - $value = $step; + $value = -$step; + $expire = 0; } - return $this->set($name, $value, 0) ? $value : false; + + return $this->set($name, $value, $expire) ? $value : false; } /** @@ -201,7 +214,11 @@ class File extends Driver */ public function rm($name) { - return $this->unlink($this->getCacheKey($name)); + $filename = $this->getCacheKey($name); + try { + return $this->unlink($filename); + } catch (\Exception $e) { + } } /** @@ -224,7 +241,11 @@ class File extends Driver $files = (array) glob($this->options['path'] . ($this->options['prefix'] ? $this->options['prefix'] . DS : '') . '*'); foreach ($files as $path) { if (is_dir($path)) { - array_map('unlink', glob($path . '/*.php')); + $matches = glob($path . '/*.php'); + if (is_array($matches)) { + array_map('unlink', $matches); + } + rmdir($path); } else { unlink($path); } diff --git a/library/think/cache/driver/Lite.php b/library/think/cache/driver/Lite.php index b9d100971e5c25120738d896a09304c0557f7b75..8cbf08f951a2a30b55500403e644a0fe3f6bab7a 100644 --- a/library/think/cache/driver/Lite.php +++ b/library/think/cache/driver/Lite.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2016 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- @@ -26,7 +26,7 @@ class Lite extends Driver ]; /** - * 架构函数 + * 构造函数 * @access public * * @param array $options @@ -77,7 +77,7 @@ class Lite extends Driver if (is_file($filename)) { // 判断是否过期 $mtime = filemtime($filename); - if ($mtime < $_SERVER['REQUEST_TIME']) { + if ($mtime < time()) { // 清除已经过期的文件 unlink($filename); return $default; @@ -91,9 +91,9 @@ class Lite extends Driver /** * 写入缓存 * @access public - * @param string $name 缓存变量名 - * @param mixed $value 存储数据 - * @param int $expire 有效时间 0为永久 + * @param string $name 缓存变量名 + * @param mixed $value 存储数据 + * @param integer|\DateTime $expire 有效时间(秒) * @return bool */ public function set($name, $value, $expire = null) @@ -101,9 +101,11 @@ class Lite extends Driver if (is_null($expire)) { $expire = $this->options['expire']; } - // 模拟永久 - if (0 === $expire) { - $expire = 10 * 365 * 24 * 3600; + if ($expire instanceof \DateTime) { + $expire = $expire->getTimestamp(); + } else { + $expire = 0 === $expire ? 10 * 365 * 24 * 3600 : $expire; + $expire = time() + $expire; } $filename = $this->getCacheKey($name); if ($this->tag && !is_file($filename)) { @@ -113,7 +115,7 @@ class Lite extends Driver // 通过设置修改时间实现有效期 if ($ret) { isset($first) && $this->setTagItem($filename); - touch($filename, $_SERVER['REQUEST_TIME'] + $expire); + touch($filename, $expire); } return $ret; } @@ -147,7 +149,7 @@ class Lite extends Driver if ($this->has($name)) { $value = $this->get($name) - $step; } else { - $value = $step; + $value = -$step; } return $this->set($name, $value, 0) ? $value : false; } diff --git a/library/think/cache/driver/Memcache.php b/library/think/cache/driver/Memcache.php index bddf71573472e7586978fa86aa522b911925135e..58703ea3474856b9d0bcf77792ae6e8fa541c28e 100644 --- a/library/think/cache/driver/Memcache.php +++ b/library/think/cache/driver/Memcache.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2016 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- @@ -12,7 +12,6 @@ namespace think\cache\driver; use think\cache\Driver; -use think\Exception; class Memcache extends Driver { @@ -26,7 +25,7 @@ class Memcache extends Driver ]; /** - * 架构函数 + * 构造函数 * @param array $options 缓存参数 * @access public * @throws \BadFunctionCallException @@ -64,7 +63,7 @@ class Memcache extends Driver public function has($name) { $key = $this->getCacheKey($name); - return $this->handler->get($key) ? true : false; + return false !== $this->handler->get($key); } /** @@ -83,9 +82,9 @@ class Memcache extends Driver /** * 写入缓存 * @access public - * @param string $name 缓存变量名 - * @param mixed $value 存储数据 - * @param integer $expire 有效时间(秒) + * @param string $name 缓存变量名 + * @param mixed $value 存储数据 + * @param integer|\DateTime $expire 有效时间(秒) * @return bool */ public function set($name, $value, $expire = null) @@ -93,6 +92,9 @@ class Memcache extends Driver if (is_null($expire)) { $expire = $this->options['expire']; } + if ($expire instanceof \DateTime) { + $expire = $expire->getTimestamp() - time(); + } if ($this->tag && !$this->has($name)) { $first = true; } @@ -114,7 +116,10 @@ class Memcache extends Driver public function inc($name, $step = 1) { $key = $this->getCacheKey($name); - return $this->handler->increment($key, $step); + if ($this->handler->get($key)) { + return $this->handler->increment($key, $step); + } + return $this->handler->set($key, $step); } /** diff --git a/library/think/cache/driver/Memcached.php b/library/think/cache/driver/Memcached.php index 4c4505b0822fb4bed8e58acf8c6e38ad2af3a076..5aab5a306b64817b02a8b2140164ad198856464c 100644 --- a/library/think/cache/driver/Memcached.php +++ b/library/think/cache/driver/Memcached.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2016 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- @@ -27,7 +27,7 @@ class Memcached extends Driver ]; /** - * 架构函数 + * 构造函数 * @param array $options 缓存参数 * @access public */ @@ -93,9 +93,9 @@ class Memcached extends Driver /** * 写入缓存 * @access public - * @param string $name 缓存变量名 - * @param mixed $value 存储数据 - * @param integer $expire 有效时间(秒) + * @param string $name 缓存变量名 + * @param mixed $value 存储数据 + * @param integer|\DateTime $expire 有效时间(秒) * @return bool */ public function set($name, $value, $expire = null) @@ -103,6 +103,9 @@ class Memcached extends Driver if (is_null($expire)) { $expire = $this->options['expire']; } + if ($expire instanceof \DateTime) { + $expire = $expire->getTimestamp() - time(); + } if ($this->tag && !$this->has($name)) { $first = true; } @@ -125,7 +128,10 @@ class Memcached extends Driver public function inc($name, $step = 1) { $key = $this->getCacheKey($name); - return $this->handler->increment($key, $step); + if ($this->handler->get($key)) { + return $this->handler->increment($key, $step); + } + return $this->handler->set($key, $step); } /** diff --git a/library/think/cache/driver/Redis.php b/library/think/cache/driver/Redis.php index e7f7876261c014b39c48f32d5f4c200d07b38ee3..027b3ea26852159323b838bd416df5fe1e559e19 100644 --- a/library/think/cache/driver/Redis.php +++ b/library/think/cache/driver/Redis.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2016 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- @@ -34,7 +34,7 @@ class Redis extends Driver ]; /** - * 架构函数 + * 构造函数 * @param array $options 缓存参数 * @access public */ @@ -46,9 +46,12 @@ class Redis extends Driver if (!empty($options)) { $this->options = array_merge($this->options, $options); } - $func = $this->options['persistent'] ? 'pconnect' : 'connect'; $this->handler = new \Redis; - $this->handler->$func($this->options['host'], $this->options['port'], $this->options['timeout']); + if ($this->options['persistent']) { + $this->handler->pconnect($this->options['host'], $this->options['port'], $this->options['timeout'], 'persistent_id_' . $this->options['select']); + } else { + $this->handler->connect($this->options['host'], $this->options['port'], $this->options['timeout']); + } if ('' != $this->options['password']) { $this->handler->auth($this->options['password']); @@ -67,7 +70,7 @@ class Redis extends Driver */ public function has($name) { - return $this->handler->get($this->getCacheKey($name)) ? true : false; + return $this->handler->exists($this->getCacheKey($name)); } /** @@ -80,20 +83,25 @@ class Redis extends Driver public function get($name, $default = false) { $value = $this->handler->get($this->getCacheKey($name)); - if (is_null($value)) { + if (is_null($value) || false === $value) { return $default; } - $jsonData = json_decode($value, true); - // 检测是否为JSON数据 true 返回JSON解析数组, false返回源数据 byron sampson - return (null === $jsonData) ? $value : $jsonData; + + try { + $result = 0 === strpos($value, 'think_serialize:') ? unserialize(substr($value, 16)) : $value; + } catch (\Exception $e) { + $result = $default; + } + + return $result; } /** * 写入缓存 * @access public - * @param string $name 缓存变量名 - * @param mixed $value 存储数据 - * @param integer $expire 有效时间(秒) + * @param string $name 缓存变量名 + * @param mixed $value 存储数据 + * @param integer|\DateTime $expire 有效时间(秒) * @return boolean */ public function set($name, $value, $expire = null) @@ -101,13 +109,15 @@ class Redis extends Driver if (is_null($expire)) { $expire = $this->options['expire']; } + if ($expire instanceof \DateTime) { + $expire = $expire->getTimestamp() - time(); + } if ($this->tag && !$this->has($name)) { $first = true; } - $key = $this->getCacheKey($name); - //对数组/对象数据进行缓存处理,保证数据完整性 byron sampson - $value = (is_object($value) || is_array($value)) ? json_encode($value) : $value; - if (is_int($expire) && $expire) { + $key = $this->getCacheKey($name); + $value = is_scalar($value) ? $value : 'think_serialize:' . serialize($value); + if ($expire) { $result = $this->handler->setex($key, $expire, $value); } else { $result = $this->handler->set($key, $value); @@ -119,26 +129,28 @@ class Redis extends Driver /** * 自增缓存(针对数值缓存) * @access public - * @param string $name 缓存变量名 - * @param int $step 步长 + * @param string $name 缓存变量名 + * @param int $step 步长 * @return false|int */ public function inc($name, $step = 1) { $key = $this->getCacheKey($name); + return $this->handler->incrby($key, $step); } /** * 自减缓存(针对数值缓存) * @access public - * @param string $name 缓存变量名 - * @param int $step 步长 + * @param string $name 缓存变量名 + * @param int $step 步长 * @return false|int */ public function dec($name, $step = 1) { $key = $this->getCacheKey($name); + return $this->handler->decrby($key, $step); } diff --git a/library/think/cache/driver/Sqlite.php b/library/think/cache/driver/Sqlite.php index 6dbd41fed15393aab11c7f489837528c8ca0ede6..dc2ee0559f99bea089b18288bfc3abcc040a1432 100644 --- a/library/think/cache/driver/Sqlite.php +++ b/library/think/cache/driver/Sqlite.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2016 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- @@ -12,7 +12,6 @@ namespace think\cache\driver; use think\cache\Driver; -use think\Exception; /** * Sqlite缓存驱动 @@ -29,7 +28,7 @@ class Sqlite extends Driver ]; /** - * 架构函数 + * 构造函数 * @param array $options 缓存参数 * @throws \BadFunctionCallException * @access public @@ -97,9 +96,9 @@ class Sqlite extends Driver /** * 写入缓存 * @access public - * @param string $name 缓存变量名 - * @param mixed $value 存储数据 - * @param integer $expire 有效时间(秒) + * @param string $name 缓存变量名 + * @param mixed $value 存储数据 + * @param integer|\DateTime $expire 有效时间(秒) * @return boolean */ public function set($name, $value, $expire = null) @@ -109,7 +108,11 @@ class Sqlite extends Driver if (is_null($expire)) { $expire = $this->options['expire']; } - $expire = (0 == $expire) ? 0 : ($_SERVER['REQUEST_TIME'] + $expire); //缓存有效期为0表示永久缓存 + if ($expire instanceof \DateTime) { + $expire = $expire->getTimestamp(); + } else { + $expire = (0 == $expire) ? 0 : (time() + $expire); //缓存有效期为0表示永久缓存 + } if (function_exists('gzcompress')) { //数据压缩 $value = gzcompress($value, 3); @@ -156,7 +159,7 @@ class Sqlite extends Driver if ($this->has($name)) { $value = $this->get($name) - $step; } else { - $value = $step; + $value = -$step; } return $this->set($name, $value, 0) ? $value : false; } diff --git a/library/think/cache/driver/Wincache.php b/library/think/cache/driver/Wincache.php index a9cc7d22aae823c70c02350d49f1af005cbe84a9..03f8d357798aed6ff51595363f1759238c84732c 100644 --- a/library/think/cache/driver/Wincache.php +++ b/library/think/cache/driver/Wincache.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2016 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- @@ -12,7 +12,6 @@ namespace think\cache\driver; use think\cache\Driver; -use think\Exception; /** * Wincache缓存驱动 @@ -26,9 +25,9 @@ class Wincache extends Driver ]; /** - * 架构函数 + * 构造函数 * @param array $options 缓存参数 - * @throws Exception + * @throws \BadFunctionCallException * @access public */ public function __construct($options = []) @@ -69,9 +68,9 @@ class Wincache extends Driver /** * 写入缓存 * @access public - * @param string $name 缓存变量名 - * @param mixed $value 存储数据 - * @param integer $expire 有效时间(秒) + * @param string $name 缓存变量名 + * @param mixed $value 存储数据 + * @param integer|\DateTime $expire 有效时间(秒) * @return boolean */ public function set($name, $value, $expire = null) @@ -79,6 +78,9 @@ class Wincache extends Driver if (is_null($expire)) { $expire = $this->options['expire']; } + if ($expire instanceof \DateTime) { + $expire = $expire->getTimestamp() - time(); + } $key = $this->getCacheKey($name); if ($this->tag && !$this->has($name)) { $first = true; diff --git a/library/think/cache/driver/Xcache.php b/library/think/cache/driver/Xcache.php index 6b18b3fc9e3697f34ad3a7c86619583325bca85e..4d94c033ca9fa68ef3b1bb1a947a6276c780a10f 100644 --- a/library/think/cache/driver/Xcache.php +++ b/library/think/cache/driver/Xcache.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2016 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- @@ -12,7 +12,6 @@ namespace think\cache\driver; use think\cache\Driver; -use think\Exception; /** * Xcache缓存驱动 @@ -26,7 +25,7 @@ class Xcache extends Driver ]; /** - * 架构函数 + * 构造函数 * @param array $options 缓存参数 * @access public * @throws \BadFunctionCallException @@ -69,9 +68,9 @@ class Xcache extends Driver /** * 写入缓存 * @access public - * @param string $name 缓存变量名 - * @param mixed $value 存储数据 - * @param integer $expire 有效时间(秒) + * @param string $name 缓存变量名 + * @param mixed $value 存储数据 + * @param integer|\DateTime $expire 有效时间(秒) * @return boolean */ public function set($name, $value, $expire = null) @@ -79,6 +78,9 @@ class Xcache extends Driver if (is_null($expire)) { $expire = $this->options['expire']; } + if ($expire instanceof \DateTime) { + $expire = $expire->getTimestamp() - time(); + } if ($this->tag && !$this->has($name)) { $first = true; } diff --git a/library/think/config/driver/Ini.php b/library/think/config/driver/Ini.php index d8dc558d432bf965e93ce270dc977623294b9288..bcd12b6974f61ed7fd812299dc8e2f0a423dd0ea 100644 --- a/library/think/config/driver/Ini.php +++ b/library/think/config/driver/Ini.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2016 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- diff --git a/library/think/config/driver/Json.php b/library/think/config/driver/Json.php index ec2419f913af6c72e0bc123f9ef13d579666cdd3..479dcc8921c56aa29d105c4f197a03b164a915e0 100644 --- a/library/think/config/driver/Json.php +++ b/library/think/config/driver/Json.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2016 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- diff --git a/library/think/config/driver/Xml.php b/library/think/config/driver/Xml.php index 5bc93015ad3dc41e8dad1218e814ca9c4ff0ecc4..1158519feccfc1713e1822b8e2649385fd66280d 100644 --- a/library/think/config/driver/Xml.php +++ b/library/think/config/driver/Xml.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2016 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- diff --git a/library/think/console/Input.php b/library/think/console/Input.php index 269196eb7436b9d022e8a80abf3b9acbd42164f0..2482dfdc01b8c58fce4a59cefec880416f37eb3e 100644 --- a/library/think/console/Input.php +++ b/library/think/console/Input.php @@ -252,7 +252,7 @@ class Input return $token; } - return null; + return; } /** diff --git a/library/think/console/command/Clear.php b/library/think/console/command/Clear.php index 020febd3a06af525f95dd56275ad6ae88d21e450..1b5102ec2f53b6608b5463d81946f9bb0d77c5c3 100644 --- a/library/think/console/command/Clear.php +++ b/library/think/console/command/Clear.php @@ -10,8 +10,10 @@ // +---------------------------------------------------------------------- namespace think\console\command; +use think\Cache; use think\console\Command; use think\console\Input; +use think\console\input\Argument; use think\console\input\Option; use think\console\Output; @@ -22,23 +24,40 @@ class Clear extends Command // 指令配置 $this ->setName('clear') + ->addArgument('type', Argument::OPTIONAL, 'type to clear', null) ->addOption('path', 'd', Option::VALUE_OPTIONAL, 'path to clear', null) ->setDescription('Clear runtime file'); } protected function execute(Input $input, Output $output) { - $path = $input->getOption('path') ?: RUNTIME_PATH; + $path = $input->getOption('path') ?: RUNTIME_PATH; + + $type = $input->getArgument('type'); + + if ($type == 'route') { + Cache::clear('route_check'); + } else { + if (is_dir($path)) { + $this->clearPath($path); + } + } + + $output->writeln("Clear Successed"); + } + + protected function clearPath($path) + { + $path = realpath($path) . DS; $files = scandir($path); if ($files) { foreach ($files as $file) { if ('.' != $file && '..' != $file && is_dir($path . $file)) { - array_map('unlink', glob($path . $file . '/*.*')); - } elseif (is_file($path . $file)) { + $this->clearPath($path . $file); + } elseif ('.gitignore' != $file && is_file($path . $file)) { unlink($path . $file); } } } - $output->writeln("Clear Successed"); } } diff --git a/library/think/console/command/Help.php b/library/think/console/command/Help.php index eb0858a368052fcf17323487eee5879613664d19..bae2c653312f35f38f72a976d96a7346c621261f 100644 --- a/library/think/console/command/Help.php +++ b/library/think/console/command/Help.php @@ -16,7 +16,6 @@ use think\console\Input; use think\console\input\Argument as InputArgument; use think\console\input\Option as InputOption; use think\console\Output; -use think\console\helper\Descriptor as DescriptorHelper; class Help extends Command { @@ -67,4 +66,4 @@ EOF $this->command = null; } -} \ No newline at end of file +} diff --git a/library/think/console/command/Lists.php b/library/think/console/command/Lists.php index ffbee07c2d0d8b9d5075c483ad51ac7ef51e9f27..084ddaa231ce3b1dd8c0b602387b49da80344c74 100644 --- a/library/think/console/command/Lists.php +++ b/library/think/console/command/Lists.php @@ -71,4 +71,4 @@ EOF new InputOption('raw', null, InputOption::VALUE_NONE, 'To output raw command list') ]); } -} \ No newline at end of file +} diff --git a/library/think/console/command/Make.php b/library/think/console/command/Make.php index b508f8d0b6fc77e104c13203dba55ba1922f35ee..d1daf34fabbaf9acb894a1bec92680d2aea4b794 100644 --- a/library/think/console/command/Make.php +++ b/library/think/console/command/Make.php @@ -11,6 +11,7 @@ namespace think\console\command; +use think\App; use think\Config; use think\console\Command; use think\console\Input; @@ -64,21 +65,21 @@ abstract class Make extends Command return str_replace(['{%className%}', '{%namespace%}', '{%app_namespace%}'], [ $class, $namespace, - Config::get('app_namespace') + App::$namespace, ], $stub); } protected function getPathName($name) { - $name = str_replace(Config::get('app_namespace') . '\\', '', $name); + $name = str_replace(App::$namespace . '\\', '', $name); return APP_PATH . str_replace('\\', '/', $name) . '.php'; } protected function getClassName($name) { - $appNamespace = Config::get('app_namespace'); + $appNamespace = App::$namespace; if (strpos($name, $appNamespace . '\\') === 0) { return $name; diff --git a/library/think/console/command/optimize/Autoload.php b/library/think/console/command/optimize/Autoload.php index 0481e1795ec9a44d5520d0bbdd9f7919c5c9746a..6a77c2cbaf5b392f13604b0c13de054acd17476a 100644 --- a/library/think/console/command/optimize/Autoload.php +++ b/library/think/console/command/optimize/Autoload.php @@ -11,6 +11,7 @@ namespace think\console\command\optimize; use think\App; +use think\Config; use think\console\Command; use think\console\Input; use think\console\Output; @@ -32,7 +33,7 @@ class Autoload extends Command /** * 类库映射 */ - + return [ EOF; @@ -42,9 +43,14 @@ EOF; 'think\\' => LIB_PATH . 'think', 'behavior\\' => LIB_PATH . 'behavior', 'traits\\' => LIB_PATH . 'traits', - '' => realpath(rtrim(EXTEND_PATH)) + '' => realpath(rtrim(EXTEND_PATH)), ]; + $root_namespace = Config::get('root_namespace'); + foreach ($root_namespace as $namespace => $dir) { + $namespacesToScan[$namespace . '\\'] = realpath($dir); + } + krsort($namespacesToScan); $classMap = []; foreach ($namespacesToScan as $namespace => $dir) { @@ -84,7 +90,7 @@ EOF; $this->output->writeln( 'Warning: Ambiguous class resolution, "' . $class . '"' . ' was found in both "' . str_replace(["',\n"], [ - '' + '', ], $classMap[$class]) . '" and "' . $path . '", the first will be used.' ); } @@ -96,20 +102,24 @@ EOF; { $baseDir = ''; - $appPath = $this->normalizePath(realpath(APP_PATH)); $libPath = $this->normalizePath(realpath(LIB_PATH)); + $appPath = $this->normalizePath(realpath(APP_PATH)); $extendPath = $this->normalizePath(realpath(EXTEND_PATH)); + $rootPath = $this->normalizePath(realpath(ROOT_PATH)); $path = $this->normalizePath($path); - if (strpos($path, $libPath . '/') === 0) { + if ($libPath !== null && strpos($path, $libPath . '/') === 0) { $path = substr($path, strlen(LIB_PATH)); $baseDir = 'LIB_PATH'; - } elseif (strpos($path, $appPath . '/') === 0) { + } elseif ($appPath !== null && strpos($path, $appPath . '/') === 0) { $path = substr($path, strlen($appPath) + 1); $baseDir = 'APP_PATH'; - } elseif (strpos($path, $extendPath . '/') === 0) { + } elseif ($extendPath !== null && strpos($path, $extendPath . '/') === 0) { $path = substr($path, strlen($extendPath) + 1); $baseDir = 'EXTEND_PATH'; + } elseif ($rootPath !== null && strpos($path, $rootPath . '/') === 0) { + $path = substr($path, strlen($rootPath) + 1); + $baseDir = 'ROOT_PATH'; } if ($path !== false) { @@ -121,6 +131,9 @@ EOF; protected function normalizePath($path) { + if ($path === false) { + return; + } $parts = []; $path = strtr($path, '\\', '/'); $prefix = ''; @@ -278,4 +291,4 @@ EOF; return $classes; } -} \ No newline at end of file +} diff --git a/library/think/console/command/optimize/Config.php b/library/think/console/command/optimize/Config.php index cadfe5ee9351fda7827e158b7cfa2ed9b2bd7214..59c69a8e5162bde5e9507fc240972f2ab457d784 100644 --- a/library/think/console/command/optimize/Config.php +++ b/library/think/console/command/optimize/Config.php @@ -30,7 +30,7 @@ class Config extends Command protected function execute(Input $input, Output $output) { - if ($input->hasArgument('module')) { + if ($input->getArgument('module')) { $module = $input->getArgument('module') . DS; } else { $module = ''; diff --git a/library/think/console/command/optimize/Route.php b/library/think/console/command/optimize/Route.php index 911e4c1474e9a5a8d5f10da300b1d90d4f1c28a1..6da1d9a626613e994ea46d51689e1761beb3b273 100644 --- a/library/think/console/command/optimize/Route.php +++ b/library/think/console/command/optimize/Route.php @@ -27,6 +27,11 @@ class Route extends Command protected function execute(Input $input, Output $output) { + + if (!is_dir(RUNTIME_PATH)) { + @mkdir(RUNTIME_PATH, 0755, true); + } + file_put_contents(RUNTIME_PATH . 'route.php', $this->buildRouteCache()); $output->writeln('Succeed!'); } diff --git a/library/think/console/command/optimize/Schema.php b/library/think/console/command/optimize/Schema.php index a1f40cd9aec124ffb474c8dc53f6957c269f0dec..33534240eefad675f0cb909a84580392e45e6328 100644 --- a/library/think/console/command/optimize/Schema.php +++ b/library/think/console/command/optimize/Schema.php @@ -25,6 +25,7 @@ class Schema extends Command protected function configure() { $this->setName('optimize:schema') + ->addOption('config', null, Option::VALUE_REQUIRED, 'db config .') ->addOption('db', null, Option::VALUE_REQUIRED, 'db name .') ->addOption('table', null, Option::VALUE_REQUIRED, 'table name .') ->addOption('module', null, Option::VALUE_REQUIRED, 'module name .') @@ -36,13 +37,18 @@ class Schema extends Command if (!is_dir(RUNTIME_PATH . 'schema')) { @mkdir(RUNTIME_PATH . 'schema', 0755, true); } + $config = []; + if ($input->hasOption('config')) { + $config = $input->getOption('config'); + } if ($input->hasOption('module')) { $module = $input->getOption('module'); // 读取模型 - $list = scandir(APP_PATH . $module . DS . 'model'); + $path = APP_PATH . $module . DS . 'model'; + $list = is_dir($path) ? scandir($path) : []; $app = App::$namespace; foreach ($list as $file) { - if ('.' == $file || '..' == $file) { + if (0 === strpos($file, '.')) { continue; } $class = '\\' . $app . '\\' . $module . '\\model\\' . pathinfo($file, PATHINFO_FILENAME); @@ -50,20 +56,21 @@ class Schema extends Command } $output->writeln('Succeed!'); return; - } else if ($input->hasOption('table')) { + } elseif ($input->hasOption('table')) { $table = $input->getOption('table'); if (!strpos($table, '.')) { - $dbName = Db::getConfig('database'); + $dbName = Db::connect($config)->getConfig('database'); } $tables[] = $table; } elseif ($input->hasOption('db')) { $dbName = $input->getOption('db'); - $tables = Db::getTables($dbName); + $tables = Db::connect($config)->getTables($dbName); } elseif (!\think\Config::get('app_multi_module')) { $app = App::$namespace; - $list = scandir(APP_PATH . 'model'); + $path = APP_PATH . 'model'; + $list = is_dir($path) ? scandir($path) : []; foreach ($list as $file) { - if ('.' == $file || '..' == $file) { + if (0 === strpos($file, '.')) { continue; } $class = '\\' . $app . '\\model\\' . pathinfo($file, PATHINFO_FILENAME); @@ -72,11 +79,11 @@ class Schema extends Command $output->writeln('Succeed!'); return; } else { - $tables = Db::getTables(); + $tables = Db::connect($config)->getTables(); } $db = isset($dbName) ? $dbName . '.' : ''; - $this->buildDataBaseSchema($tables, $db); + $this->buildDataBaseSchema($tables, $db, $config); $output->writeln('Succeed!'); } @@ -94,16 +101,16 @@ class Schema extends Command } } - protected function buildDataBaseSchema($tables, $db) + protected function buildDataBaseSchema($tables, $db, $config) { if ('' == $db) { - $dbName = Db::getConfig('database') . '.'; + $dbName = Db::connect($config)->getConfig('database') . '.'; } else { $dbName = $db; } foreach ($tables as $table) { $content = 'getFields($db . $table); $content .= var_export($info, true) . ';'; file_put_contents(RUNTIME_PATH . 'schema' . DS . $dbName . $table . EXT, $content); } diff --git a/library/think/console/output/Ask.php b/library/think/console/output/Ask.php index 4d360bce9a38b0bb3bdbfaef6767414fc0251e30..3933eb296b33364547da562fdecb48974df7181d 100644 --- a/library/think/console/output/Ask.php +++ b/library/think/console/output/Ask.php @@ -337,4 +337,4 @@ class Ask return self::$stty = $exitcode === 0; } -} \ No newline at end of file +} diff --git a/library/think/console/output/Descriptor.php b/library/think/console/output/Descriptor.php index 6d98d53c78437bae6d8a888242d6cd83cd9b3859..23dc64812be920d4799e30c719d68ef437f159bb 100644 --- a/library/think/console/output/Descriptor.php +++ b/library/think/console/output/Descriptor.php @@ -69,7 +69,7 @@ class Descriptor * 描述参数 * @param InputArgument $argument * @param array $options - * @return string|mixed + * @return void */ protected function describeInputArgument(InputArgument $argument, array $options = []) { @@ -93,7 +93,7 @@ class Descriptor * 描述选项 * @param InputOption $option * @param array $options - * @return string|mixed + * @return void */ protected function describeInputOption(InputOption $option, array $options = []) { @@ -128,7 +128,7 @@ class Descriptor * 描述输入 * @param InputDefinition $definition * @param array $options - * @return string|mixed + * @return void */ protected function describeInputDefinition(InputDefinition $definition, array $options = []) { @@ -173,7 +173,7 @@ class Descriptor * 描述指令 * @param Command $command * @param array $options - * @return string|mixed + * @return void */ protected function describeCommand(Command $command, array $options = []) { @@ -208,7 +208,7 @@ class Descriptor * 描述控制台 * @param Console $console * @param array $options - * @return string|mixed + * @return void */ protected function describeConsole(Console $console, array $options = []) { diff --git a/library/think/console/output/driver/Console.php b/library/think/console/output/driver/Console.php index af4341b7e20a9f1ea38f4dcdf6268f33ec2b58e4..8f29fd020a7584edd4acd81c3619b48610c5c534 100644 --- a/library/think/console/output/driver/Console.php +++ b/library/think/console/output/driver/Console.php @@ -37,6 +37,11 @@ class Console $this->formatter->setDecorated($decorated); } + public function getFormatter() + { + return $this->formatter; + } + public function setDecorated($decorated) { $this->formatter->setDecorated($decorated); @@ -196,7 +201,7 @@ class Console private function getSttyColumns() { if (!function_exists('proc_open')) { - return null; + return; } $descriptorspec = [1 => ['pipe', 'w'], 2 => ['pipe', 'w']]; @@ -209,7 +214,7 @@ class Console return $info; } - return null; + return; } /** @@ -219,7 +224,7 @@ class Console private function getMode() { if (!function_exists('proc_open')) { - return null; + return; } $descriptorspec = [1 => ['pipe', 'w'], 2 => ['pipe', 'w']]; @@ -234,7 +239,7 @@ class Console return $matches[2] . 'x' . $matches[1]; } } - return null; + return; } private function stringWidth($string) @@ -356,10 +361,10 @@ class Console { if (DIRECTORY_SEPARATOR === '\\') { return - '10.0.10586' === PHP_WINDOWS_VERSION_MAJOR . '.' . PHP_WINDOWS_VERSION_MINOR . '.' . PHP_WINDOWS_VERSION_BUILD - || false !== getenv('ANSICON') - || 'ON' === getenv('ConEmuANSI') - || 'xterm' === getenv('TERM'); + '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($stream); diff --git a/library/think/controller/Rest.php b/library/think/controller/Rest.php index a75cfd375a853f45c7730947d64f363d69fc83ec..43ab2f629483ebf32b945f20470407887740f9f4 100644 --- a/library/think/controller/Rest.php +++ b/library/think/controller/Rest.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2016 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- @@ -32,7 +32,7 @@ abstract class Rest ]; /** - * 架构函数 取得模板对象实例 + * 构造函数 取得模板对象实例 * @access public */ public function __construct() @@ -43,7 +43,7 @@ abstract class Rest if ('' == $ext) { // 自动检测资源类型 $this->type = $request->type(); - } elseif (!preg_match('/\(' . $this->restTypeList . '\)$/i', $ext)) { + } elseif (!preg_match('/(' . $this->restTypeList . ')$/i', $ext)) { // 资源类型非法 则用默认资源类型访问 $this->type = $this->restDefaultType; } else { @@ -51,7 +51,7 @@ abstract class Rest } // 请求方式检测 $method = strtolower($request->method()); - if (false === stripos($this->restMethodList, $method)) { + if (!preg_match('/(' . $this->restMethodList . ')$/i', $method)) { // 请求方式非法 则用默认请求方法 $method = $this->restDefaultMethod; } diff --git a/library/think/controller/Yar.php b/library/think/controller/Yar.php index fcf5ced1b3ec38d1cfda7194ca859a4e034fb2a7..af4e9a1a8cc7022000cece1c475fb4f521ba84af 100644 --- a/library/think/controller/Yar.php +++ b/library/think/controller/Yar.php @@ -18,7 +18,7 @@ abstract class Yar { /** - * 架构函数 + * 构造函数 * @access public */ public function __construct() diff --git a/library/think/db/Builder.php b/library/think/db/Builder.php index 15a240ad0d7fe700fa6db92e9a6b3e637ae16f73..58b45aa88700a1b1c29b5ce473d13fdd98816c6a 100644 --- a/library/think/db/Builder.php +++ b/library/think/db/Builder.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2016 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- @@ -12,9 +12,6 @@ namespace think\db; use PDO; -use think\Db; -use think\db\Connection; -use think\db\Query; use think\Exception; abstract class Builder @@ -25,34 +22,45 @@ abstract class Builder protected $query; // 数据库表达式 - 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']; + protected $exp = ['eq' => '=', 'neq' => '<>', 'gt' => '>', 'egt' => '>=', 'lt' => '<', 'elt' => '<=', 'notlike' => 'NOT LIKE', 'not like' => '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%'; + protected $selectSql = 'SELECT%DISTINCT% %FIELD% FROM %TABLE%%FORCE%%JOIN%%WHERE%%GROUP%%HAVING%%UNION%%ORDER%%LIMIT%%LOCK%%COMMENT%'; protected $insertSql = '%INSERT% INTO %TABLE% (%FIELD%) VALUES (%DATA%) %COMMENT%'; - protected $insertAllSql = 'INSERT INTO %TABLE% (%FIELD%) %DATA% %COMMENT%'; + protected $insertAllSql = '%INSERT% INTO %TABLE% (%FIELD%) %DATA% %COMMENT%'; protected $updateSql = 'UPDATE %TABLE% SET %SET% %JOIN% %WHERE% %ORDER%%LIMIT% %LOCK%%COMMENT%'; protected $deleteSql = 'DELETE FROM %TABLE% %USING% %JOIN% %WHERE% %ORDER%%LIMIT% %LOCK%%COMMENT%'; /** - * 架构函数 + * 构造函数 * @access public - * @param Connection $connection 数据库连接对象实例 + * @param Connection $connection 数据库连接对象实例 + * @param Query $query 数据库查询对象实例 */ - public function __construct(Connection $connection) + public function __construct(Connection $connection, Query $query) { $this->connection = $connection; + $this->query = $query; } /** - * 设置当前的Query对象实例 - * @access protected - * @param Query $query 当前查询对象实例 - * @return void + * 获取当前的连接对象实例 + * @access public + * @return Connection + */ + public function getConnection() + { + return $this->connection; + } + + /** + * 获取当前的Query对象实例 + * @access public + * @return Query */ - public function setQuery(Query $query) + public function getQuery() { - $this->query = $query; + return $this->query; } /** @@ -72,6 +80,7 @@ abstract class Builder * @param array $data 数据 * @param array $options 查询参数 * @return array + * @throws Exception */ protected function parseData($data, $options) { @@ -80,7 +89,7 @@ abstract class Builder } // 获取绑定信息 - $bind = $this->query->getFieldsBind($options); + $bind = $this->query->getFieldsBind($options['table']); if ('*' == $options['field']) { $fields = array_keys($bind); } else { @@ -89,22 +98,43 @@ abstract class Builder $result = []; foreach ($data as $key => $val) { - $item = $this->parseKey($key); - if (!in_array($key, $fields, true)) { + if ('*' != $options['field'] && !in_array($key, $fields, true)) { + continue; + } + + $item = $this->parseKey($key, $options, true); + if ($val instanceof Expression) { + $result[$item] = $val->getValue(); + continue; + } elseif (is_object($val) && method_exists($val, '__toString')) { + // 对象数据写入 + $val = $val->__toString(); + } + if (false === strpos($key, '.') && !in_array($key, $fields, true)) { if ($options['strict']) { throw new Exception('fields not exists:[' . $key . ']'); } - } elseif (isset($val[0]) && 'exp' == $val[0]) { - $result[$item] = $val[1]; } elseif (is_null($val)) { $result[$item] = 'NULL'; + } elseif (is_array($val) && !empty($val)) { + switch (strtolower($val[0])) { + case 'inc': + $result[$item] = $item . '+' . floatval($val[1]); + break; + case 'dec': + $result[$item] = $item . '-' . floatval($val[1]); + break; + case 'exp': + throw new Exception('not support data:[' . $val[0] . ']'); + } } elseif (is_scalar($val)) { // 过滤非标量数据 - if ($this->query->isBind(substr($val, 1))) { + if (0 === strpos($val, ':') && $this->query->isBind(substr($val, 1))) { $result[$item] = $val; } else { - $this->query->bind($key, $val, isset($bind[$key]) ? $bind[$key] : PDO::PARAM_STR); - $result[$item] = ':' . $key; + $key = str_replace('.', '_', $key); + $this->query->bind('data__' . $key, $val, isset($bind[$key]) ? $bind[$key] : PDO::PARAM_STR); + $result[$item] = ':data__' . $key; } } } @@ -115,9 +145,10 @@ abstract class Builder * 字段名分析 * @access protected * @param string $key + * @param array $options * @return string */ - protected function parseKey($key) + protected function parseKey($key, $options = [], $strict = false) { return $key; } @@ -146,10 +177,11 @@ abstract class Builder /** * field分析 * @access protected - * @param mixed $fields + * @param mixed $fields + * @param array $options * @return string */ - protected function parseField($fields) + protected function parseField($fields, $options = []) { if ('*' == $fields || empty($fields)) { $fieldsStr = '*'; @@ -157,10 +189,12 @@ abstract class Builder // 支持 'field1'=>'field2' 这样的字段别名定义 $array = []; foreach ($fields as $key => $field) { - if (!is_numeric($key)) { - $array[] = $this->parseKey($key) . ' AS ' . $this->parseKey($field); + if ($field instanceof Expression) { + $array[] = $field->getValue(); + } elseif (!is_numeric($key)) { + $array[] = $this->parseKey($key, $options) . ' AS ' . $this->parseKey($field, $options, true); } else { - $array[] = $this->parseKey($field); + $array[] = $this->parseKey($field, $options); } } $fieldsStr = implode(',', $array); @@ -171,24 +205,27 @@ abstract class Builder /** * table分析 * @access protected - * @param mixed $table + * @param mixed $tables + * @param array $options * @return string */ - protected function parseTable($tables) + protected function parseTable($tables, $options = []) { - if (is_array($tables)) { - // 支持别名定义 - foreach ($tables as $table => $alias) { - $array[] = !is_numeric($table) ? - $this->parseKey($table) . ' ' . $this->parseKey($alias) : - $this->parseKey($alias); + $item = []; + foreach ((array) $tables as $key => $table) { + if (!is_numeric($key)) { + $key = $this->parseSqlTable($key); + $item[] = $this->parseKey($key) . ' ' . (isset($options['alias'][$table]) ? $this->parseKey($options['alias'][$table]) : $this->parseKey($table)); + } else { + $table = $this->parseSqlTable($table); + if (isset($options['alias'][$table])) { + $item[] = $this->parseKey($table) . ' ' . $this->parseKey($options['alias'][$table]); + } else { + $item[] = $this->parseKey($table); + } } - $tables = $array; - } elseif (is_string($tables)) { - $tables = $this->parseSqlTable($tables); - $tables = array_map([$this, 'parseKey'], explode(',', $tables)); } - return implode(',', $tables); + return implode(',', $item); } /** @@ -201,6 +238,14 @@ abstract class Builder protected function parseWhere($where, $options) { $whereStr = $this->buildWhere($where, $options); + if (!empty($options['soft_delete'])) { + // 附加软删除条件 + list($field, $condition) = $options['soft_delete']; + + $binds = $this->query->getFieldsBind($options['table']); + $whereStr = $whereStr ? '( ' . $whereStr . ' ) AND ' : ''; + $whereStr = $whereStr . $this->parseWhereItem($field, $condition, '', $options, $binds); + } return empty($whereStr) ? '' : ' WHERE ' . $whereStr; } @@ -222,15 +267,20 @@ abstract class Builder } $whereStr = ''; - $binds = $this->query->getFieldsBind($options); + $binds = $this->query->getFieldsBind($options['table']); foreach ($where as $key => $val) { $str = []; foreach ($val as $field => $value) { - if ($value instanceof \Closure) { + if ($value instanceof Expression) { + $str[] = ' ' . $key . ' ( ' . $value->getValue() . ' )'; + } elseif ($value instanceof \Closure) { // 使用闭包查询 $query = new Query($this->connection); call_user_func_array($value, [ & $query]); - $str[] = ' ' . $key . ' ( ' . $this->buildWhere($query->getOptions('where'), $options) . ' )'; + $whereClause = $this->buildWhere($query->getOptions('where'), $options); + if (!empty($whereClause)) { + $str[] = ' ' . $key . ' ( ' . $whereClause . ' )'; + } } elseif (strpos($field, '|')) { // 不同字段使用相同查询条件(OR) $array = explode('|', $field); @@ -256,6 +306,7 @@ abstract class Builder $whereStr .= empty($whereStr) ? substr(implode(' ', $str), strlen($key) + 1) : implode(' ', $str); } + return $whereStr; } @@ -263,11 +314,11 @@ abstract class Builder protected function parseWhereItem($field, $val, $rule = '', $options = [], $binds = [], $bindName = null) { // 字段分析 - $key = $field ? $this->parseKey($field) : ''; + $key = $field ? $this->parseKey($field, $options, true) : ''; // 查询规则和条件 if (!is_array($val)) { - $val = ['=', $val]; + $val = is_null($val) ? ['null', ''] : ['=', $val]; } list($exp, $value) = $val; @@ -296,12 +347,24 @@ abstract class Builder throw new Exception('where express error:' . $exp); } } - $bindName = $bindName ?: 'where_' . str_replace('.', '_', $field); + $bindName = $bindName ?: 'where_' . $rule . '_' . str_replace(['.', '-'], '_', $field); + if (preg_match('/\W/', $bindName)) { + // 处理带非单词字符的字段名 + $bindName = md5($bindName); + } + + if ($value instanceof Expression) { + + } elseif (is_object($value) && method_exists($value, '__toString')) { + // 对象数据写入 + $value = $value->__toString(); + } + $bindType = isset($binds[$field]) ? $binds[$field] : PDO::PARAM_STR; if (is_scalar($value) && array_key_exists($field, $binds) && !in_array($exp, ['EXP', 'NOT NULL', 'NULL', 'IN', 'NOT IN', 'BETWEEN', 'NOT BETWEEN']) && strpos($exp, 'TIME') === false) { if (strpos($value, ':') !== 0 || !$this->query->isBind(substr($value, 1))) { if ($this->query->isBind($bindName)) { - $bindName .= '_' . uniqid(); + $bindName .= '_' . str_replace('.', '_', uniqid('', true)); } $this->query->bind($bindName, $value, $bindType); $value = ':' . $bindName; @@ -309,12 +372,31 @@ abstract class Builder } $whereStr = ''; - if (in_array($exp, ['=', '<>', '>', '>=', '<', '<=', 'LIKE', 'NOT LIKE'])) { - // 比较运算 及 模糊匹配 - $whereStr .= $key . ' ' . $exp . ' ' . $this->parseValue($value, $field); + if (in_array($exp, ['=', '<>', '>', '>=', '<', '<='])) { + // 比较运算 + if ($value instanceof \Closure) { + $whereStr .= $key . ' ' . $exp . ' ' . $this->parseClosure($value); + } else { + $whereStr .= $key . ' ' . $exp . ' ' . $this->parseValue($value, $field); + } + } elseif ('LIKE' == $exp || 'NOT LIKE' == $exp) { + // 模糊匹配 + if (is_array($value)) { + foreach ($value as $item) { + $array[] = $key . ' ' . $exp . ' ' . $this->parseValue($item, $field); + } + $logic = isset($val[2]) ? $val[2] : 'AND'; + $whereStr .= '(' . implode($array, ' ' . strtoupper($logic) . ' ') . ')'; + } else { + $whereStr .= $key . ' ' . $exp . ' ' . $this->parseValue($value, $field); + } } elseif ('EXP' == $exp) { // 表达式查询 - $whereStr .= '( ' . $key . ' ' . $value . ' )'; + if ($value instanceof Expression) { + $whereStr .= '( ' . $key . ' ' . $value->getValue() . ' )'; + } else { + throw new Exception('where express error:' . $exp); + } } elseif (in_array($exp, ['NOT NULL', 'NULL'])) { // NULL 查询 $whereStr .= $key . ' IS ' . $exp; @@ -323,31 +405,45 @@ abstract class Builder if ($value instanceof \Closure) { $whereStr .= $key . ' ' . $exp . ' ' . $this->parseClosure($value); } else { - $value = is_array($value) ? $value : explode(',', $value); + $value = array_unique(is_array($value) ? $value : explode(',', $value)); if (array_key_exists($field, $binds)) { $bind = []; $array = []; - foreach ($value as $k => $v) { - $bind[$bindName . '_in_' . $k] = [$v, $bindType]; - $array[] = ':' . $bindName . '_in_' . $k; + $i = 0; + foreach ($value as $v) { + $i++; + if ($this->query->isBind($bindName . '_in_' . $i)) { + $bindKey = $bindName . '_in_' . uniqid() . '_' . $i; + } else { + $bindKey = $bindName . '_in_' . $i; + } + $bind[$bindKey] = [$v, $bindType]; + $array[] = ':' . $bindKey; } $this->query->bind($bind); $zone = implode(',', $array); } else { $zone = implode(',', $this->parseValue($value, $field)); } - $whereStr .= $key . ' ' . $exp . ' (' . $zone . ')'; + $whereStr .= $key . ' ' . $exp . ' (' . (empty($zone) ? "''" : $zone) . ')'; } } elseif (in_array($exp, ['NOT BETWEEN', 'BETWEEN'])) { // BETWEEN 查询 $data = is_array($value) ? $value : explode(',', $value); if (array_key_exists($field, $binds)) { + if ($this->query->isBind($bindName . '_between_1')) { + $bindKey1 = $bindName . '_between_1' . uniqid(); + $bindKey2 = $bindName . '_between_2' . uniqid(); + } else { + $bindKey1 = $bindName . '_between_1'; + $bindKey2 = $bindName . '_between_2'; + } $bind = [ - $bindName . '_between_1' => [$data[0], $bindType], - $bindName . '_between_2' => [$data[1], $bindType], + $bindKey1 => [$data[0], $bindType], + $bindKey2 => [$data[1], $bindType], ]; $this->query->bind($bind); - $between = ':' . $bindName . '_between_1' . ' AND :' . $bindName . '_between_2'; + $between = ':' . $bindKey1 . ' AND :' . $bindKey2; } else { $between = $this->parseValue($data[0], $field) . ' AND ' . $this->parseValue($data[1], $field); } @@ -392,12 +488,23 @@ abstract class Builder protected function parseDateTime($value, $key, $options = [], $bindName = null, $bindType = null) { // 获取时间字段类型 - $type = $this->query->getFieldsType($options); + if (strpos($key, '.')) { + list($table, $key) = explode('.', $key); + if (isset($options['alias']) && $pos = array_search($table, $options['alias'])) { + $table = $pos; + } + } else { + $table = $options['table']; + } + $type = $this->query->getTableInfo($table, 'type'); if (isset($type[$key])) { $info = $type[$key]; } if (isset($info)) { - $value = strtotime($value) ?: $value; + if (is_string($value)) { + $value = strtotime($value) ?: $value; + } + if (preg_match('/(datetime|timestamp)/is', $info)) { // 日期及时间戳类型 $value = date('Y-m-d H:i:s', $value); @@ -407,6 +514,11 @@ abstract class Builder } } $bindName = $bindName ?: $key; + + if ($this->query->isBind($bindName)) { + $bindName .= '_' . str_replace('.', '_', uniqid('', true)); + } + $this->query->bind($bindName, $value, $bindType); return ':' . $bindName; } @@ -414,7 +526,7 @@ abstract class Builder /** * limit分析 * @access protected - * @param mixed $lmit + * @param mixed $limit * @return string */ protected function parseLimit($limit) @@ -425,14 +537,31 @@ abstract class Builder /** * join分析 * @access protected - * @param mixed $join + * @param array $join + * @param array $options 查询条件 * @return string */ - protected function parseJoin($join) + protected function parseJoin($join, $options = []) { $joinStr = ''; if (!empty($join)) { - $joinStr = ' ' . implode(' ', $join) . ' '; + foreach ($join as $item) { + list($table, $type, $on) = $item; + $condition = []; + foreach ((array) $on as $val) { + if ($val instanceof Expression) { + $condition[] = $val->getValue(); + } elseif (strpos($val, '=')) { + list($val1, $val2) = explode('=', $val, 2); + $condition[] = $this->parseKey($val1, $options) . '=' . $this->parseKey($val2, $options); + } else { + $condition[] = $val; + } + } + + $table = $this->parseTable($table, $options); + $joinStr .= ' ' . $type . ' JOIN ' . $table . ' ON ' . implode(' AND ', $condition); + } } return $joinStr; } @@ -441,26 +570,34 @@ abstract class Builder * order分析 * @access protected * @param mixed $order + * @param array $options 查询条件 * @return string */ - protected function parseOrder($order) + protected function parseOrder($order, $options = []) { - if (is_array($order)) { - $array = []; - foreach ($order as $key => $val) { + if (empty($order)) { + return ''; + } + + $array = []; + foreach ($order as $key => $val) { + if ($val instanceof Expression) { + $array[] = $val->getValue(); + } elseif ('[rand]' == $val) { + $array[] = $this->parseRand(); + } else { if (is_numeric($key)) { - if (false === strpos($val, '(')) { - $array[] = $this->parseKey($val); - } elseif ('[rand]' == $val) { - $array[] = $this->parseRand(); - } + list($key, $sort) = explode(' ', strpos($val, ' ') ? $val : $val . ' '); } else { - $sort = in_array(strtolower(trim($val)), ['asc', 'desc']) ? ' ' . $val : ''; - $array[] = $this->parseKey($key) . ' ' . $sort; + $sort = $val; } + $sort = strtoupper($sort); + $sort = in_array($sort, ['ASC', 'DESC'], true) ? ' ' . $sort : ''; + $array[] = $this->parseKey($key, $options, true) . $sort; } - $order = implode(',', $array); } + $order = implode(',', $array); + return !empty($order) ? ' ORDER BY ' . $order : ''; } @@ -472,7 +609,7 @@ abstract class Builder */ protected function parseGroup($group) { - return !empty($group) ? ' GROUP BY ' . $group : ''; + return !empty($group) ? ' GROUP BY ' . $this->parseKey($group) : ''; } /** @@ -494,6 +631,9 @@ abstract class Builder */ protected function parseComment($comment) { + if (false !== strpos($comment, '*/')) { + $comment = strstr($comment, '*/', true); + } return !empty($comment) ? ' /* ' . $comment . ' */' : ''; } @@ -523,12 +663,12 @@ abstract class Builder unset($union['type']); foreach ($union as $u) { if ($u instanceof \Closure) { - $sql[] = $type . ' ' . $this->parseClosure($u, false); + $sql[] = $type . ' ' . $this->parseClosure($u); } elseif (is_string($u)) { - $sql[] = $type . ' ' . $this->parseSqlTable($u); + $sql[] = $type . ' ( ' . $this->parseSqlTable($u) . ' )'; } } - return implode(' ', $sql); + return ' ' . implode(' ', $sql); } /** @@ -543,22 +683,22 @@ abstract class Builder return ''; } - if (is_array($index)) { - $index = join(",", $index); - } - - return sprintf(" FORCE INDEX ( %s ) ", $index); + return sprintf(" FORCE INDEX ( %s ) ", is_array($index) ? implode(',', $index) : $index); } /** * 设置锁机制 * @access protected - * @param bool $locl + * @param bool|string $lock * @return string */ protected function parseLock($lock = false) { - return $lock ? ' FOR UPDATE ' : ''; + if (is_bool($lock)) { + return $lock ? ' FOR UPDATE ' : ''; + } elseif (is_string($lock)) { + return ' ' . trim($lock) . ' '; + } } /** @@ -572,14 +712,14 @@ abstract class Builder $sql = str_replace( ['%TABLE%', '%DISTINCT%', '%FIELD%', '%JOIN%', '%WHERE%', '%GROUP%', '%HAVING%', '%ORDER%', '%LIMIT%', '%UNION%', '%LOCK%', '%COMMENT%', '%FORCE%'], [ - $this->parseTable($options['table']), + $this->parseTable($options['table'], $options), $this->parseDistinct($options['distinct']), - $this->parseField($options['field']), - $this->parseJoin($options['join']), + $this->parseField($options['field'], $options), + $this->parseJoin($options['join'], $options), $this->parseWhere($options['where'], $options), $this->parseGroup($options['group']), $this->parseHaving($options['having']), - $this->parseOrder($options['order']), + $this->parseOrder($options['order'], $options), $this->parseLimit($options['limit']), $this->parseUnion($options['union']), $this->parseLock($options['lock']), @@ -611,7 +751,7 @@ abstract class Builder ['%INSERT%', '%TABLE%', '%FIELD%', '%DATA%', '%COMMENT%'], [ $replace ? 'REPLACE' : 'INSERT', - $this->parseTable($options['table']), + $this->parseTable($options['table'], $options), implode(' , ', $fields), implode(' , ', $values), $this->parseComment($options['comment']), @@ -625,26 +765,33 @@ abstract class Builder * @access public * @param array $dataSet 数据集 * @param array $options 表达式 + * @param bool $replace 是否replace * @return string + * @throws Exception */ - public function insertAll($dataSet, $options) + public function insertAll($dataSet, $options = [], $replace = false) { // 获取合法的字段 if ('*' == $options['field']) { - $fields = array_keys($this->query->getFieldsType($options)); + $fields = array_keys($this->query->getFieldsType($options['table'])); } else { $fields = $options['field']; } - foreach ($dataSet as &$data) { + 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 . ']'); } unset($data[$key]); + } elseif (is_null($val)) { + $data[$key] = 'NULL'; } elseif (is_scalar($val)) { $data[$key] = $this->parseValue($val, $key); + } elseif (is_object($val) && method_exists($val, '__toString')) { + // 对象数据写入 + $data[$key] = $val->__toString(); } else { // 过滤掉非标量数据 unset($data[$key]); @@ -652,22 +799,29 @@ abstract class Builder } $value = array_values($data); $values[] = 'SELECT ' . implode(',', $value); + + if (!isset($insertFields)) { + $insertFields = array_keys($data); + } + } + + foreach ($insertFields as $field) { + $fields[] = $this->parseKey($field, $options, true); } - $fields = array_map([$this, 'parseKey'], array_keys(reset($dataSet))); - $sql = str_replace( - ['%TABLE%', '%FIELD%', '%DATA%', '%COMMENT%'], + + return str_replace( + ['%INSERT%', '%TABLE%', '%FIELD%', '%DATA%', '%COMMENT%'], [ - $this->parseTable($options['table']), - implode(' , ', $fields), + $replace ? 'REPLACE' : 'INSERT', + $this->parseTable($options['table'], $options), + implode(' , ', $insertFields), implode(' UNION ALL ', $values), $this->parseComment($options['comment']), ], $this->insertAllSql); - - return $sql; } /** - * 生成slectinsert SQL + * 生成select insert SQL * @access public * @param array $fields 数据 * @param string $table 数据表 @@ -681,20 +835,20 @@ abstract class Builder } $fields = array_map([$this, 'parseKey'], $fields); - $sql = 'INSERT INTO ' . $this->parseTable($table) . ' (' . implode(',', $fields) . ') ' . $this->select($options); + $sql = 'INSERT INTO ' . $this->parseTable($table, $options) . ' (' . implode(',', $fields) . ') ' . $this->select($options); return $sql; } /** * 生成update SQL * @access public - * @param array $fields 数据 + * @param array $data 数据 * @param array $options 表达式 * @return string */ public function update($data, $options) { - $table = $this->parseTable($options['table']); + $table = $this->parseTable($options['table'], $options); $data = $this->parseData($data, $options); if (empty($data)) { return ''; @@ -706,11 +860,11 @@ abstract class Builder $sql = str_replace( ['%TABLE%', '%SET%', '%JOIN%', '%WHERE%', '%ORDER%', '%LIMIT%', '%LOCK%', '%COMMENT%'], [ - $this->parseTable($options['table']), + $this->parseTable($options['table'], $options), implode(',', $set), - $this->parseJoin($options['join']), + $this->parseJoin($options['join'], $options), $this->parseWhere($options['where'], $options), - $this->parseOrder($options['order']), + $this->parseOrder($options['order'], $options), $this->parseLimit($options['limit']), $this->parseLock($options['lock']), $this->parseComment($options['comment']), @@ -730,11 +884,11 @@ abstract class Builder $sql = str_replace( ['%TABLE%', '%USING%', '%JOIN%', '%WHERE%', '%ORDER%', '%LIMIT%', '%LOCK%', '%COMMENT%'], [ - $this->parseTable($options['table']), - !empty($options['using']) ? ' USING ' . $this->parseTable($options['using']) . ' ' : '', - $this->parseJoin($options['join']), + $this->parseTable($options['table'], $options), + !empty($options['using']) ? ' USING ' . $this->parseTable($options['using'], $options) . ' ' : '', + $this->parseJoin($options['join'], $options), $this->parseWhere($options['where'], $options), - $this->parseOrder($options['order']), + $this->parseOrder($options['order'], $options), $this->parseLimit($options['limit']), $this->parseLock($options['lock']), $this->parseComment($options['comment']), diff --git a/library/think/db/Connection.php b/library/think/db/Connection.php index f405f9b9f812ca5829a70a17deb51b31ec043dcf..578cc8f9840d0a522c15833ffd7594a861b134a4 100644 --- a/library/think/db/Connection.php +++ b/library/think/db/Connection.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2016 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- @@ -13,10 +13,8 @@ namespace think\db; use PDO; use PDOStatement; -use think\Collection; use think\Db; use think\db\exception\BindParamException; -use think\db\Query; use think\Debug; use think\Exception; use think\exception\PDOException; @@ -52,60 +50,66 @@ abstract class Connection protected $linkRead; protected $linkWrite; - // 查询结果类型 - protected $resultSetType = 'array'; // 查询结果类型 protected $fetchType = PDO::FETCH_ASSOC; // 字段属性大小写 protected $attrCase = PDO::CASE_LOWER; // 监听回调 protected static $event = []; - // 查询对象 - protected $query = []; + // 使用Builder类 + protected $builder; // 数据库连接参数配置 protected $config = [ // 数据库类型 - 'type' => '', + 'type' => '', // 服务器地址 - 'hostname' => '', + 'hostname' => '', // 数据库名 - 'database' => '', + 'database' => '', // 用户名 - 'username' => '', + 'username' => '', // 密码 - 'password' => '', + 'password' => '', // 端口 - 'hostport' => '', + 'hostport' => '', // 连接dsn - 'dsn' => '', + 'dsn' => '', // 数据库连接参数 - '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' => '', + // 模型写入后自动读取主服务器 + 'read_master' => false, // 是否严格检查字段是否存在 - 'fields_strict' => true, + 'fields_strict' => true, + // 数据返回类型 + 'result_type' => PDO::FETCH_ASSOC, // 数据集返回类型 - 'resultset_type' => 'array', + 'resultset_type' => 'array', // 自动写入时间戳字段 - 'auto_timestamp' => false, + 'auto_timestamp' => false, + // 时间字段取出后的默认时间格式 + 'datetime_format' => 'Y-m-d H:i:s', // 是否需要进行SQL性能分析 - 'sql_explain' => false, + 'sql_explain' => false, // Builder类 - 'builder' => '', + 'builder' => '', // Query类 - 'query' => '\\think\\db\\Query', + 'query' => '\\think\\db\\Query', + // 是否需要断线重连 + 'break_reconnect' => false, ]; // PDO连接参数 @@ -117,8 +121,11 @@ abstract class Connection PDO::ATTR_EMULATE_PREPARES => false, ]; + // 绑定参数 + protected $bind = []; + /** - * 架构函数 读取数据库配置信息 + * 构造函数 读取数据库配置信息 * @access public * @param array $config 数据库配置数组 */ @@ -130,19 +137,28 @@ abstract class Connection } /** - * 创建指定模型的查询对象 - * @access public - * @param string $model 模型类名称 - * @param string $queryClass 查询对象类名 + * 获取新的查询对象 + * @access protected * @return Query */ - public function model($model, $queryClass = '') + protected function getQuery() { - if (!isset($this->query[$model])) { - $class = $queryClass ?: $this->config['query']; - $this->query[$model] = new $class($this, $model); + $class = $this->config['query']; + return new $class($this); + } + + /** + * 获取当前连接器类对应的Builder类 + * @access public + * @return string + */ + public function getBuilder() + { + if (!empty($this->builder)) { + return $this->builder; + } else { + return $this->getConfig('builder') ?: '\\think\\db\\builder\\' . ucfirst($this->getConfig('type')); } - return $this->query[$model]; } /** @@ -154,11 +170,7 @@ abstract class Connection */ public function __call($method, $args) { - if (!isset($this->query['database'])) { - $class = $this->config['query']; - $this->query['database'] = new $class($this); - } - return call_user_func_array([$this->query['database'], $method], $args); + return call_user_func_array([$this->getQuery(), $method], $args); } /** @@ -268,9 +280,10 @@ abstract class Connection } // 记录当前字段属性大小写设置 $this->attrCase = $params[PDO::ATTR_CASE]; - // 记录数据集返回类型 - if (isset($config['resultset_type'])) { - $this->resultSetType = $config['resultset_type']; + + // 数据返回类型 + if (isset($config['result_type'])) { + $this->fetchType = $config['result_type']; } try { if (empty($config['dsn'])) { @@ -324,85 +337,130 @@ abstract class Connection * @access public * @param string $sql sql指令 * @param array $bind 参数绑定 - * @param boolean $master 是否在主服务器读操作 - * @param bool|string $class 指定返回的数据集对象 + * @param bool $master 是否在主服务器读操作 + * @param bool $pdo 是否返回PDO对象 * @return mixed - * @throws BindParamException * @throws PDOException + * @throws \Exception */ - public function query($sql, $bind = [], $master = false, $class = false) + public function query($sql, $bind = [], $master = false, $pdo = false) { $this->initConnect($master); if (!$this->linkID) { return false; } - // 根据参数绑定组装最终的SQL语句 - $this->queryStr = $this->getRealSql($sql, $bind); - //释放前次的查询结果 - if (!empty($this->PDOStatement)) { - $this->free(); + // 记录SQL语句 + $this->queryStr = $sql; + if ($bind) { + $this->bind = $bind; } Db::$queryTimes++; try { // 调试开始 $this->debug(true); + // 预处理 $this->PDOStatement = $this->linkID->prepare($sql); + + // 是否为存储过程调用 + $procedure = in_array(strtolower(substr(trim($sql), 0, 4)), ['call', 'exec']); // 参数绑定 - $this->bindValue($bind); + if ($procedure) { + $this->bindParam($bind); + } else { + $this->bindValue($bind); + } // 执行查询 - $result = $this->PDOStatement->execute(); + $this->PDOStatement->execute(); // 调试结束 - $this->debug(false); - $procedure = in_array(strtolower(substr(trim($sql), 0, 4)), ['call', 'exec']); - return $this->getResult($class, $procedure); + $this->debug(false, '', $master); + // 返回结果集 + return $this->getResult($pdo, $procedure); } catch (\PDOException $e) { - throw new PDOException($e, $this->config, $this->queryStr); + if ($this->isBreak($e)) { + return $this->close()->query($sql, $bind, $master, $pdo); + } + throw new PDOException($e, $this->config, $this->getLastsql()); + } catch (\Throwable $e) { + if ($this->isBreak($e)) { + return $this->close()->query($sql, $bind, $master, $pdo); + } + throw $e; + } catch (\Exception $e) { + if ($this->isBreak($e)) { + return $this->close()->query($sql, $bind, $master, $pdo); + } + throw $e; } } /** * 执行语句 * @access public - * @param string $sql sql指令 - * @param array $bind 参数绑定 + * @param string $sql sql指令 + * @param array $bind 参数绑定 + * @param Query $query 查询对象 * @return int - * @throws BindParamException * @throws PDOException + * @throws \Exception */ - public function execute($sql, $bind = []) + public function execute($sql, $bind = [], Query $query = null) { $this->initConnect(true); if (!$this->linkID) { return false; } - // 根据参数绑定组装最终的SQL语句 - $this->queryStr = $this->getRealSql($sql, $bind); - //释放前次的查询结果 - if (!empty($this->PDOStatement)) { - $this->free(); + // 记录SQL语句 + $this->queryStr = $sql; + if ($bind) { + $this->bind = $bind; } Db::$executeTimes++; try { // 调试开始 $this->debug(true); + // 预处理 $this->PDOStatement = $this->linkID->prepare($sql); - // 参数绑定操作 - $this->bindValue($bind); + + // 是否为存储过程调用 + $procedure = in_array(strtolower(substr(trim($sql), 0, 4)), ['call', 'exec']); + // 参数绑定 + if ($procedure) { + $this->bindParam($bind); + } else { + $this->bindValue($bind); + } // 执行语句 - $result = $this->PDOStatement->execute(); + $this->PDOStatement->execute(); // 调试结束 - $this->debug(false); + $this->debug(false, '', true); + + if ($query && !empty($this->config['deploy']) && !empty($this->config['read_master'])) { + $query->readMaster(); + } $this->numRows = $this->PDOStatement->rowCount(); return $this->numRows; } catch (\PDOException $e) { - throw new PDOException($e, $this->config, $this->queryStr); + if ($this->isBreak($e)) { + return $this->close()->execute($sql, $bind, $query); + } + throw new PDOException($e, $this->config, $this->getLastsql()); + } catch (\Throwable $e) { + if ($this->isBreak($e)) { + return $this->close()->execute($sql, $bind, $query); + } + throw $e; + } catch (\Exception $e) { + if ($this->isBreak($e)) { + return $this->close()->execute($sql, $bind, $query); + } + throw $e; } } @@ -415,23 +473,27 @@ abstract class Connection */ public function getRealSql($sql, array $bind = []) { - if ($bind) { - foreach ($bind as $key => $val) { - $value = is_array($val) ? $val[0] : $val; - $type = is_array($val) ? $val[1] : PDO::PARAM_STR; - if (PDO::PARAM_STR == $type) { - $value = $this->quote($value); - } - // 判断占位符 - $sql = is_numeric($key) ? - substr_replace($sql, $value, strpos($sql, '?'), 1) : - str_replace( - [':' . $key . ')', ':' . $key . ',', ':' . $key . ' '], - [$value . ')', $value . ',', $value . ' '], - $sql . ' '); + if (is_array($sql)) { + $sql = implode(';', $sql); + } + + foreach ($bind as $key => $val) { + $value = is_array($val) ? $val[0] : $val; + $type = is_array($val) ? $val[1] : PDO::PARAM_STR; + if (PDO::PARAM_STR == $type) { + $value = $this->quote($value); + } elseif (PDO::PARAM_INT == $type) { + $value = (float) $value; } + // 判断占位符 + $sql = is_numeric($key) ? + substr_replace($sql, $value, strpos($sql, '?'), 1) : + str_replace( + [':' . $key . ')', ':' . $key . ',', ':' . $key . ' ', ':' . $key . PHP_EOL], + [$value . ')', $value . ',', $value . ' ', $value . PHP_EOL], + $sql . ' '); } - return $sql; + return rtrim($sql); } /** @@ -441,7 +503,7 @@ abstract class Connection * @access public * @param array $bind 要绑定的参数列表 * @return void - * @throws \think\Exception + * @throws BindParamException */ protected function bindValue(array $bind = []) { @@ -449,6 +511,9 @@ abstract class Connection // 占位符 $param = is_numeric($key) ? $key + 1 : ':' . $key; if (is_array($val)) { + if (PDO::PARAM_INT == $val[1] && '' === $val[0]) { + $val[0] = 0; + } $result = $this->PDOStatement->bindValue($param, $val[0], $val[1]); } else { $result = $this->PDOStatement->bindValue($param, $val); @@ -457,7 +522,7 @@ abstract class Connection throw new BindParamException( "Error occurred when binding parameters '{$param}'", $this->config, - $this->queryStr, + $this->getLastsql(), $bind ); } @@ -465,46 +530,66 @@ abstract class Connection } /** - * 获得数据集 + * 存储过程的输入输出参数绑定 + * @access public + * @param array $bind 要绑定的参数列表 + * @return void + * @throws BindParamException + */ + protected function bindParam($bind) + { + foreach ($bind as $key => $val) { + $param = is_numeric($key) ? $key + 1 : ':' . $key; + if (is_array($val)) { + array_unshift($val, $param); + $result = call_user_func_array([$this->PDOStatement, 'bindParam'], $val); + } else { + $result = $this->PDOStatement->bindValue($param, $val); + } + if (!$result) { + $param = array_shift($val); + throw new BindParamException( + "Error occurred when binding parameters '{$param}'", + $this->config, + $this->getLastsql(), + $bind + ); + } + } + } + + /** + * 获得数据集数组 * @access protected - * @param bool|string $class true 返回PDOStatement 字符串用于指定返回的类名 - * @param bool $procedure 是否存储过程 - * @return mixed + * @param bool $pdo 是否返回PDOStatement + * @param bool $procedure 是否存储过程 + * @return PDOStatement|array */ - protected function getResult($class = '', $procedure = false) + protected function getResult($pdo = false, $procedure = false) { - if (true === $class) { + if ($pdo) { // 返回PDOStatement对象处理 return $this->PDOStatement; } if ($procedure) { // 存储过程返回结果 - return $this->procedure($class); + return $this->procedure(); } $result = $this->PDOStatement->fetchAll($this->fetchType); $this->numRows = count($result); - - if (!empty($class)) { - // 返回指定数据集对象类 - $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) + protected function procedure() { $item = []; do { - $result = $this->getResult($class); + $result = $this->getResult(); if ($result) { $item[] = $result; } @@ -544,7 +629,8 @@ abstract class Connection /** * 启动事务 * @access public - * @return void + * @return bool|mixed + * @throws \Exception */ public function startTrans() { @@ -554,13 +640,27 @@ abstract class Connection } ++$this->transTimes; + try { + if (1 == $this->transTimes) { + $this->linkID->beginTransaction(); + } elseif ($this->transTimes > 1 && $this->supportSavepoint()) { + $this->linkID->exec( + $this->parseSavepoint('trans' . $this->transTimes) + ); + } - if (1 == $this->transTimes) { - $this->linkID->beginTransaction(); - } elseif ($this->transTimes > 1 && $this->supportSavepoint()) { - $this->linkID->exec( - $this->parseSavepoint('trans' . $this->transTimes) - ); + } catch (\Exception $e) { + if ($this->isBreak($e)) { + --$this->transTimes; + return $this->close()->startTrans(); + } + throw $e; + } catch (\Error $e) { + if ($this->isBreak($e)) { + --$this->transTimes; + return $this->close()->startTrans(); + } + throw $e; } } @@ -638,7 +738,7 @@ abstract class Connection * @param array $sqlArray SQL批处理指令 * @return boolean */ - public function batchQuery($sqlArray = []) + public function batchQuery($sqlArray = [], $bind = [], Query $query = null) { if (!is_array($sqlArray)) { return false; @@ -647,7 +747,7 @@ abstract class Connection $this->startTrans(); try { foreach ($sqlArray as $sql) { - $this->execute($sql); + $this->execute($sql, $bind, $query); } // 提交事务 $this->commit(); @@ -655,6 +755,7 @@ abstract class Connection $this->rollback(); throw $e; } + return true; } @@ -680,12 +781,55 @@ abstract class Connection } /** - * 关闭数据库 + * 关闭数据库(或者重新连接) * @access public + * @return $this */ public function close() { - $this->linkID = null; + $this->linkID = null; + $this->linkWrite = null; + $this->linkRead = null; + $this->links = []; + // 释放查询 + $this->free(); + return $this; + } + + /** + * 是否断线 + * @access protected + * @param \PDOException|\Exception $e 异常对象 + * @return bool + */ + protected function isBreak($e) + { + if (!$this->config['break_reconnect']) { + return false; + } + + $info = [ + 'server has gone away', + 'no connection to the server', + 'Lost connection', + 'is dead or not enabled', + 'Error while sending', + 'decryption failed or bad record mac', + 'server closed the connection unexpectedly', + 'SSL connection has been closed unexpectedly', + 'Error writing data to the connection', + 'Resource deadlock avoided', + 'failed with errno', + ]; + + $error = $e->getMessage(); + + foreach ($info as $msg) { + if (false !== stripos($error, $msg)) { + return true; + } + } + return false; } /** @@ -695,7 +839,7 @@ abstract class Connection */ public function getLastSql() { - return $this->queryStr; + return $this->getRealSql($this->queryStr, $this->bind); } /** @@ -733,7 +877,7 @@ abstract class Connection $error = ''; } if ('' != $this->queryStr) { - $error .= "\n [ SQL语句 ] : " . $this->queryStr; + $error .= "\n [ SQL语句 ] : " . $this->getLastsql(); } return $error; } @@ -756,9 +900,10 @@ abstract class Connection * @access protected * @param boolean $start 调试开始标记 true 开始 false 结束 * @param string $sql 执行的SQL语句 留空自动获取 + * @param boolean $master 主从标记 * @return void */ - protected function debug($start, $sql = '') + protected function debug($start, $sql = '', $master = false) { if (!empty($this->config['debug'])) { // 开启数据库调试模式 @@ -768,15 +913,14 @@ abstract class Connection // 记录操作结束时间 Debug::remark('queryEndTime', 'time'); $runtime = Debug::getRangeTime('queryStartTime', 'queryEndTime'); - $sql = $sql ?: $this->queryStr; - $log = $sql . ' [ RunTime:' . $runtime . 's ]'; + $sql = $sql ?: $this->getLastsql(); $result = []; // SQL性能分析 if ($this->config['sql_explain'] && 0 === stripos(trim($sql), 'select')) { $result = $this->getExplain($sql); } // SQL监听 - $this->trigger($sql, $runtime, $result); + $this->trigger($sql, $runtime, $result, $master); } } } @@ -798,19 +942,27 @@ abstract class Connection * @param string $sql SQL语句 * @param float $runtime SQL运行时间 * @param mixed $explain SQL分析 - * @return bool + * @param bool $master 主从标记 + * @return void */ - protected function trigger($sql, $runtime, $explain = []) + protected function trigger($sql, $runtime, $explain = [], $master = false) { if (!empty(self::$event)) { foreach (self::$event as $callback) { if (is_callable($callback)) { - call_user_func_array($callback, [$sql, $runtime, $explain]); + call_user_func_array($callback, [$sql, $runtime, $explain, $master]); } } } else { // 未注册监听则记录到日志中 - Log::record('[ SQL ] ' . $sql . ' [ RunTime:' . $runtime . 's ]', 'sql'); + if ($this->config['deploy']) { + // 分布式记录当前操作的主从 + $master = $master ? 'master|' : 'slave|'; + } else { + $master = ''; + } + + Log::record('[ SQL ] ' . $sql . ' [ ' . $master . 'RunTime:' . $runtime . 's ]', 'sql'); if (!empty($explain)) { Log::record('[ EXPLAIN : ' . var_export($explain, true) . ' ]', 'sql'); } @@ -827,7 +979,7 @@ abstract class Connection { if (!empty($this->config['deploy'])) { // 采用分布式数据库 - if ($master) { + if ($master || $this->transTimes) { if (!$this->linkWrite) { $this->linkWrite = $this->multiConnect(true); } diff --git a/library/think/db/Expression.php b/library/think/db/Expression.php new file mode 100644 index 0000000000000000000000000000000000000000..f1b92abd7b93116c0c9b681f45c3d8e15c0b8596 --- /dev/null +++ b/library/think/db/Expression.php @@ -0,0 +1,48 @@ + +// +---------------------------------------------------------------------- + +namespace think\db; + +class Expression +{ + /** + * 查询表达式 + * + * @var string + */ + protected $value; + + /** + * 创建一个查询表达式 + * + * @param string $value + * @return void + */ + public function __construct($value) + { + $this->value = $value; + } + + /** + * 获取表达式 + * + * @return string + */ + public function getValue() + { + return $this->value; + } + + public function __toString() + { + return (string) $this->value; + } +} diff --git a/library/think/db/Query.php b/library/think/db/Query.php index 2e9308cc66838c81079de3621530f47aeb18cb2e..bb16a1f54a6aab06f9b4663d5b8d538e1e0a2c29 100644 --- a/library/think/db/Query.php +++ b/library/think/db/Query.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2016 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- @@ -12,12 +12,11 @@ namespace think\db; use PDO; +use think\App; 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; @@ -27,13 +26,14 @@ use think\exception\PDOException; use think\Loader; use think\Model; use think\model\Relation; +use think\model\relation\OneToOne; use think\Paginator; class Query { // 数据库Connection对象实例 protected $connection; - // 数据库驱动类型 + // 数据库Builder对象实例 protected $builder; // 当前模型类名称 protected $model; @@ -51,19 +51,24 @@ class Query protected $bind = []; // 数据表信息 protected static $info = []; + // 回调事件 + private static $event = []; + // 读取主库 + protected static $readMaster = []; /** - * 架构函数 + * 构造函数 * @access public * @param Connection $connection 数据库对象实例 - * @param string $model 模型名 + * @param Model $model 模型对象 */ - public function __construct(Connection $connection = null, $model = '') + public function __construct(Connection $connection = null, $model = null) { $this->connection = $connection ?: Db::connect([], true); - $this->builder = $this->connection->getConfig('builder') ?: $this->connection->getConfig('type'); $this->prefix = $this->connection->getConfig('prefix'); $this->model = $model; + // 设置当前连接的Builder对象 + $this->setBuilder(); } /** @@ -87,6 +92,13 @@ class Query $name = Loader::parseName(substr($method, 10)); $where[$name] = $args[0]; return $this->where($where)->value($args[1]); + } elseif ($this->model && method_exists($this->model, 'scope' . $method)) { + // 动态调用命名范围 + $method = 'scope' . $method; + array_unshift($args, $this); + + call_user_func_array([$this->model, $method], $args); + return $this; } else { throw new Exception('method not exist:' . __CLASS__ . '->' . $method); } @@ -111,9 +123,61 @@ class Query public function connect($config) { $this->connection = Db::connect($config); + $this->setBuilder(); + $this->prefix = $this->connection->getConfig('prefix'); + return $this; + } + + /** + * 设置当前的数据库Builder对象 + * @access protected + * @return void + */ + protected function setBuilder() + { + $class = $this->connection->getBuilder(); + $this->builder = new $class($this->connection, $this); + } + + /** + * 获取当前的模型对象实例 + * @access public + * @return Model|null + */ + public function getModel() + { + return $this->model; + } + + /** + * 设置后续从主库读取数据 + * @access public + * @param bool $allTable + * @return void + */ + public function readMaster($allTable = false) + { + if ($allTable) { + $table = '*'; + } else { + $table = isset($this->options['table']) ? $this->options['table'] : $this->getTable(); + } + + static::$readMaster[$table] = true; + return $this; } + /** + * 获取当前的builder实例对象 + * @access public + * @return Builder + */ + public function getBuilder() + { + return $this->builder; + } + /** * 指定默认的数据表名(不含前缀) * @access public @@ -194,21 +258,21 @@ class Query /** * 执行语句 * @access public - * @param string $sql sql指令 - * @param array $bind 参数绑定 + * @param string $sql sql指令 + * @param array $bind 参数绑定 * @return int * @throws BindParamException * @throws PDOException */ public function execute($sql, $bind = []) { - return $this->connection->execute($sql, $bind); + return $this->connection->execute($sql, $bind, $this); } /** * 获取最近插入的ID * @access public - * @param string $sequence 自增序列名 + * @param string $sequence 自增序列名 * @return string */ public function getLastInsID($sequence = null) @@ -276,9 +340,9 @@ class Query * @param array $sql SQL批处理指令 * @return boolean */ - public function batchQuery($sql = []) + public function batchQuery($sql = [], $bind = []) { - return $this->connection->batchQuery($sql); + return $this->connection->batchQuery($sql, $bind); } /** @@ -350,66 +414,50 @@ class Query } } - /** - * 获取当前的builder实例对象 - * @access protected - * @return Builder - */ - protected function builder() - { - static $builder = []; - $driver = $this->builder; - if (!isset($builder[$driver])) { - $class = false !== strpos($driver, '\\') ? $driver : '\\think\\db\\builder\\' . ucfirst($driver); - $builder[$driver] = new $class($this->connection); - } - // 设置当前查询对象 - $builder[$driver]->setQuery($this); - return $builder[$driver]; - } - /** * 得到某个字段的值 * @access public * @param string $field 字段名 * @param mixed $default 默认值 + * @param bool $force 强制转为数字类型 * @return mixed */ - public function value($field, $default = null) + public function value($field, $default = null, $force = false) { - $result = null; - if (!empty($this->options['cache'])) { + $result = false; + if (empty($this->options['fetch_sql']) && !empty($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)); + $key = is_string($cache['key']) ? $cache['key'] : md5($this->connection->getConfig('database') . '.' . $field . serialize($this->options) . serialize($this->bind)); $result = Cache::get($key); } - if (!$result) { + if (false === $result) { if (isset($this->options['field'])) { unset($this->options['field']); } - $pdo = $this->field($field)->fetchPdo(true)->find(); + $pdo = $this->field($field)->limit(1)->getPdo(); if (is_string($pdo)) { // 返回SQL语句 return $pdo; } + $result = $pdo->fetchColumn(); - if (isset($cache)) { + if ($force) { + $result = (float) $result; + } + + if (isset($cache) && false !== $result) { // 缓存数据 - if (isset($cache['tag'])) { - Cache::tag($cache['tag'])->set($key, $result, $cache['expire']); - } else { - Cache::set($key, $result, $cache['expire']); - } + $this->cacheData($key, $result, $cache); } } else { // 清空查询条件 $this->options = []; } - return !is_null($result) ? $result : $default; + return false !== $result ? $result : $default; } /** @@ -422,23 +470,25 @@ class Query public function column($field, $key = '') { $result = false; - if (!empty($this->options['cache'])) { + if (empty($this->options['fetch_sql']) && !empty($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)); + $guid = is_string($cache['key']) ? $cache['key'] : md5($this->connection->getConfig('database') . '.' . $field . serialize($this->options) . serialize($this->bind)); $result = Cache::get($guid); } - if (!$result) { + if (false === $result) { if (isset($this->options['field'])) { unset($this->options['field']); } - if ($key && '*' != $field) { + if (is_null($field)) { + $field = '*'; + } elseif ($key && '*' != $field) { $field = $key . ',' . $field; } - $pdo = $this->field($field)->fetchPdo(true)->select(); + $pdo = $this->field($field)->getPdo(); if (is_string($pdo)) { // 返回SQL语句 return $pdo; @@ -453,6 +503,9 @@ class Query $key1 = array_shift($fields); $key2 = $fields ? array_shift($fields) : ''; $key = $key ?: $key1; + if (strpos($key, '.')) { + list($alias, $key) = explode('.', $key); + } foreach ($resultSet as $val) { if ($count > 2) { $result[$val[$key]] = $val; @@ -468,11 +521,7 @@ class Query } if (isset($cache) && isset($guid)) { // 缓存数据 - if (isset($cache['tag'])) { - Cache::tag($cache['tag'])->set($guid, $result, $cache['expire']); - } else { - Cache::set($guid, $result, $cache['expire']); - } + $this->cacheData($guid, $result, $cache); } } else { // 清空查询条件 @@ -485,11 +534,48 @@ class Query * COUNT查询 * @access public * @param string $field 字段名 - * @return integer + * @return integer|string */ public function count($field = '*') { - return (int) $this->value('COUNT(' . $field . ') AS tp_count', 0); + if (isset($this->options['group'])) { + if (!preg_match('/^[\w\.\*]+$/', $field)) { + throw new Exception('not support data:' . $field); + } + // 支持GROUP + $options = $this->getOptions(); + $subSql = $this->options($options)->field('count(' . $field . ')')->bind($this->bind)->buildSql(); + + $count = $this->table([$subSql => '_group_count_'])->value('COUNT(*) AS tp_count', 0, true); + } else { + $count = $this->aggregate('COUNT', $field, true); + } + + return is_string($count) ? $count : (int) $count; + + } + + /** + * 聚合查询 + * @access public + * @param string $aggregate 聚合方法 + * @param string $field 字段名 + * @param bool $force 强制转为数字类型 + * @return mixed + */ + public function aggregate($aggregate, $field, $force = false) + { + if (0 === stripos($field, 'DISTINCT ')) { + list($distinct, $field) = explode(' ', $field); + } + + if (!preg_match('/^[\w\.\+\-\*]+$/', $field)) { + throw new Exception('not support data:' . $field); + } + + $result = $this->value($aggregate . '(' . (!empty($distinct) ? 'DISTINCT ' : '') . $field . ') AS tp_' . strtolower($aggregate), 0, $force); + + return $result; } /** @@ -498,31 +584,33 @@ class Query * @param string $field 字段名 * @return float|int */ - public function sum($field = '*') + public function sum($field) { - return $this->value('SUM(' . $field . ') AS tp_sum', 0) + 0; + return $this->aggregate('SUM', $field, true); } /** * MIN查询 * @access public * @param string $field 字段名 - * @return float|int + * @param bool $force 强制转为数字类型 + * @return mixed */ - public function min($field = '*') + public function min($field, $force = true) { - return $this->value('MIN(' . $field . ') AS tp_min', 0) + 0; + return $this->aggregate('MIN', $field, $force); } /** * MAX查询 * @access public * @param string $field 字段名 - * @return float|int + * @param bool $force 强制转为数字类型 + * @return mixed */ - public function max($field = '*') + public function max($field, $force = true) { - return $this->value('MAX(' . $field . ') AS tp_max', 0) + 0; + return $this->aggregate('MAX', $field, $force); } /** @@ -531,9 +619,9 @@ class Query * @param string $field 字段名 * @return float|int */ - public function avg($field = '*') + public function avg($field) { - return $this->value('AVG(' . $field . ') AS tp_avg', 0) + 0; + return $this->aggregate('AVG', $field, true); } /** @@ -572,17 +660,15 @@ class Query } if ($lazyTime > 0) { // 延迟写入 - $guid = md5($this->getTable() . '_' . $field . '_' . serialize($condition)); + $guid = md5($this->getTable() . '_' . $field . '_' . serialize($condition) . serialize($this->bind)); $step = $this->lazyWrite('inc', $guid, $step, $lazyTime); if (false === $step) { // 清空查询条件 $this->options = []; return true; - } else { - return $this->setField($field, $step); } } - return $this->setField($field, ['exp', $field . '+' . $step]); + return $this->setField($field, ['inc', $step]); } /** @@ -603,17 +689,16 @@ class Query } if ($lazyTime > 0) { // 延迟写入 - $guid = md5($this->getTable() . '_' . $field . '_' . serialize($condition)); + $guid = md5($this->getTable() . '_' . $field . '_' . serialize($condition) . serialize($this->bind)); $step = $this->lazyWrite('dec', $guid, $step, $lazyTime); if (false === $step) { // 清空查询条件 $this->options = []; return true; - } else { - return $this->setField($field, $step); } + return $this->setField($field, ['inc', $step]); } - return $this->setField($field, ['exp', $field . '-' . $step]); + return $this->setField($field, ['dec', $step]); } /** @@ -631,16 +716,16 @@ class Query if (!Cache::has($guid . '_time')) { // 计时开始 Cache::set($guid . '_time', $_SERVER['REQUEST_TIME'], 0); - Cache::$type($guid, $step, 0); + Cache::$type($guid, $step); } elseif ($_SERVER['REQUEST_TIME'] > Cache::get($guid . '_time') + $lazyTime) { // 删除缓存 - $value = Cache::$type($guid, $step, 0); + $value = Cache::$type($guid, $step); Cache::rm($guid); Cache::rm($guid . '_time'); return 0 === $value ? false : $value; } else { // 更新缓存 - Cache::$type($guid, $step, 0); + Cache::$type($guid, $step); } return false; } @@ -663,39 +748,51 @@ class Query } } } else { - $prefix = $this->prefix; - // 传入的表名为数组 - if (is_array($join)) { - if (0 !== $key = key($join)) { - // 设置了键名则键名为表名,键值作为表的别名 - $table = $key . ' ' . array_shift($join); - } else { - $table = array_shift($join); - } - if (count($join)) { - // 有设置第二个元素则把第二元素作为表前缀 - $table = (string) current($join) . $table; - } elseif (false === strpos($table, '.')) { - // 加上默认的表前缀 - $table = $prefix . $table; - } + $table = $this->getJoinTable($join); + + $this->options['join'][] = [$table, strtoupper($type), $condition]; + } + return $this; + } + + /** + * 获取Join表名及别名 支持 + * ['prefix_table或者子查询'=>'alias'] 'prefix_table alias' 'table alias' + * @access public + * @param array|string $join + * @return array|string + */ + protected function getJoinTable($join, &$alias = null) + { + // 传入的表名为数组 + if (is_array($join)) { + $table = $join; + $alias = array_shift($join); + } else { + $join = trim($join); + if (false !== strpos($join, '(')) { + // 使用子查询 + $table = $join; } else { - $join = trim($join); - if (0 === strpos($join, '__')) { - $table = $this->parseSqlTable($join); - } elseif (false === strpos($join, '(') && false === strpos($join, '.') && !empty($prefix) && 0 !== strpos($join, $prefix)) { - // 传入的表名中不带有'('并且不以默认的表前缀开头时加上默认的表前缀 - $table = $prefix . $join; + $prefix = $this->prefix; + if (strpos($join, ' ')) { + // 使用别名 + list($table, $alias) = explode(' ', $join); } else { $table = $join; + if (false === strpos($join, '.') && 0 !== strpos($join, '__')) { + $alias = $join; + } + } + if ($prefix && false === strpos($table, '.') && 0 !== strpos($table, $prefix) && 0 !== strpos($table, '__')) { + $table = $this->getTable($table); } } - if (is_array($condition)) { - $condition = implode(' AND ', $condition); + if (isset($alias) && $table != $alias) { + $table = [$table => $alias]; } - $this->options['join'][] = strtoupper($type) . ' JOIN ' . $table . ' ON ' . $condition; } - return $this; + return $table; } /** @@ -731,8 +828,15 @@ class Query { if (empty($field)) { return $this; + } elseif ($field instanceof Expression) { + $this->options['field'][] = $field; + return $this; } + if (is_string($field)) { + if (preg_match('/[\<\'\"\(]/', $field)) { + return $this->fieldRaw($field); + } $field = array_map('trim', explode(',', $field)); } if (true === $field) { @@ -756,37 +860,123 @@ class Query } if (isset($this->options['field'])) { - $field = array_merge($this->options['field'], $field); + $field = array_merge((array) $this->options['field'], $field); } $this->options['field'] = array_unique($field); return $this; } + /** + * 表达式方式指定查询字段 + * @access public + * @param string $field 字段名 + * @param array $bind 参数绑定 + * @return $this + */ + public function fieldRaw($field, array $bind = []) + { + $this->options['field'][] = $this->raw($field); + + if ($bind) { + $this->bind($bind); + } + + return $this; + } + + /** + * 设置数据 + * @access public + * @param mixed $field 字段名或者数据 + * @param mixed $value 字段值 + * @return $this + */ + public function data($field, $value = null) + { + if (is_array($field)) { + $this->options['data'] = isset($this->options['data']) ? array_merge($this->options['data'], $field) : $field; + } else { + $this->options['data'][$field] = $value; + } + return $this; + } + + /** + * 字段值增长 + * @access public + * @param string|array $field 字段名 + * @param integer $step 增长值 + * @return $this + */ + public function inc($field, $step = 1) + { + $fields = is_string($field) ? explode(',', $field) : $field; + foreach ($fields as $field) { + $this->data($field, ['inc', $step]); + } + return $this; + } + + /** + * 字段值减少 + * @access public + * @param string|array $field 字段名 + * @param integer $step 增长值 + * @return $this + */ + public function dec($field, $step = 1) + { + $fields = is_string($field) ? explode(',', $field) : $field; + foreach ($fields as $field) { + $this->data($field, ['dec', $step]); + } + return $this; + } + + /** + * 使用表达式设置数据 + * @access public + * @param string $field 字段名 + * @param string $value 字段值 + * @return $this + */ + public function exp($field, $value) + { + $this->data($field, $this->raw($value)); + return $this; + } + + /** + * 使用表达式设置数据 + * @access public + * @param mixed $value 表达式 + * @return Expression + */ + public function raw($value) + { + return new Expression($value); + } + /** * 指定JOIN查询字段 * @access public * @param string|array $table 数据表 * @param string|array $field 查询字段 - * @param string|array $on JOIN条件 + * @param mixed $on JOIN条件 * @param string $type JOIN类型 * @return $this */ public function view($join, $field = true, $on = null, $type = 'INNER') { $this->options['view'] = true; - if (is_array($join) && is_null($field)) { + if (is_array($join) && key($join) === 0) { foreach ($join as $key => $val) { - $this->view($key, $val[0], isset($val[1]) ? $val[1] : null, isset($val[2]) ? $val[2] : 'INNER'); + $this->view($val[0], $val[1], isset($val[2]) ? $val[2] : null, isset($val[3]) ? $val[3] : '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); + $table = $this->getJoinTable($join, $alias); + if (true === $field) { $fields = $alias . '.*'; } else { @@ -803,16 +993,16 @@ class Query } else { $name = $alias . '.' . $key; } - $fields[] = $name . ' AS ' . $val; + $fields[$name] = $val; $this->options['map'][$val] = $name; } } } $this->field($fields); if ($on) { - $this->join($table . ' ' . $alias, $on, $type); + $this->join($table, $on, $type); } else { - $this->table($table . ' ' . $alias); + $this->table($table); } } return $this; @@ -880,6 +1070,202 @@ class Query return $this; } + /** + * 指定表达式查询条件 + * @access public + * @param string $where 查询条件 + * @param array $bind 参数绑定 + * @param string $logic 查询逻辑 and or xor + * @return $this + */ + public function whereRaw($where, $bind = [], $logic = 'AND') + { + $this->options['where'][$logic][] = $this->raw($where); + + if ($bind) { + $this->bind($bind); + } + + return $this; + } + + /** + * 指定表达式查询条件 OR + * @access public + * @param string $where 查询条件 + * @param array $bind 参数绑定 + * @return $this + */ + public function whereOrRaw($where, $bind = []) + { + return $this->whereRaw($where, $bind, 'OR'); + } + + /** + * 指定Null查询条件 + * @access public + * @param mixed $field 查询字段 + * @param string $logic 查询逻辑 and or xor + * @return $this + */ + public function whereNull($field, $logic = 'AND') + { + $this->parseWhereExp($logic, $field, 'null', null, [], true); + return $this; + } + + /** + * 指定NotNull查询条件 + * @access public + * @param mixed $field 查询字段 + * @param string $logic 查询逻辑 and or xor + * @return $this + */ + public function whereNotNull($field, $logic = 'AND') + { + $this->parseWhereExp($logic, $field, 'notnull', null, [], true); + return $this; + } + + /** + * 指定Exists查询条件 + * @access public + * @param mixed $condition 查询条件 + * @param string $logic 查询逻辑 and or xor + * @return $this + */ + public function whereExists($condition, $logic = 'AND') + { + $this->options['where'][strtoupper($logic)][] = ['exists', $condition]; + return $this; + } + + /** + * 指定NotExists查询条件 + * @access public + * @param mixed $condition 查询条件 + * @param string $logic 查询逻辑 and or xor + * @return $this + */ + public function whereNotExists($condition, $logic = 'AND') + { + $this->options['where'][strtoupper($logic)][] = ['not exists', $condition]; + return $this; + } + + /** + * 指定In查询条件 + * @access public + * @param mixed $field 查询字段 + * @param mixed $condition 查询条件 + * @param string $logic 查询逻辑 and or xor + * @return $this + */ + public function whereIn($field, $condition, $logic = 'AND') + { + $this->parseWhereExp($logic, $field, 'in', $condition, [], true); + return $this; + } + + /** + * 指定NotIn查询条件 + * @access public + * @param mixed $field 查询字段 + * @param mixed $condition 查询条件 + * @param string $logic 查询逻辑 and or xor + * @return $this + */ + public function whereNotIn($field, $condition, $logic = 'AND') + { + $this->parseWhereExp($logic, $field, 'not in', $condition, [], true); + return $this; + } + + /** + * 指定Like查询条件 + * @access public + * @param mixed $field 查询字段 + * @param mixed $condition 查询条件 + * @param string $logic 查询逻辑 and or xor + * @return $this + */ + public function whereLike($field, $condition, $logic = 'AND') + { + $this->parseWhereExp($logic, $field, 'like', $condition, [], true); + return $this; + } + + /** + * 指定NotLike查询条件 + * @access public + * @param mixed $field 查询字段 + * @param mixed $condition 查询条件 + * @param string $logic 查询逻辑 and or xor + * @return $this + */ + public function whereNotLike($field, $condition, $logic = 'AND') + { + $this->parseWhereExp($logic, $field, 'not like', $condition, [], true); + return $this; + } + + /** + * 指定Between查询条件 + * @access public + * @param mixed $field 查询字段 + * @param mixed $condition 查询条件 + * @param string $logic 查询逻辑 and or xor + * @return $this + */ + public function whereBetween($field, $condition, $logic = 'AND') + { + $this->parseWhereExp($logic, $field, 'between', $condition, [], true); + return $this; + } + + /** + * 指定NotBetween查询条件 + * @access public + * @param mixed $field 查询字段 + * @param mixed $condition 查询条件 + * @param string $logic 查询逻辑 and or xor + * @return $this + */ + public function whereNotBetween($field, $condition, $logic = 'AND') + { + $this->parseWhereExp($logic, $field, 'not between', $condition, [], true); + return $this; + } + + /** + * 指定Exp查询条件 + * @access public + * @param mixed $field 查询字段 + * @param mixed $condition 查询条件 + * @param string $logic 查询逻辑 and or xor + * @return $this + */ + public function whereExp($field, $condition, $logic = 'AND') + { + $this->parseWhereExp($logic, $field, 'exp', $this->raw($condition), [], true); + return $this; + } + + /** + * 设置软删除字段及条件 + * @access public + * @param false|string $field 查询字段 + * @param mixed $condition 查询条件 + * @return $this + */ + public function useSoftDelete($field, $condition = null) + { + if ($field) { + $this->options['soft_delete'] = [$field, $condition ?: ['null', '']]; + } + return $this; + } + /** * 分析查询表达式 * @access public @@ -888,10 +1274,12 @@ class Query * @param mixed $op 查询表达式 * @param mixed $condition 查询条件 * @param array $param 查询参数 + * @param bool $strict 严格模式 * @return void */ - protected function parseWhereExp($logic, $field, $op, $condition, $param = []) + protected function parseWhereExp($logic, $field, $op, $condition, $param = [], $strict = false) { + $logic = strtoupper($logic); if ($field instanceof \Closure) { $this->options['where'][$logic][] = is_string($op) ? [$op, $field] : $field; return; @@ -900,8 +1288,17 @@ class Query if (is_string($field) && !empty($this->options['via']) && !strpos($field, '.')) { $field = $this->options['via'] . '.' . $field; } - if (is_string($field) && preg_match('/[,=\>\<\'\"\(\s]/', $field)) { - $where[] = ['exp', $field]; + + if ($field instanceof Expression) { + return $this->whereRaw($field, is_array($op) ? $op : []); + } elseif ($strict) { + // 使用严格模式查询 + $where[$field] = [$op, $condition]; + + // 记录一个字段多次查询条件 + $this->options['multi'][$logic][$field][] = $where[$field]; + } elseif (is_string($field) && preg_match('/[,=\>\<\'\"\(\s]/', $field)) { + $where[] = ['exp', $this->raw($field)]; if (is_array($op)) { // 参数绑定 $this->bind($op); @@ -910,31 +1307,100 @@ class Query if (is_array($field)) { // 数组批量查询 $where = $field; - } elseif ($field) { - // 字符串查询 - if (is_numeric($field)) { - $where[] = ['exp', $field]; - } else { - $where[$field] = ['null', '']; + foreach ($where as $k => $val) { + $this->options['multi'][$logic][$k][] = $val; } + } elseif ($field && is_string($field)) { + // 字符串查询 + $where[$field] = ['null', '']; + $this->options['multi'][$logic][$field][] = $where[$field]; } } elseif (is_array($op)) { $where[$field] = $param; } elseif (in_array(strtolower($op), ['null', 'notnull', 'not null'])) { // null查询 $where[$field] = [$op, '']; + + $this->options['multi'][$logic][$field][] = $where[$field]; } elseif (is_null($condition)) { // 字段相等查询 $where[$field] = ['eq', $op]; + + $this->options['multi'][$logic][$field][] = $where[$field]; } else { - $where[$field] = [$op, $condition]; + if ('exp' == strtolower($op)) { + $where[$field] = ['exp', $this->raw($condition)]; + // 参数绑定 + if (isset($param[2]) && is_array($param[2])) { + $this->bind($param[2]); + } + } else { + $where[$field] = [$op, $condition]; + } + // 记录一个字段多次查询条件 + $this->options['multi'][$logic][$field][] = $where[$field]; + } + + if (!empty($where)) { + if (!isset($this->options['where'][$logic])) { + $this->options['where'][$logic] = []; + } + if (is_string($field) && $this->checkMultiField($field, $logic)) { + $where[$field] = $this->options['multi'][$logic][$field]; + } elseif (is_array($field)) { + foreach ($field as $key => $val) { + if ($this->checkMultiField($key, $logic)) { + $where[$key] = $this->options['multi'][$logic][$key]; + } + } + } + $this->options['where'][$logic] = array_merge($this->options['where'][$logic], $where); + } + } + + /** + * 检查是否存在一个字段多次查询条件 + * @access public + * @param string $field 查询字段 + * @param string $logic 查询逻辑 and or xor + * @return bool + */ + private function checkMultiField($field, $logic) + { + return isset($this->options['multi'][$logic][$field]) && count($this->options['multi'][$logic][$field]) > 1; + } + + /** + * 去除某个查询条件 + * @access public + * @param string $field 查询字段 + * @param string $logic 查询逻辑 and or xor + * @return $this + */ + public function removeWhereField($field, $logic = 'AND') + { + $logic = strtoupper($logic); + if (isset($this->options['where'][$logic][$field])) { + unset($this->options['where'][$logic][$field]); + unset($this->options['multi'][$logic][$field]); } - if (!empty($where)) { - if (!isset($this->options['where'][$logic])) { - $this->options['where'][$logic] = []; - } - $this->options['where'][$logic] = array_merge($this->options['where'][$logic], $where); + return $this; + } + + /** + * 去除查询参数 + * @access public + * @param string|bool $option 参数名 true 表示去除所有参数 + * @return $this + */ + public function removeOption($option = true) + { + if (true === $option) { + $this->options = []; + } elseif (is_string($option) && isset($this->options[$option])) { + unset($this->options[$option]); } + return $this; } /** @@ -971,17 +1437,17 @@ class Query /** * 分页查询 - * @param int|null $listRows 每页数量 - * @param int|bool $simple 简洁模式或者总记录数 - * @param array $config 配置参数 - * page:当前页, - * path:url路径, - * query:url额外参数, - * fragment:url锚点, - * var_page:分页变量, - * list_rows:每页数量 - * type:分页类名 - * @return \think\paginator\Collection + * @param int|array $listRows 每页数量 数组表示配置参数 + * @param int|bool $simple 是否简洁模式或者总记录数 + * @param array $config 配置参数 + * page:当前页, + * path:url路径, + * query:url额外参数, + * fragment:url锚点, + * var_page:分页变量, + * list_rows:每页数量 + * type:分页类名 + * @return \think\Paginator * @throws DbException */ public function paginate($listRows = null, $simple = false, $config = []) @@ -990,8 +1456,13 @@ class Query $total = $simple; $simple = false; } - $config = array_merge(Config::get('paginate'), $config); - $listRows = $listRows ?: $config['list_rows']; + if (is_array($listRows)) { + $config = array_merge(Config::get('paginate'), $listRows); + $listRows = $config['list_rows']; + } else { + $config = array_merge(Config::get('paginate'), $config); + $listRows = $listRows ?: $config['list_rows']; + } /** @var Paginator $class */ $class = false !== strpos($config['type'], '\\') ? $config['type'] : '\\think\\paginator\\driver\\' . ucwords($config['type']); @@ -1006,8 +1477,11 @@ class Query if (!isset($total) && !$simple) { $options = $this->getOptions(); - $total = $this->count(); + + unset($this->options['order'], $this->options['limit'], $this->options['page'], $this->options['field']); + $bind = $this->bind; + $total = $this->count(); $results = $this->options($options)->bind($bind)->page($page, $listRows)->select(); } elseif ($simple) { $results = $this->limit(($page - 1) * $listRows, $listRows + 1)->select(); @@ -1021,11 +1495,44 @@ class Query /** * 指定当前操作的数据表 * @access public - * @param string $table 表名 + * @param mixed $table 表名 * @return $this */ public function table($table) { + if (is_string($table)) { + if (strpos($table, ')')) { + // 子查询 + } elseif (strpos($table, ',')) { + $tables = explode(',', $table); + $table = []; + foreach ($tables as $item) { + list($item, $alias) = explode(' ', trim($item)); + if ($alias) { + $this->alias([$item => $alias]); + $table[$item] = $alias; + } else { + $table[] = $item; + } + } + } elseif (strpos($table, ' ')) { + list($table, $alias) = explode(' ', $table); + + $table = [$table => $alias]; + $this->alias($table); + } + } else { + $tables = $table; + $table = []; + foreach ($tables as $key => $val) { + if (is_numeric($key)) { + $table[] = $val; + } else { + $this->alias([$key => $val]); + $table[$key] = $val; + } + } + } $this->options['table'] = $table; return $this; } @@ -1051,42 +1558,78 @@ class Query */ public function order($field, $order = null) { - if (!empty($field)) { - if (is_string($field)) { - if (!empty($this->options['via'])) { - $field = $this->options['via'] . '.' . $field; - } + if (empty($field)) { + return $this; + } elseif ($field instanceof Expression) { + $this->options['order'][] = $field; + return $this; + } + + if (is_string($field)) { + if (!empty($this->options['via'])) { + $field = $this->options['via'] . '.' . $field; + } + if (strpos($field, ',')) { + $field = array_map('trim', explode(',', $field)); + } else { $field = empty($order) ? $field : [$field => $order]; - } elseif (!empty($this->options['via'])) { - foreach ($field as $key => $val) { - if (is_numeric($key)) { - $field[$key] = $this->options['via'] . '.' . $val; - } else { - $field[$this->options['via'] . '.' . $key] = $val; - unset($field[$key]); - } + } + } elseif (!empty($this->options['via'])) { + foreach ($field as $key => $val) { + if (is_numeric($key)) { + $field[$key] = $this->options['via'] . '.' . $val; + } else { + $field[$this->options['via'] . '.' . $key] = $val; + unset($field[$key]); } } - $this->options['order'] = $field; } + if (!isset($this->options['order'])) { + $this->options['order'] = []; + } + if (is_array($field)) { + $this->options['order'] = array_merge($this->options['order'], $field); + } else { + $this->options['order'][] = $field; + } + + return $this; + } + + /** + * 表达式方式指定Field排序 + * @access public + * @param string $field 排序字段 + * @param array $bind 参数绑定 + * @return $this + */ + public function orderRaw($field, array $bind = []) + { + $this->options['order'][] = $this->raw($field); + + if ($bind) { + $this->bind($bind); + } + return $this; } /** * 查询缓存 * @access public - * @param mixed $key 缓存key - * @param integer $expire 缓存有效期 - * @param string $tag 缓存标签 + * @param mixed $key 缓存key + * @param integer|\DateTime $expire 缓存有效期 + * @param string $tag 缓存标签 * @return $this */ public function cache($key = true, $expire = null, $tag = null) { // 增加快捷调用方式 cache(10) 等同于 cache(true, 10) - if (is_numeric($key) && is_null($expire)) { + if ($key instanceof \DateTime || (is_numeric($key) && is_null($expire))) { $expire = $key; $key = true; } + if (false !== $key) { $this->options['cache'] = ['key' => $key, 'expire' => $expire, 'tag' => $tag]; } @@ -1120,7 +1663,7 @@ class Query /** * 指定查询lock * @access public - * @param boolean $lock 是否lock + * @param bool|string $lock 是否lock * @return $this */ public function lock($lock = false) @@ -1145,12 +1688,32 @@ class Query /** * 指定数据表别名 * @access public - * @param string $alias 数据表别名 + * @param mixed $alias 数据表别名 * @return $this */ public function alias($alias) { - $this->options['alias'] = $alias; + if (is_array($alias)) { + foreach ($alias as $key => $val) { + if (false !== strpos($key, '__')) { + $table = $this->parseSqlTable($key); + } else { + $table = $key; + } + $this->options['alias'][$table] = $val; + } + } else { + if (isset($this->options['table'])) { + $table = is_array($this->options['table']) ? key($this->options['table']) : $this->options['table']; + if (false !== strpos($table, '__')) { + $table = $this->parseSqlTable($table); + } + } else { + $table = $this->getTable(); + } + + $this->options['alias'][$table] = $alias; + } return $this; } @@ -1198,19 +1761,7 @@ class Query */ public function fetchPdo($pdo = true) { - $this->options['fetch_class'] = $pdo; - return $this; - } - - /** - * 指定数据集返回对象 - * @access public - * @param string $class 指定返回的数据集对象类名 - * @return $this - */ - public function fetchClass($class) - { - $this->options['fetch_class'] = $class; + $this->options['fetch_pdo'] = $pdo; return $this; } @@ -1277,44 +1828,49 @@ class Query * 查询日期或者时间 * @access public * @param string $field 日期字段名 - * @param string $op 比较运算符或者表达式 + * @param string|array $op 比较运算符或者表达式 * @param string|array $range 比较范围 * @return $this */ public function whereTime($field, $op, $range = null) { 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; + if (is_array($op)) { + $range = $op; + } else { + // 使用日期表达式 + switch (strtolower($op)) { + case 'today': + case 'd': + $range = ['today', 'tomorrow']; + break; + case 'week': + case 'w': + $range = ['this week 00:00:00', 'next week 00:00:00']; + break; + case 'month': + case 'm': + $range = ['first Day of this month 00:00:00', 'first Day of next month 00:00:00']; + break; + case 'year': + case 'y': + $range = ['this year 1/1', 'next year 1/1']; + 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 = ['first Day of last month 00:00:00', 'first Day of this month 00:00:00']; + break; + case 'last year': + $range = ['last year 1/1', 'this year 1/1']; + break; + default: + $range = $op; + } } $op = is_array($range) ? 'between' : '>'; } @@ -1325,7 +1881,7 @@ class Query /** * 获取数据表信息 * @access public - * @param string $tableName 数据表名 留空自动获取 + * @param mixed $tableName 数据表名 留空自动获取 * @param string $fetch 获取信息类型 包括 fields type bind pk * @return mixed */ @@ -1345,15 +1901,21 @@ class Query $tableName = $this->parseSqlTable($tableName); } + // 修正子查询作为表名的问题 + if (strpos($tableName, ')')) { + return []; + } + list($guid) = explode(' ', $tableName); - if (!isset(self::$info[$guid])) { + $db = $this->getConfig('database'); + if (!isset(self::$info[$db . '.' . $guid])) { if (!strpos($guid, '.')) { - $schema = $this->getConfig('database') . '.' . $guid; + $schema = $db . '.' . $guid; } else { $schema = $guid; } // 读取缓存 - if (is_file(RUNTIME_PATH . 'schema/' . $schema . '.php')) { + if (!App::$debug && is_file(RUNTIME_PATH . 'schema/' . $schema . '.php')) { $info = include RUNTIME_PATH . 'schema/' . $schema . '.php'; } else { $info = $this->connection->getFields($guid); @@ -1374,9 +1936,9 @@ class Query } else { $pk = null; } - self::$info[$guid] = ['fields' => $fields, 'type' => $type, 'bind' => $bind, 'pk' => $pk]; + self::$info[$db . '.' . $guid] = ['fields' => $fields, 'type' => $type, 'bind' => $bind, 'pk' => $pk]; } - return $fetch ? self::$info[$guid][$fetch] : self::$info[$guid]; + return $fetch ? self::$info[$db . '.' . $guid][$fetch] : self::$info[$db . '.' . $guid]; } /** @@ -1390,27 +1952,27 @@ class Query if (!empty($this->pk)) { $pk = $this->pk; } else { - $pk = $this->getTableInfo(is_array($options) ? $options['table'] : $options, 'pk'); + $pk = $this->getTableInfo(is_array($options) && isset($options['table']) ? $options['table'] : $options, 'pk'); } return $pk; } // 获取当前数据表字段信息 - public function getTableFields($options) + public function getTableFields($table = '') { - return $this->getTableInfo($options['table'], 'fields'); + return $this->getTableInfo($table ?: $this->getOptions('table'), 'fields'); } // 获取当前数据表字段类型 - public function getFieldsType($options) + public function getFieldsType($table = '') { - return $this->getTableInfo($options['table'], 'type'); + return $this->getTableInfo($table ?: $this->getOptions('table'), 'type'); } // 获取当前数据表绑定信息 - public function getFieldsBind($options) + public function getFieldsBind($table = '') { - $types = $this->getFieldsType($options); + $types = $this->getFieldsType($table); $bind = []; if ($types) { foreach ($types as $key => $type) { @@ -1428,7 +1990,9 @@ class Query */ protected function getFieldBindType($type) { - if (preg_match('/(int|double|float|decimal|real|numeric|serial)/is', $type)) { + if (0 === strpos($type, 'set') || 0 === strpos($type, 'enum')) { + $bind = PDO::PARAM_STR; + } elseif (preg_match('/(int|double|float|decimal|real|numeric|serial|bit)/is', $type)) { $bind = PDO::PARAM_INT; } elseif (preg_match('/bool/is', $type)) { $bind = PDO::PARAM_BOOL; @@ -1510,71 +2074,81 @@ class Query $with = explode(',', $with); } - $i = 0; - $currentModel = $this->model; + $first = true; /** @var Model $class */ - $class = new $currentModel; + $class = $this->model; foreach ($with as $key => $relation) { - $closure = false; + $subRelation = ''; + $closure = false; if ($relation instanceof \Closure) { // 支持闭包查询过滤关联条件 $closure = $relation; $relation = $key; $with[$key] = $key; + } elseif (is_array($relation)) { + $subRelation = $relation; + $relation = $key; } elseif (is_string($relation) && strpos($relation, '.')) { $with[$key] = $relation; list($relation, $subRelation) = explode('.', $relation, 2); } /** @var Relation $model */ - $model = $class->$relation(); - $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(); - $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']))); - $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]); - //指定获取关联的字段 - //需要在 回调中 调方法 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++; + $relation = Loader::parseName($relation, 1, false); + $model = $class->$relation(); + if ($model instanceof OneToOne && 0 == $model->getEagerlyType()) { + $model->eagerly($this, $relation, $subRelation, $closure, $first); + $first = false; } elseif ($closure) { $with[$key] = $closure; } } $this->via(); - $this->options['with'] = $with; + if (isset($this->options['with'])) { + $this->options['with'] = array_merge($this->options['with'], $with); + } else { + $this->options['with'] = $with; + } + return $this; + } + + /** + * 关联统计 + * @access public + * @param string|array $relation 关联方法名 + * @param bool $subQuery 是否使用子查询 + * @return $this + */ + public function withCount($relation, $subQuery = true) + { + if (!$subQuery) { + $this->options['with_count'] = $relation; + } else { + $relations = is_string($relation) ? explode(',', $relation) : $relation; + if (!isset($this->options['field'])) { + $this->field('*'); + } + foreach ($relations as $key => $relation) { + $closure = $name = null; + if ($relation instanceof \Closure) { + $closure = $relation; + $relation = $key; + } elseif (!is_int($key)) { + $name = $relation; + $relation = $key; + } + $relation = Loader::parseName($relation, 1, false); + + $count = '(' . $this->model->$relation()->getRelationCountQuery($closure, $name) . ')'; + + if (empty($name)) { + $name = Loader::parseName($relation) . '_count'; + } + + $this->field([$count => $name]); + } + } return $this; } @@ -1609,12 +2183,22 @@ class Query /** * 设置关联查询 * @access public - * @param string $relation 关联名称 + * @param string|array $relation 关联名称 * @return $this */ public function relation($relation) { - $this->options['relation'] = $relation; + if (empty($relation)) { + return $this; + } + if (is_string($relation)) { + $relation = explode(',', $relation); + } + if (isset($this->options['relation'])) { + $this->options['relation'] = array_merge($this->options['relation'], $relation); + } else { + $this->options['relation'] = $relation; + } return $this; } @@ -1630,8 +2214,9 @@ class Query { $pk = $this->getPk($options); // 获取当前数据表 - if (!empty($options['alias'])) { - $alias = $options['alias']; + $table = is_array($options['table']) ? key($options['table']) : $options['table']; + if (!empty($options['alias'][$table])) { + $alias = $options['alias'][$table]; } if (is_string($pk)) { $key = isset($alias) ? $alias . '.' . $pk : $pk; @@ -1672,12 +2257,13 @@ class Query * @param string $sequence 自增序列名 * @return integer|string */ - public function insert(array $data, $replace = false, $getLastInsID = false, $sequence = null) + public function insert(array $data = [], $replace = false, $getLastInsID = false, $sequence = null) { // 分析查询表达式 $options = $this->parseExpress(); + $data = array_merge($options['data'], $data); // 生成SQL语句 - $sql = $this->builder()->insert($data, $options, $replace); + $sql = $this->builder->insert($data, $options, $replace); // 获取参数绑定 $bind = $this->getBind(); if ($options['fetch_sql']) { @@ -1686,10 +2272,22 @@ class Query } // 执行操作 - $result = $this->execute($sql, $bind); - if ($getLastInsID) { - $sequence = $sequence ?: (isset($options['sequence']) ? $options['sequence'] : null); - return $this->getLastInsID($sequence); + $result = 0 === $sql ? 0 : $this->execute($sql, $bind, $this); + if ($result) { + $sequence = $sequence ?: (isset($options['sequence']) ? $options['sequence'] : null); + $lastInsId = $this->getLastInsID($sequence); + if ($lastInsId) { + $pk = $this->getPk($options); + if (is_string($pk)) { + $data[$pk] = $lastInsId; + } + } + $options['data'] = $data; + $this->trigger('after_insert', $options); + + if ($getLastInsID) { + return $lastInsId; + } } return $result; } @@ -1710,26 +2308,40 @@ class Query /** * 批量插入记录 * @access public - * @param mixed $dataSet 数据集 + * @param mixed $dataSet 数据集 + * @param boolean $replace 是否replace + * @param integer $limit 每次写入数据限制 * @return integer|string */ - public function insertAll(array $dataSet) + public function insertAll(array $dataSet, $replace = false, $limit = null) { // 分析查询表达式 $options = $this->parseExpress(); if (!is_array(reset($dataSet))) { return false; } + // 生成SQL语句 - $sql = $this->builder()->insertAll($dataSet, $options); + if (is_null($limit)) { + $sql = $this->builder->insertAll($dataSet, $options, $replace); + } else { + $array = array_chunk($dataSet, $limit, true); + foreach ($array as $item) { + $sql[] = $this->builder->insertAll($item, $options, $replace); + } + } + // 获取参数绑定 $bind = $this->getBind(); if ($options['fetch_sql']) { // 获取实际执行的SQL语句 return $this->connection->getRealSql($sql, $bind); + } elseif (is_array($sql)) { + // 执行操作 + return $this->batchQuery($sql, $bind, $this); } else { // 执行操作 - return $this->execute($sql, $bind); + return $this->execute($sql, $bind, $this); } } @@ -1747,7 +2359,7 @@ class Query $options = $this->parseExpress(); // 生成SQL语句 $table = $this->parseSqlTable($table); - $sql = $this->builder()->selectInsert($fields, $table, $options); + $sql = $this->builder->selectInsert($fields, $table, $options); // 获取参数绑定 $bind = $this->getBind(); if ($options['fetch_sql']) { @@ -1755,7 +2367,7 @@ class Query return $this->connection->getRealSql($sql, $bind); } else { // 执行操作 - return $this->execute($sql, $bind); + return $this->execute($sql, $bind, $this); } } @@ -1767,12 +2379,13 @@ class Query * @throws Exception * @throws PDOException */ - public function update(array $data) + public function update(array $data = []) { $options = $this->parseExpress(); + $data = array_merge($options['data'], $data); $pk = $this->getPk($options); - if (isset($options['cache']) && is_string($options['cache'])) { - $key = $options['cache']; + if (isset($options['cache']) && is_string($options['cache']['key'])) { + $key = $options['cache']['key']; } if (empty($options['where'])) { @@ -1801,11 +2414,12 @@ class Query } else { $options['where']['AND'] = $where; } - } elseif (is_string($pk) && isset($options['where']['AND'][$pk]) && is_scalar($options['where']['AND'][$pk])) { - $key = 'think:' . $options['table'] . '|' . $options['where']['AND'][$pk]; + } elseif (!isset($key) && is_string($pk) && isset($options['where']['AND'][$pk])) { + $key = $this->getCacheKey($options['where']['AND'][$pk], $options, $this->bind); } + // 生成UPDATE SQL语句 - $sql = $this->builder()->update($data, $options); + $sql = $this->builder->update($data, $options); // 获取参数绑定 $bind = $this->getBind(); if ($options['fetch_sql']) { @@ -1816,10 +2430,44 @@ class Query if (isset($key) && Cache::get($key)) { // 删除缓存 Cache::rm($key); + } elseif (!empty($options['cache']['tag'])) { + Cache::clear($options['cache']['tag']); } // 执行操作 - return '' == $sql ? 0 : $this->execute($sql, $bind); + $result = '' == $sql ? 0 : $this->execute($sql, $bind, $this); + if ($result) { + if (is_string($pk) && isset($where[$pk])) { + $data[$pk] = $where[$pk]; + } elseif (is_string($pk) && isset($key) && strpos($key, '|')) { + list($a, $val) = explode('|', $key); + $data[$pk] = $val; + } + $options['data'] = $data; + $this->trigger('after_update', $options); + } + return $result; + } + } + + /** + * 执行查询但只返回PDOStatement对象 + * @access public + * @return \PDOStatement|string + */ + public function getPdo() + { + // 分析查询表达式 + $options = $this->parseExpress(); + // 生成查询SQL + $sql = $this->builder->select($options); + // 获取参数绑定 + $bind = $this->getBind(); + if ($options['fetch_sql']) { + // 获取实际执行的SQL语句 + return $this->connection->getRealSql($sql, $bind); } + // 执行查询操作 + return $this->query($sql, $bind, $options['master'], true); } /** @@ -1855,63 +2503,120 @@ class Query // 判断查询缓存 $cache = $options['cache']; unset($options['cache']); - $key = is_string($cache['key']) ? $cache['key'] : md5(serialize($options)); + $key = is_string($cache['key']) ? $cache['key'] : md5($this->connection->getConfig('database') . '.' . serialize($options) . serialize($this->bind)); $resultSet = Cache::get($key); } - if (!$resultSet) { + if (false === $resultSet) { // 生成查询SQL - $sql = $this->builder()->select($options); + $sql = $this->builder->select($options); // 获取参数绑定 $bind = $this->getBind(); if ($options['fetch_sql']) { // 获取实际执行的SQL语句 return $this->connection->getRealSql($sql, $bind); } - // 执行查询操作 - $resultSet = $this->query($sql, $bind, $options['master'], $options['fetch_class']); - if ($resultSet instanceof \PDOStatement) { - // 返回PDOStatement对象 - return $resultSet; + $options['data'] = $data; + if ($resultSet = $this->trigger('before_select', $options)) { + } else { + // 执行查询操作 + $resultSet = $this->query($sql, $bind, $options['master'], $options['fetch_pdo']); + + if ($resultSet instanceof \PDOStatement) { + // 返回PDOStatement对象 + return $resultSet; + } } - if (isset($cache)) { + if (isset($cache) && false !== $resultSet) { // 缓存数据集 - if (isset($cache['tag'])) { - Cache::tag($cache['tag'])->set($key, $resultSet, $cache['expire']); - } else { - Cache::set($key, $resultSet, $cache['expire']); - } + $this->cacheData($key, $resultSet, $cache); } } - // 返回结果处理 - if (count($resultSet) > 0) { - // 数据列表读取后的处理 - if (!empty($this->model)) { - // 生成模型对象 - $model = $this->model; + // 数据列表读取后的处理 + if (!empty($this->model)) { + // 生成模型对象 + if (count($resultSet) > 0) { foreach ($resultSet as $key => $result) { - /** @var Model $result */ - $result = new $model($result); - $result->isUpdate(true); + /** @var Model $model */ + $model = $this->model->newInstance($result); + $model->isUpdate(true); + // 关联查询 if (!empty($options['relation'])) { - $result->relationQuery($options['relation']); + $model->relationQuery($options['relation']); } - $resultSet[$key] = $result; + // 关联统计 + if (!empty($options['with_count'])) { + $model->relationCount($model, $options['with_count']); + } + $resultSet[$key] = $model; } - if (!empty($options['with']) && $result instanceof Model) { + if (!empty($options['with'])) { // 预载入 - $resultSet = $result->eagerlyResultSet($resultSet, $options['with'], is_object($resultSet) ? get_class($resultSet) : ''); + $model->eagerlyResultSet($resultSet, $options['with']); } + // 模型数据集转换 + $resultSet = $model->toCollection($resultSet); + } else { + $resultSet = $this->model->toCollection($resultSet); } - } elseif (!empty($options['fail'])) { + } elseif ('collection' == $this->connection->getConfig('resultset_type')) { + // 返回Collection对象 + $resultSet = new Collection($resultSet); + } + // 返回结果处理 + if (!empty($options['fail']) && count($resultSet) == 0) { $this->throwNotFound($options); } return $resultSet; } + /** + * 缓存数据 + * @access public + * @param string $key 缓存标识 + * @param mixed $data 缓存数据 + * @param array $config 缓存参数 + */ + protected function cacheData($key, $data, $config = []) + { + if (isset($config['tag'])) { + Cache::tag($config['tag'])->set($key, $data, $config['expire']); + } else { + Cache::set($key, $data, $config['expire']); + } + } + + /** + * 生成缓存标识 + * @access public + * @param mixed $value 缓存数据 + * @param array $options 缓存参数 + * @param array $bind 绑定参数 + * @return string + */ + protected function getCacheKey($value, $options, $bind = []) + { + if (is_scalar($value)) { + $data = $value; + } elseif (is_array($value) && is_string($value[0]) && 'eq' == strtolower($value[0])) { + $data = $value[1]; + } + $prefix = $this->connection->getConfig('database') . '.'; + + if (isset($data)) { + return 'think:' . $prefix . (is_array($options['table']) ? key($options['table']) : $options['table']) . '|' . $data; + } + + try { + return md5($prefix . serialize($options) . serialize($bind)); + } catch (\Exception $e) { + throw new Exception('closure not support cache(true)'); + } + } + /** * 查找单条记录 * @access public @@ -1931,10 +2636,12 @@ class Query } // 分析查询表达式 $options = $this->parseExpress(); - + $pk = $this->getPk($options); if (!is_null($data)) { // AR模式分析主键条件 $this->parsePkWhere($data, $options); + } elseif (!empty($options['cache']) && true === $options['cache']['key'] && is_string($pk) && isset($options['where']['AND'][$pk])) { + $key = $this->getCacheKey($options['where']['AND'][$pk], $options, $this->bind); } $options['limit'] = 1; @@ -1943,62 +2650,77 @@ class Query // 判断查询缓存 $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)); + $key = 'think:' . $this->connection->getConfig('database') . '.' . (is_array($options['table']) ? key($options['table']) : $options['table']) . '|' . $data; + } elseif (is_string($cache['key'])) { + $key = $cache['key']; + } elseif (!isset($key)) { + $key = md5($this->connection->getConfig('database') . '.' . serialize($options) . serialize($this->bind)); } $result = Cache::get($key); } - if (!$result) { + if (false === $result) { // 生成查询SQL - $sql = $this->builder()->select($options); + $sql = $this->builder->select($options); // 获取参数绑定 $bind = $this->getBind(); if ($options['fetch_sql']) { // 获取实际执行的SQL语句 return $this->connection->getRealSql($sql, $bind); } - // 执行查询 - $result = $this->query($sql, $bind, $options['master'], $options['fetch_class']); + if (is_string($pk)) { + if (!is_array($data)) { + if (isset($key) && strpos($key, '|')) { + list($a, $val) = explode('|', $key); + $item[$pk] = $val; + } else { + $item[$pk] = $data; + } + $data = $item; + } + } + $options['data'] = $data; + // 事件回调 + if ($result = $this->trigger('before_find', $options)) { + } else { + // 执行查询 + $resultSet = $this->query($sql, $bind, $options['master'], $options['fetch_pdo']); - if ($result instanceof \PDOStatement) { - // 返回PDOStatement对象 - return $result; + if ($resultSet instanceof \PDOStatement) { + // 返回PDOStatement对象 + return $resultSet; + } + $result = isset($resultSet[0]) ? $resultSet[0] : null; } - if (isset($cache)) { + if (isset($cache) && $result) { // 缓存数据 - if (isset($cache['tag'])) { - Cache::tag($cache['tag'])->set($key, $result, $cache['expire']); - } else { - Cache::set($key, $result, $cache['expire']); - } + $this->cacheData($key, $result, $cache); } } // 数据处理 - if (!empty($result[0])) { - $data = $result[0]; + if (!empty($result)) { if (!empty($this->model)) { // 返回模型对象 - $model = $this->model; - $data = new $model($data); - $data->isUpdate(true, isset($options['where']['AND']) ? $options['where']['AND'] : null); + $result = $this->model->newInstance($result); + $result->isUpdate(true, isset($options['where']['AND']) ? $options['where']['AND'] : null); // 关联查询 if (!empty($options['relation'])) { - $data->relationQuery($options['relation']); + $result->relationQuery($options['relation']); } + // 预载入查询 if (!empty($options['with'])) { - // 预载入 - $data->eagerlyResult($data, $options['with'], is_object($result) ? get_class($result) : ''); + $result->eagerlyResult($result, $options['with']); + } + // 关联统计 + if (!empty($options['with_count'])) { + $result->relationCount($result, $options['with_count']); } } } elseif (!empty($options['fail'])) { $this->throwNotFound($options); - } else { - $data = null; } - return $data; + return $result; } /** @@ -2011,9 +2733,11 @@ class Query protected function throwNotFound($options = []) { if (!empty($this->model)) { - throw new ModelNotFoundException('model data Not Found:' . $this->model, $this->model, $options); + $class = get_class($this->model); + throw new ModelNotFoundException('model data Not Found:' . $class, $class, $options); } else { - throw new DataNotFoundException('table data not Found:' . $options['table'], $options['table'], $options); + $table = is_array($options['table']) ? key($options['table']) : $options['table']; + throw new DataNotFoundException('table data not Found:' . $table, $table, $options); } } @@ -2051,28 +2775,61 @@ class Query * @param integer $count 每次处理的数据数量 * @param callable $callback 处理回调方法 * @param string $column 分批处理的字段名 + * @param string $order 排序规则 * @return boolean + * @throws \LogicException */ - public function chunk($count, $callback, $column = null) + public function chunk($count, $callback, $column = null, $order = 'asc') { - $options = $this->getOptions(); - $column = $column ?: $this->getPk(isset($options['table']) ? $options['table'] : ''); - $bind = $this->bind; - $resultSet = $this->limit($count)->order($column, 'asc')->select(); + $options = $this->getOptions(); + if (empty($options['table'])) { + $options['table'] = $this->getTable(); + } + $column = $column ?: $this->getPk($options); + + if (isset($options['order'])) { + if (App::$debug) { + throw new \LogicException('chunk not support call order'); + } + unset($options['order']); + } + $bind = $this->bind; + if (is_array($column)) { + $times = 1; + $query = $this->options($options)->page($times, $count); + } else { + if (strpos($column, '.')) { + list($alias, $key) = explode('.', $column); + } else { + $key = $column; + } + $query = $this->options($options)->limit($count); + } + $resultSet = $query->order($column, $order)->select(); + + while (count($resultSet) > 0) { + if ($resultSet instanceof Collection) { + $resultSet = $resultSet->all(); + } - while (!empty($resultSet)) { if (false === call_user_func($callback, $resultSet)) { return false; } - $end = end($resultSet); - $lastId = is_array($end) ? $end[$column] : $end->$column; - $resultSet = $this->options($options) - ->limit($count) - ->bind($bind) - ->where($column, '>', $lastId) - ->order($column, 'asc') - ->select(); + + if (is_array($column)) { + $times++; + $query = $this->options($options)->page($times, $count); + } else { + $end = end($resultSet); + $lastId = is_array($end) ? $end[$key] : $end->getData($key); + $query = $this->options($options) + ->limit($count) + ->where($column, 'asc' == strtolower($order) ? '>' : '<', $lastId); + } + + $resultSet = $query->bind($bind)->order($column, $order)->select(); } + return true; } @@ -2112,8 +2869,9 @@ class Query { // 分析查询表达式 $options = $this->parseExpress(); - if (isset($options['cache']) && is_string($options['cache'])) { - $key = $options['cache']; + $pk = $this->getPk($options); + if (isset($options['cache']) && is_string($options['cache']['key'])) { + $key = $options['cache']['key']; } if (!is_null($data) && true !== $data) { @@ -2123,6 +2881,8 @@ class Query } // AR模式分析主键条件 $this->parsePkWhere($data, $options); + } elseif (!isset($key) && is_string($pk) && isset($options['where']['AND'][$pk])) { + $key = $this->getCacheKey($options['where']['AND'][$pk], $options, $this->bind); } if (true !== $data && empty($options['where'])) { @@ -2130,7 +2890,7 @@ class Query throw new Exception('delete without condition'); } // 生成删除SQL语句 - $sql = $this->builder()->delete($options); + $sql = $this->builder->delete($options); // 获取参数绑定 $bind = $this->getBind(); if ($options['fetch_sql']) { @@ -2142,9 +2902,21 @@ class Query if (isset($key) && Cache::get($key)) { // 删除缓存 Cache::rm($key); + } elseif (!empty($options['cache']['tag'])) { + Cache::clear($options['cache']['tag']); } // 执行操作 - return $this->execute($sql, $bind); + $result = $this->execute($sql, $bind, $this); + if ($result) { + if (!is_array($data) && is_string($pk) && isset($key) && strpos($key, '|')) { + list($a, $val) = explode('|', $key); + $item[$pk] = $val; + $data = $item; + } + $options['data'] = $data; + $this->trigger('after_delete', $options); + } + return $result; } /** @@ -2201,25 +2973,28 @@ class Query } } - // 表别名 - if (!empty($options['alias'])) { - $options['table'] .= ' ' . $options['alias']; - } - if (!isset($options['field'])) { $options['field'] = '*'; } + if (!isset($options['data'])) { + $options['data'] = []; + } + if (!isset($options['strict'])) { $options['strict'] = $this->getConfig('fields_strict'); } - foreach (['master', 'lock', 'fetch_class', 'fetch_sql', 'distinct'] as $name) { + foreach (['master', 'lock', 'fetch_pdo', 'fetch_sql', 'distinct'] as $name) { if (!isset($options[$name])) { $options[$name] = false; } } + if (isset(static::$readMaster['*']) || (is_string($options['table']) && isset(static::$readMaster[$options['table']]))) { + $options['master'] = true; + } + foreach (['join', 'union', 'group', 'having', 'limit', 'order', 'force', 'comment'] as $name) { if (!isset($options[$name])) { $options[$name] = ''; @@ -2239,4 +3014,32 @@ class Query return $options; } + /** + * 注册回调方法 + * @access public + * @param string $event 事件名 + * @param callable $callback 回调方法 + * @return void + */ + public static function event($event, $callback) + { + self::$event[$event] = $callback; + } + + /** + * 触发事件 + * @access protected + * @param string $event 事件名 + * @param mixed $params 额外参数 + * @return bool + */ + protected function trigger($event, $params = []) + { + $result = false; + if (isset(self::$event[$event])) { + $callback = self::$event[$event]; + $result = call_user_func_array($callback, [$params, $this]); + } + return $result; + } } diff --git a/library/think/db/builder/Mysql.php b/library/think/db/builder/Mysql.php index 04586906510ef91fddee3fb567486381d70a57e6..be2af7141f8b9cf03762dd087b9c730679ba58f2 100644 --- a/library/think/db/builder/Mysql.php +++ b/library/think/db/builder/Mysql.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2016 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- @@ -12,31 +12,115 @@ namespace think\db\builder; use think\db\Builder; +use think\Exception; /** * mysql数据库驱动 */ class Mysql extends Builder { - protected $updateSql = 'UPDATE %TABLE% %JOIN% SET %SET% %WHERE% %ORDER%%LIMIT% %LOCK%%COMMENT%'; + + protected $insertAllSql = '%INSERT% INTO %TABLE% (%FIELD%) VALUES %DATA% %COMMENT%'; + protected $updateSql = 'UPDATE %TABLE% %JOIN% SET %SET% %WHERE% %ORDER%%LIMIT% %LOCK%%COMMENT%'; + + /** + * 生成insertall SQL + * @access public + * @param array $dataSet 数据集 + * @param array $options 表达式 + * @param bool $replace 是否replace + * @return string + * @throws Exception + */ + public function insertAll($dataSet, $options = [], $replace = false) + { + // 获取合法的字段 + if ('*' == $options['field']) { + $fields = array_keys($this->query->getFieldsType($options['table'])); + } else { + $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 . ']'); + } + unset($data[$key]); + } elseif (is_null($val)) { + $data[$key] = 'NULL'; + } elseif (is_scalar($val)) { + $data[$key] = $this->parseValue($val, $key); + } elseif (is_object($val) && method_exists($val, '__toString')) { + // 对象数据写入 + $data[$key] = $val->__toString(); + } else { + // 过滤掉非标量数据 + unset($data[$key]); + } + } + $value = array_values($data); + $values[] = '( ' . implode(',', $value) . ' )'; + + if (!isset($insertFields)) { + $insertFields = array_map([$this, 'parseKey'], array_keys($data)); + } + } + + return str_replace( + ['%INSERT%', '%TABLE%', '%FIELD%', '%DATA%', '%COMMENT%'], + [ + $replace ? 'REPLACE' : 'INSERT', + $this->parseTable($options['table'], $options), + implode(' , ', $insertFields), + implode(' , ', $values), + $this->parseComment($options['comment']), + ], $this->insertAllSql); + } /** * 字段和表名处理 * @access protected - * @param string $key + * @param mixed $key + * @param array $options * @return string */ - protected function parseKey($key) + protected function parseKey($key, $options = [], $strict = false) { + if (is_numeric($key)) { + return $key; + } elseif ($key instanceof Expression) { + return $key->getValue(); + } + $key = trim($key); if (strpos($key, '$.') && false === strpos($key, '(')) { // JSON字段支持 list($field, $name) = explode('$.', $key); - $key = 'json_extract(' . $field . ', \'$.' . $name . '\')'; + return 'json_extract(' . $field . ', \'$.' . $name . '\')'; + } elseif (strpos($key, '.') && !preg_match('/[,\'\"\(\)`\s]/', $key)) { + list($table, $key) = explode('.', $key, 2); + if ('__TABLE__' == $table) { + $table = $this->query->getTable(); + } + if (isset($options['alias'][$table])) { + $table = $options['alias'][$table]; + } + } + + if ($strict && !preg_match('/^[\w\.\*]+$/', $key)) { + throw new Exception('not support data:' . $key); } - if (!preg_match('/[,\'\"\*\(\)`.\s]/', $key)) { + if ('*' != $key && ($strict || !preg_match('/[,\'\"\*\(\)`.\s]/', $key))) { $key = '`' . $key . '`'; } + if (isset($table)) { + if (strpos($table, '.')) { + $table = str_replace('.', '`.`', $table); + } + $key = '`' . $table . '`.' . $key; + } return $key; } diff --git a/library/think/db/builder/Pgsql.php b/library/think/db/builder/Pgsql.php index 0a955a5b71cc4d68efddbb1cb752f18aaca98432..acc2289619683e67a2ffa143d39ea4f813fb6ab9 100644 --- a/library/think/db/builder/Pgsql.php +++ b/library/think/db/builder/Pgsql.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2016 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- @@ -18,6 +18,8 @@ use think\db\Builder; */ class Pgsql extends Builder { + protected $insertSql = 'INSERT INTO %TABLE% (%FIELD%) VALUES (%DATA%) %COMMENT%'; + protected $insertAllSql = 'INSERT INTO %TABLE% (%FIELD%) %DATA% %COMMENT%'; /** * limit分析 @@ -42,16 +44,34 @@ class Pgsql extends Builder /** * 字段和表名处理 * @access protected - * @param string $key + * @param mixed $key + * @param array $options * @return string */ - protected function parseKey($key) + protected function parseKey($key, $options = [], $strict = false) { + if (is_numeric($key)) { + return $key; + } elseif ($key instanceof Expression) { + return $key->getValue(); + } + $key = trim($key); if (strpos($key, '$.') && false === strpos($key, '(')) { // JSON字段支持 list($field, $name) = explode('$.', $key); $key = $field . '->>\'' . $name . '\''; + } elseif (strpos($key, '.')) { + list($table, $key) = explode('.', $key, 2); + if ('__TABLE__' == $table) { + $table = $this->query->getTable(); + } + if (isset($options['alias'][$table])) { + $table = $options['alias'][$table]; + } + } + if (isset($table)) { + $key = $table . '.' . $key; } return $key; } diff --git a/library/think/db/builder/Sqlite.php b/library/think/db/builder/Sqlite.php index 17e59cb25d75a00b32cdd8e2608ca91190645d28..c727f04b8dd27d0bceadd2b90da89453449fa0f8 100644 --- a/library/think/db/builder/Sqlite.php +++ b/library/think/db/builder/Sqlite.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2016 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- @@ -22,6 +22,7 @@ class Sqlite extends Builder /** * limit * @access public + * @param string $limit * @return string */ public function parseLimit($limit) @@ -48,4 +49,34 @@ class Sqlite extends Builder return 'RANDOM()'; } + /** + * 字段和表名处理 + * @access protected + * @param mixed $key + * @param array $options + * @return string + */ + protected function parseKey($key, $options = [], $strict = false) + { + if (is_numeric($key)) { + return $key; + } elseif ($key instanceof Expression) { + return $key->getValue(); + } + + $key = trim($key); + if (strpos($key, '.')) { + list($table, $key) = explode('.', $key, 2); + if ('__TABLE__' == $table) { + $table = $this->query->getTable(); + } + if (isset($options['alias'][$table])) { + $table = $options['alias'][$table]; + } + } + if (isset($table)) { + $key = $table . '.' . $key; + } + return $key; + } } diff --git a/library/think/db/builder/Sqlsrv.php b/library/think/db/builder/Sqlsrv.php index bf630d90ffdb0e07715c06330f65f72a8a8c7557..dc425d9eb36e8ece72905ade56e315dce5fe07c6 100644 --- a/library/think/db/builder/Sqlsrv.php +++ b/library/think/db/builder/Sqlsrv.php @@ -12,6 +12,7 @@ namespace think\db\builder; use think\db\Builder; +use think\db\Expression; /** * Sqlsrv数据库驱动 @@ -21,33 +22,42 @@ 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 $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%'; + protected $deleteSql = 'DELETE FROM %TABLE% %USING% FROM %TABLE% %JOIN% %WHERE% %LIMIT% %LOCK%%COMMENT%'; + protected $insertSql = 'INSERT INTO %TABLE% (%FIELD%) VALUES (%DATA%) %COMMENT%'; + protected $insertAllSql = 'INSERT INTO %TABLE% (%FIELD%) %DATA% %COMMENT%'; /** * order分析 * @access protected * @param mixed $order + * @param array $options * @return string */ - protected function parseOrder($order) + protected function parseOrder($order, $options = []) { - if (is_array($order)) { - $array = []; - foreach ($order as $key => $val) { - if (is_numeric($key)) { - if (false === strpos($val, '(')) { - $array[] = $this->parseKey($val); - } elseif ('[rand]' == $val) { - $array[] = $this->parseRand(); - } + if (empty($order)) { + return ' ORDER BY rand()'; + } + + $array = []; + foreach ($order as $key => $val) { + if ($val instanceof Expression) { + $array[] = $val->getValue(); + } elseif (is_numeric($key)) { + if (false === strpos($val, '(')) { + $array[] = $this->parseKey($val, $options); + } elseif ('[rand]' == $val) { + $array[] = $this->parseRand(); } else { - $sort = in_array(strtolower(trim($val)), ['asc', 'desc']) ? ' ' . $val : ''; - $array[] = $this->parseKey($key) . ' ' . $sort; + $array[] = $val; } + } else { + $sort = in_array(strtolower(trim($val)), ['asc', 'desc'], true) ? ' ' . $val : ''; + $array[] = $this->parseKey($key, $options, true) . ' ' . $sort; } - $order = implode(',', $array); } - return !empty($order) ? ' ORDER BY ' . $order : ' ORDER BY rand()'; + + return ' ORDER BY ' . implode(',', $array); } /** @@ -61,17 +71,39 @@ class Sqlsrv extends Builder } /** - * 字段名分析 + * 字段和表名处理 * @access protected - * @param string $key + * @param mixed $key + * @param array $options * @return string */ - protected function parseKey($key) + protected function parseKey($key, $options = [], $strict = false) { + if (is_numeric($key)) { + return $key; + } elseif ($key instanceof Expression) { + return $key->getValue(); + } $key = trim($key); - if (!is_numeric($key) && !preg_match('/[,\'\"\*\(\)\[.\s]/', $key)) { + if (strpos($key, '.') && !preg_match('/[,\'\"\(\)\[\s]/', $key)) { + list($table, $key) = explode('.', $key, 2); + if ('__TABLE__' == $table) { + $table = $this->query->getTable(); + } + if (isset($options['alias'][$table])) { + $table = $options['alias'][$table]; + } + } + + if ($strict && !preg_match('/^[\w\.\*]+$/', $key)) { + throw new Exception('not support data:' . $key); + } + if ('*' != $key && ($strict || !preg_match('/[,\'\"\*\(\)\[.\s]/', $key))) { $key = '[' . $key . ']'; } + if (isset($table)) { + $key = '[' . $table . '].' . $key; + } return $key; } diff --git a/library/think/db/connector/Mysql.php b/library/think/db/connector/Mysql.php index 47b069fbd3c413326081454a5ea6ef4bdd8cf357..be1a85ce753b1ad4b184759cf0185475e78b2403 100644 --- a/library/think/db/connector/Mysql.php +++ b/library/think/db/connector/Mysql.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2016 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- @@ -21,6 +21,8 @@ use think\Log; class Mysql extends Connection { + protected $builder = '\\think\\db\\builder\\Mysql'; + /** * 解析pdo连接的dsn信息 * @access protected @@ -29,12 +31,15 @@ class Mysql extends Connection */ protected function parseDsn($config) { - $dsn = 'mysql:dbname=' . $config['database'] . ';host=' . $config['hostname']; - if (!empty($config['hostport'])) { - $dsn .= ';port=' . $config['hostport']; - } elseif (!empty($config['socket'])) { - $dsn .= ';unix_socket=' . $config['socket']; + if (!empty($config['socket'])) { + $dsn = 'mysql:unix_socket=' . $config['socket']; + } elseif (!empty($config['hostport'])) { + $dsn = 'mysql:host=' . $config['hostname'] . ';port=' . $config['hostport']; + } else { + $dsn = 'mysql:host=' . $config['hostname']; } + $dsn .= ';dbname=' . $config['database']; + if (!empty($config['charset'])) { $dsn .= ';charset=' . $config['charset']; } @@ -49,17 +54,15 @@ class Mysql extends Connection */ public function getFields($tableName) { - $this->initConnect(true); list($tableName) = explode(' ', $tableName); - if (strpos($tableName, '.')) { - $tableName = str_replace('.', '`.`', $tableName); + if (false === strpos($tableName, '`')) { + if (strpos($tableName, '.')) { + $tableName = str_replace('.', '`.`', $tableName); + } + $tableName = '`' . $tableName . '`'; } - $sql = 'SHOW COLUMNS FROM `' . $tableName . '`'; - // 调试开始 - $this->debug(true); - $pdo = $this->linkID->query($sql); - // 调试结束 - $this->debug(false, $sql); + $sql = 'SHOW COLUMNS FROM ' . $tableName; + $pdo = $this->query($sql, [], false, true); $result = $pdo->fetchAll(PDO::FETCH_ASSOC); $info = []; if ($result) { @@ -86,13 +89,8 @@ class Mysql extends Connection */ public function getTables($dbName = '') { - $this->initConnect(true); - $sql = !empty($dbName) ? 'SHOW TABLES FROM ' . $dbName : 'SHOW TABLES '; - // 调试开始 - $this->debug(true); - $pdo = $this->linkID->query($sql); - // 调试结束 - $this->debug(false, $sql); + $sql = !empty($dbName) ? 'SHOW TABLES FROM ' . $dbName : 'SHOW TABLES '; + $pdo = $this->query($sql, [], false, true); $result = $pdo->fetchAll(PDO::FETCH_ASSOC); $info = []; foreach ($result as $key => $val) { @@ -124,4 +122,5 @@ class Mysql extends Connection { return true; } + } diff --git a/library/think/db/connector/Pgsql.php b/library/think/db/connector/Pgsql.php index e9866cf31302599f77fac730accae7421f9e7736..bbcf57683c554ec1089d0fec542eaa9b0412ee7c 100644 --- a/library/think/db/connector/Pgsql.php +++ b/library/think/db/connector/Pgsql.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2016 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- @@ -19,6 +19,7 @@ use think\db\Connection; */ class Pgsql extends Connection { + protected $builder = '\\think\\db\\builder\\Pgsql'; /** * 解析pdo连接的dsn信息 @@ -43,14 +44,11 @@ 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 . '\');'; - // 调试开始 - $this->debug(true); - $pdo = $this->linkID->query($sql); - // 调试结束 - $this->debug(false, $sql); + + $pdo = $this->query($sql, [], false, true); $result = $pdo->fetchAll(PDO::FETCH_ASSOC); $info = []; if ($result) { @@ -77,13 +75,8 @@ class Pgsql extends Connection */ public function getTables($dbName = '') { - $this->initConnect(true); - $sql = "select tablename as Tables_in_test from pg_tables where schemaname ='public'"; - // 调试开始 - $this->debug(true); - $pdo = $this->linkID->query($sql); - // 调试结束 - $this->debug(false, $sql); + $sql = "select tablename as Tables_in_test from pg_tables where schemaname ='public'"; + $pdo = $this->query($sql, [], false, true); $result = $pdo->fetchAll(PDO::FETCH_ASSOC); $info = []; foreach ($result as $key => $val) { diff --git a/library/think/db/connector/Sqlite.php b/library/think/db/connector/Sqlite.php index 95b023e5b268805a400efd9ca7b87f836ee39d40..c4e3a72404a8fccf32926ed1072e66ff3044f63f 100644 --- a/library/think/db/connector/Sqlite.php +++ b/library/think/db/connector/Sqlite.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2016 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- @@ -20,6 +20,8 @@ use think\db\Connection; class Sqlite extends Connection { + protected $builder = '\\think\\db\\builder\\Sqlite'; + /** * 解析pdo连接的dsn信息 * @access protected @@ -40,14 +42,10 @@ class Sqlite extends Connection */ public function getFields($tableName) { - $this->initConnect(true); list($tableName) = explode(' ', $tableName); $sql = 'PRAGMA table_info( ' . $tableName . ' )'; - // 调试开始 - $this->debug(true); - $pdo = $this->linkID->query($sql); - // 调试结束 - $this->debug(false, $sql); + + $pdo = $this->query($sql, [], false, true); $result = $pdo->fetchAll(PDO::FETCH_ASSOC); $info = []; if ($result) { @@ -74,15 +72,12 @@ class Sqlite extends Connection */ public function getTables($dbName = '') { - $this->initConnect(true); + $sql = "SELECT name FROM sqlite_master WHERE type='table' " . "UNION ALL SELECT name FROM sqlite_temp_master " . "WHERE type='table' ORDER BY name"; - // 调试开始 - $this->debug(true); - $pdo = $this->linkID->query($sql); - // 调试结束 - $this->debug(false, $sql); + + $pdo = $this->query($sql, [], false, true); $result = $pdo->fetchAll(PDO::FETCH_ASSOC); $info = []; foreach ($result as $key => $val) { diff --git a/library/think/db/connector/Sqlsrv.php b/library/think/db/connector/Sqlsrv.php index 18148051362093ad13d1aae27212e84ba35b042c..35c660059525d165243e2b3881d922abb3de632e 100644 --- a/library/think/db/connector/Sqlsrv.php +++ b/library/think/db/connector/Sqlsrv.php @@ -25,7 +25,7 @@ class Sqlsrv extends Connection PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_STRINGIFY_FETCHES => false, ]; - + protected $builder = '\\think\\db\\builder\\Sqlsrv'; /** * 解析pdo连接的dsn信息 * @access protected @@ -49,20 +49,19 @@ 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 + $tableNames = explode('.', $tableName); + $tableName = isset($tableNames[1]) ? $tableNames[1] : $tableNames[0]; + + $sql = "SELECT column_name, data_type, column_default, is_nullable FROM information_schema.tables AS t JOIN information_schema.columns AS c ON t.table_catalog = c.table_catalog AND t.table_schema = c.table_schema AND t.table_name = c.table_name WHERE t.table_name = '$tableName'"; - // 调试开始 - $this->debug(true); - $pdo = $this->linkID->query($sql); - // 调试结束 - $this->debug(false, $sql); + + $pdo = $this->query($sql, [], false, true); $result = $pdo->fetchAll(PDO::FETCH_ASSOC); $info = []; if ($result) { @@ -99,16 +98,12 @@ class Sqlsrv extends Connection */ public function getTables($dbName = '') { - $this->initConnect(true); $sql = "SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE = 'BASE TABLE' "; - // 调试开始 - $this->debug(true); - $pdo = $this->linkID->query($sql); - // 调试结束 - $this->debug(false, $sql); + + $pdo = $this->query($sql, [], false, true); $result = $pdo->fetchAll(PDO::FETCH_ASSOC); $info = []; foreach ($result as $key => $val) { diff --git a/library/think/db/exception/BindParamException.php b/library/think/db/exception/BindParamException.php index 585a0d73aec6f027f8c71536ecc932d361c075c0..4ed195468e87ad4275431f42380200b9ce4cf493 100644 --- a/library/think/db/exception/BindParamException.php +++ b/library/think/db/exception/BindParamException.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2016 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- @@ -16,7 +16,7 @@ use think\exception\DbException; /** * PDO参数绑定异常 */ -class BindParamException extends DbException +class BindParamException extends DbException { /** diff --git a/library/think/db/exception/DataNotFoundException.php b/library/think/db/exception/DataNotFoundException.php index efd66d3bfc8f49edbf7db87061672258f9c8b533..f2542ac6ea07280e3286820623fb128a7e2345c1 100644 --- a/library/think/db/exception/DataNotFoundException.php +++ b/library/think/db/exception/DataNotFoundException.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2016 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- @@ -13,7 +13,7 @@ namespace think\db\exception; use think\exception\DbException; -class DataNotFoundException extends DbException +class DataNotFoundException extends DbException { protected $table; @@ -23,10 +23,10 @@ class DataNotFoundException extends DbException * @param string $table * @param array $config */ - public function __construct($message, $table = '', Array $config = []) + public function __construct($message, $table = '', array $config = []) { - $this->message = $message; - $this->table = $table; + $this->message = $message; + $this->table = $table; $this->setData('Database Config', $config); } diff --git a/library/think/db/exception/ModelNotFoundException.php b/library/think/db/exception/ModelNotFoundException.php index 69b70965bf0d7724a5ee9419fa3bbb5d99d52950..6e5f930cc2efdafda93a1177683fa9223a2d3b12 100644 --- a/library/think/db/exception/ModelNotFoundException.php +++ b/library/think/db/exception/ModelNotFoundException.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2016 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- @@ -13,7 +13,7 @@ namespace think\db\exception; use think\exception\DbException; -class ModelNotFoundException extends DbException +class ModelNotFoundException extends DbException { protected $model; @@ -22,10 +22,10 @@ class ModelNotFoundException extends DbException * @param string $message * @param string $model */ - public function __construct($message, $model = '', Array $config = []) + public function __construct($message, $model = '', array $config = []) { - $this->message = $message; - $this->model = $model; + $this->message = $message; + $this->model = $model; $this->setData('Database Config', $config); } diff --git a/library/think/debug/Console.php b/library/think/debug/Console.php index 8a232c8c1bc8a9c525f3e335eb8dea0e95ad3700..c17911b2d70e9a80f8dc655b3142a00e273ad2b5 100644 --- a/library/think/debug/Console.php +++ b/library/think/debug/Console.php @@ -137,7 +137,7 @@ JS; } break; case '错误': - $msg = str_replace("\n", '\n', $m); + $msg = str_replace("\n", '\n', json_encode($m)); $style = 'color:#F4006B;font-size:14px;'; $line[] = "console.error(\"%c{$msg}\", \"{$style}\");"; break; diff --git a/library/think/debug/Html.php b/library/think/debug/Html.php index f8651aa9277b72f6559db1328e4b9c8426ee8d41..b6be7adbe341cde1ee333eec31659e910af07e92 100644 --- a/library/think/debug/Html.php +++ b/library/think/debug/Html.php @@ -53,7 +53,7 @@ class Html return false; } // 获取基本信息 - $runtime = number_format(microtime(true) - THINK_START_TIME, 10); + $runtime = number_format(microtime(true) - THINK_START_TIME, 10, '.', ''); $reqs = $runtime > 0 ? number_format(1 / $runtime, 2) : '∞'; $mem = number_format((memory_get_usage() - THINK_START_MEM) / 1024, 2); diff --git a/library/think/exception/DbException.php b/library/think/exception/DbException.php index bd6fb442712489ba1d38992017cb016532d76361..0ae80ad1750e6c2b077401409b6bfa016c7ec3ee 100644 --- a/library/think/exception/DbException.php +++ b/library/think/exception/DbException.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2016 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- @@ -36,6 +36,7 @@ class DbException extends Exception 'Error SQL' => $sql, ]); + unset($config['username'], $config['password']); $this->setData('Database Config', $config); } diff --git a/library/think/exception/ErrorException.php b/library/think/exception/ErrorException.php index 2f1525c0584ccb4ba300d65e70873785cdd15876..b3a9a30a4c948330b7e46372e181769af8dc17f7 100644 --- a/library/think/exception/ErrorException.php +++ b/library/think/exception/ErrorException.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2016 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- diff --git a/library/think/exception/Handle.php b/library/think/exception/Handle.php index 8b6e6c40db1deede87d82353625aba93fb1efc7c..f523db09f048cea8f8fb680058e37d1d08790be7 100644 --- a/library/think/exception/Handle.php +++ b/library/think/exception/Handle.php @@ -21,11 +21,16 @@ use think\Response; class Handle { - + protected $render; protected $ignoreReport = [ '\\think\\exception\\HttpException', ]; + public function setRender($render) + { + $this->render = $render; + } + /** * Report or log an exception. * @@ -52,6 +57,10 @@ class Handle $log = "[{$data['code']}]{$data['message']}"; } + if (Config::get('record_trace')) { + $log .= "\r\n" . $exception->getTraceAsString(); + } + Log::record($log, 'error'); } } @@ -74,6 +83,13 @@ class Handle */ public function render(Exception $e) { + if ($this->render && $this->render instanceof \Closure) { + $result = call_user_func_array($this->render, [$e]); + if ($result) { + return $result; + } + } + if ($e instanceof HttpException) { return $this->renderHttpException($e); } else { diff --git a/library/think/exception/PDOException.php b/library/think/exception/PDOException.php index 677092b9cb2594d84fc31de9ba76e3d7a1fc22b8..044f82a05e4df2b59ce774fedba2f5f6deed8d60 100644 --- a/library/think/exception/PDOException.php +++ b/library/think/exception/PDOException.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2016 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- @@ -11,8 +11,6 @@ namespace think\exception; -use think\exception\DbException; - /** * PDO异常处理类 * 重新封装了系统的\PDOException类 diff --git a/library/think/exception/RouteNotFoundException.php b/library/think/exception/RouteNotFoundException.php new file mode 100644 index 0000000000000000000000000000000000000000..d22e3a63792e126963562c83b8e644aa4e027444 --- /dev/null +++ b/library/think/exception/RouteNotFoundException.php @@ -0,0 +1,22 @@ + +// +---------------------------------------------------------------------- + +namespace think\exception; + +class RouteNotFoundException extends HttpException +{ + + public function __construct() + { + parent::__construct(404, 'Route Not Found'); + } + +} diff --git a/library/think/log/driver/File.php b/library/think/log/driver/File.php index c4e462794744b9ccb6bc8629e27480129a20c237..bace4c2f45f04c0f7f8411a2a62aba88d0b6df47 100644 --- a/library/think/log/driver/File.php +++ b/library/think/log/driver/File.php @@ -11,6 +11,9 @@ namespace think\log\driver; +use think\App; +use think\Request; + /** * 本地化调试输出到文件 */ @@ -18,9 +21,12 @@ class File { protected $config = [ 'time_format' => ' c ', + 'single' => false, 'file_size' => 2097152, 'path' => LOG_PATH, 'apart_level' => [], + 'max_files' => 0, + 'json' => false, ]; // 实例化并传入参数 @@ -34,58 +40,233 @@ class File /** * 日志写入接口 * @access public - * @param array $log 日志信息 + * @param array $log 日志信息 + * @param bool $append 是否追加请求信息 * @return bool */ - public function save(array $log = []) + public function save(array $log = [], $append = false) { - $now = date($this->config['time_format']); - $destination = $this->config['path'] . date('Ym') . DS . date('d') . '.log'; + $destination = $this->getMasterLogFile(); $path = dirname($destination); !is_dir($path) && mkdir($path, 0755, true); - //检测日志文件大小,超过配置大小则备份日志文件重新生成 - if (is_file($destination) && floor($this->config['file_size']) <= filesize($destination)) { - rename($destination, dirname($destination) . DS . $_SERVER['REQUEST_TIME'] . '-' . basename($destination)); - } - - // 获取基本信息 - if (isset($_SERVER['HTTP_HOST'])) { - $current_uri = $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI']; - } else { - $current_uri = "cmd:" . implode(' ', $_SERVER['argv']); - } - - $runtime = number_format(microtime(true) - THINK_START_TIME, 10); - $reqs = $runtime > 0 ? number_format(1 / $runtime, 2) : '∞'; - $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()) . ']'; - - $info = '[ log ] ' . $current_uri . $time_str . $memory_str . $file_load . "\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 = isset($_SERVER['REQUEST_METHOD']) ? $_SERVER['REQUEST_METHOD'] : 'CLI'; - $uri = isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : ''; + $info = []; foreach ($log as $type => $val) { - $level = ''; + foreach ($val as $msg) { if (!is_string($msg)) { $msg = var_export($msg, true); } - $level .= '[ ' . $type . ' ] ' . $msg . "\r\n"; + + $info[$type][] = $this->config['json'] ? $msg : '[ ' . $type . ' ] ' . $msg; } - if (in_array($type, $this->config['apart_level'])) { + + if (!$this->config['json'] && (true === $this->config['apart_level'] || in_array($type, $this->config['apart_level']))) { // 独立记录的日志级别 - $filename = $path . DS . date('d') . '_' . $type . '.log'; - error_log("[{$now}] {$server} {$remote} {$method} {$uri}\r\n{$level}\r\n---------------------------------------------------------------\r\n", 3, $filename); + $filename = $this->getApartLevelFile($path, $type); + + $this->write($info[$type], $filename, true, $append); + unset($info[$type]); + } + } + + if ($info) { + return $this->write($info, $destination, false, $append); + } + + return true; + } + + /** + * 获取主日志文件名 + * @access public + * @return string + */ + protected function getMasterLogFile() + { + $cli = PHP_SAPI == 'cli' ? '_cli' : ''; + if ($this->config['single']) { + $name = is_string($this->config['single']) ? $this->config['single'] : 'single'; + $destination = $this->config['path'] . $name . $cli . '.log'; + } else { + if ($this->config['max_files']) { + $filename = date('Ymd') . $cli . '.log'; + $files = glob($this->config['path'] . '*.log'); + + try { + if (count($files) > $this->config['max_files']) { + unlink($files[0]); + } + } catch (\Exception $e) { + } } else { - $info .= $level; + $filename = date('Ym') . DIRECTORY_SEPARATOR . date('d') . $cli . '.log'; } + + $destination = $this->config['path'] . $filename; } - return error_log("[{$now}] {$server} {$remote} {$method} {$uri}\r\n{$info}\r\n---------------------------------------------------------------\r\n", 3, $destination); + + return $destination; } + /** + * 获取独立日志文件名 + * @access public + * @param string $path 日志目录 + * @param string $type 日志类型 + * @return string + */ + protected function getApartLevelFile($path, $type) + { + $cli = PHP_SAPI == 'cli' ? '_cli' : ''; + + if ($this->config['single']) { + $name = is_string($this->config['single']) ? $this->config['single'] : 'single'; + } elseif ($this->config['max_files']) { + $name = date('Ymd'); + } else { + $name = date('d'); + } + + return $path . DIRECTORY_SEPARATOR . $name . '_' . $type . $cli . '.log'; + } + + /** + * 日志写入 + * @access protected + * @param array $message 日志信息 + * @param string $destination 日志文件 + * @param bool $apart 是否独立文件写入 + * @param bool $append 是否追加请求信息 + * @return bool + */ + protected function write($message, $destination, $apart = false, $append = false) + { + // 检测日志文件大小,超过配置大小则备份日志文件重新生成 + $this->checkLogSize($destination); + + // 日志信息封装 + $info['timestamp'] = date($this->config['time_format']); + + foreach ($message as $type => $msg) { + $msg = is_array($msg) ? implode("\r\n", $msg) : $msg; + if (PHP_SAPI == 'cli') { + $info['msg'] = $msg; + $info['type'] = $type; + } else { + $info[$type] = $msg; + } + } + + if (PHP_SAPI == 'cli') { + $message = $this->parseCliLog($info); + } else { + // 添加调试日志 + $this->getDebugLog($info, $append, $apart); + + $message = $this->parseLog($info); + } + + return error_log($message, 3, $destination); + } + + /** + * 检查日志文件大小并自动生成备份文件 + * @access protected + * @param string $destination 日志文件 + * @return void + */ + protected function checkLogSize($destination) + { + if (is_file($destination) && floor($this->config['file_size']) <= filesize($destination)) { + try { + rename($destination, dirname($destination) . DIRECTORY_SEPARATOR . time() . '-' . basename($destination)); + } catch (\Exception $e) { + } + } + } + + /** + * CLI日志解析 + * @access protected + * @param array $info 日志信息 + * @return string + */ + protected function parseCliLog($info) + { + if ($this->config['json']) { + $message = json_encode($info, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . "\r\n"; + } else { + $now = $info['timestamp']; + unset($info['timestamp']); + + $message = implode("\r\n", $info); + + $message = "[{$now}]" . $message . "\r\n"; + } + + return $message; + } + + /** + * 解析日志 + * @access protected + * @param array $info 日志信息 + * @return string + */ + protected function parseLog($info) + { + $request = Request::instance(); + $requestInfo = [ + 'ip' => $request->ip(), + 'method' => $request->method(), + 'host' => $request->host(), + 'uri' => $request->url(), + ]; + + if ($this->config['json']) { + $info = $requestInfo + $info; + return json_encode($info, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . "\r\n"; + } + + array_unshift($info, "---------------------------------------------------------------\r\n[{$info['timestamp']}] {$requestInfo['ip']} {$requestInfo['method']} {$requestInfo['host']}{$requestInfo['uri']}"); + unset($info['timestamp']); + + return implode("\r\n", $info) . "\r\n"; + } + + protected function getDebugLog(&$info, $append, $apart) + { + if (App::$debug && $append) { + + if ($this->config['json']) { + // 获取基本信息 + $runtime = round(microtime(true) - THINK_START_TIME, 10); + $reqs = $runtime > 0 ? number_format(1 / $runtime, 2) : '∞'; + + $memory_use = number_format((memory_get_usage() - THINK_START_MEM) / 1024, 2); + + $info = [ + 'runtime' => number_format($runtime, 6) . 's', + 'reqs' => $reqs . 'req/s', + 'memory' => $memory_use . 'kb', + 'file' => count(get_included_files()), + ] + $info; + + } elseif (!$apart) { + // 增加额外的调试信息 + $runtime = round(microtime(true) - THINK_START_TIME, 10); + $reqs = $runtime > 0 ? number_format(1 / $runtime, 2) : '∞'; + + $memory_use = number_format((memory_get_usage() - THINK_START_MEM) / 1024, 2); + + $time_str = '[运行时间:' . number_format($runtime, 6) . 's] [吞吐率:' . $reqs . 'req/s]'; + $memory_str = ' [内存消耗:' . $memory_use . 'kb]'; + $file_load = ' [文件加载:' . count(get_included_files()) . ']'; + + array_unshift($info, $time_str . $memory_str . $file_load); + } + } + } } diff --git a/library/think/log/driver/Socket.php b/library/think/log/driver/Socket.php index f24b6091db6ac73281772911521d2aae54a3fbc9..4f62915bceb1fa86b08b450e9630a94ba700ec39 100644 --- a/library/think/log/driver/Socket.php +++ b/library/think/log/driver/Socket.php @@ -11,6 +11,8 @@ namespace think\log\driver; +use think\App; + /** * github: https://github.com/luofei614/SocketLog * @author luofei614 @@ -41,7 +43,7 @@ class Socket protected $allowForceClientIds = []; //配置强制推送且被授权的client_id /** - * 架构函数 + * 构造函数 * @param array $config 缓存参数 * @access public */ @@ -58,29 +60,32 @@ class Socket * @param array $log 日志信息 * @return bool */ - public function save(array $log = []) + public function save(array $log = [], $append = false) { if (!$this->check()) { return false; } - $runtime = number_format(microtime(true) - THINK_START_TIME, 10); - $reqs = $runtime > 0 ? number_format(1 / $runtime, 2) : '∞'; - $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']); + $trace = []; + if (App::$debug) { + $runtime = round(microtime(true) - THINK_START_TIME, 10); + $reqs = $runtime > 0 ? number_format(1 / $runtime, 2) : '∞'; + $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']); + } + // 基本信息 + $trace[] = [ + 'type' => 'group', + 'msg' => $current_uri . $time_str . $memory_str . $file_load, + 'css' => $this->css['page'], + ]; } - // 基本信息 - $trace[] = [ - 'type' => 'group', - 'msg' => $current_uri . $time_str . $memory_str . $file_load, - 'css' => $this->css['page'], - ]; foreach ($log as $type => $val) { $trace[] = [ @@ -204,19 +209,19 @@ class Socket } if (!isset($_SERVER[$key])) { - return null; + return; } if (empty($args)) { if (!preg_match('/SocketLog\((.*?)\)/', $_SERVER[$key], $match)) { $args = ['tabid' => null]; - return null; + return; } parse_str($match[1], $args); } if (isset($args[$name])) { return $args[$name]; } - return null; + return; } /** diff --git a/library/think/model/Collection.php b/library/think/model/Collection.php new file mode 100644 index 0000000000000000000000000000000000000000..0406533c1791a62aa660ebe1b13a09b65fb70265 --- /dev/null +++ b/library/think/model/Collection.php @@ -0,0 +1,79 @@ + +// +---------------------------------------------------------------------- + +namespace think\model; + +use think\Collection as BaseCollection; +use think\Model; + +class Collection extends BaseCollection +{ + /** + * 延迟预载入关联查询 + * @access public + * @param mixed $relation 关联 + * @return $this + */ + public function load($relation) + { + $item = current($this->items); + $item->eagerlyResultSet($this->items, $relation); + return $this; + } + + /** + * 设置需要隐藏的输出属性 + * @access public + * @param array $hidden 属性列表 + * @param bool $override 是否覆盖 + * @return $this + */ + public function hidden($hidden = [], $override = false) + { + $this->each(function ($model) use ($hidden, $override) { + /** @var Model $model */ + $model->hidden($hidden, $override); + }); + return $this; + } + + /** + * 设置需要输出的属性 + * @param array $visible + * @param bool $override 是否覆盖 + * @return $this + */ + public function visible($visible = [], $override = false) + { + $this->each(function ($model) use ($visible, $override) { + /** @var Model $model */ + $model->visible($visible, $override); + }); + return $this; + } + + /** + * 设置需要追加的输出属性 + * @access public + * @param array $append 属性列表 + * @param bool $override 是否覆盖 + * @return $this + */ + public function append($append = [], $override = false) + { + $this->each(function ($model) use ($append, $override) { + /** @var Model $model */ + $model && $model->append($append, $override); + }); + return $this; + } + +} diff --git a/library/think/model/Merge.php b/library/think/model/Merge.php index c28d0fd7558635f52abd851a7f7e9e407c264b30..4a9da81e46566b571f9388de0fed599da8fa82a2 100644 --- a/library/think/model/Merge.php +++ b/library/think/model/Merge.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2016 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- @@ -12,6 +12,7 @@ namespace think\model; use think\Db; +use think\db\Query; use think\Model; class Merge extends Model @@ -22,7 +23,7 @@ class Merge extends Model protected $mapFields = []; // 需要处理的模型映射字段,避免混淆 array( id => 'user.id' ) /** - * 架构函数 + * 构造函数 * @access public * @param array|object $data 数据 */ @@ -39,9 +40,9 @@ class Merge extends Model /** * 查找单条记录 * @access public - * @param mixed $data 主键值或者查询条件(闭包) - * @param string $with 关联预查询 - * @param bool $cache 是否缓存 + * @param mixed $data 主键值或者查询条件(闭包) + * @param string|array $with 关联预查询 + * @param bool $cache 是否缓存 * @return \think\Model */ public static function get($data = null, $with = [], $cache = false) @@ -61,14 +62,14 @@ class Merge extends Model { $class = new static(); $master = $class->name; - $fields = self::getModelField($query, $master, '', $class->mapFields); + $fields = self::getModelField($query, $master, '', $class->mapFields, $class->field); $query->alias($master)->field($fields); foreach ($class->relationModel as $key => $model) { $name = is_int($key) ? $model : $key; $table = is_int($key) ? $query->getTable($name) : $model; $query->join($table . ' ' . $name, $name . '.' . $class->fk . '=' . $master . '.' . $class->getPk()); - $fields = self::getModelField($query, $name, $table, $class->mapFields); + $fields = self::getModelField($query, $name, $table, $class->mapFields, $class->field); $query->field($fields); } return $query; @@ -77,16 +78,17 @@ class Merge extends Model /** * 获取关联模型的字段 并解决混淆 * @access protected - * @param \think\db\Query $query 查询对象 - * @param string $name 模型名称 - * @param string $table 关联表名称 - * @param array $map 字段映射 + * @param \think\db\Query $query 查询对象 + * @param string $name 模型名称 + * @param string $table 关联表名称 + * @param array $map 字段映射 + * @param array $fields 查询字段 * @return array */ - protected static function getModelField($query, $name, $table = '', $map = []) + protected static function getModelField($query, $name, $table = '', $map = [], $fields = []) { // 获取模型的字段信息 - $fields = $query->getTableInfo($table, 'fields'); + $fields = $fields ?: $query->getTableInfo($table, 'fields'); $array = []; foreach ($fields as $field) { if ($key = array_search($name . '.' . $field, $map)) { @@ -102,8 +104,9 @@ class Merge extends Model /** * 查找所有记录 * @access public - * @param mixed $data 主键列表或者查询条件(闭包) - * @param string $with 关联预查询 + * @param mixed $data 主键列表或者查询条件(闭包) + * @param array|string $with 关联预查询 + * @param bool $cache * @return array|false|string */ public static function all($data = null, $with = [], $cache = false) @@ -116,24 +119,21 @@ class Merge extends Model /** * 处理写入的模型数据 * @access public - * @param string $model 模型名称 - * @param array $data 数据 - * @param bool $insert 是否新增 - * @return void + * @param string $model 模型名称 + * @param array $data 数据 + * @return array */ - protected function parseData($model, $data, $insert = false) + protected function parseData($model, $data) { $item = []; foreach ($data as $key => $val) { - if ($insert || in_array($key, $this->change) || $this->isPk($key)) { - if ($this->fk != $key && array_key_exists($key, $this->mapFields)) { - list($name, $key) = explode('.', $this->mapFields[$key]); - if ($model == $name) { - $item[$key] = $val; - } - } else { + if ($this->fk != $key && array_key_exists($key, $this->mapFields)) { + list($name, $key) = explode('.', $this->mapFields[$key]); + if ($model == $name) { $item[$key] = $val; } + } else { + $item[$key] = $val; } } return $item; @@ -142,10 +142,11 @@ class Merge extends Model /** * 保存模型数据 以及关联数据 * @access public - * @param mixed $data 数据 - * @param array $where 更新条件 - * @param string $sequence 自增序列名 - * @return integer|false + * @param mixed $data 数据 + * @param array $where 更新条件 + * @param string $sequence 自增序列名 + * @return false|int + * @throws \Exception */ public function save($data = [], $where = [], $sequence = null) { @@ -156,7 +157,7 @@ class Merge extends Model } // 数据对象赋值 foreach ($data as $key => $value) { - $this->setAttr($key, $value); + $this->setAttr($key, $value, $data); } if (!empty($where)) { $this->isUpdate = true; @@ -167,12 +168,18 @@ class Merge extends Model $this->autoCompleteData($this->auto); // 自动写入更新时间 - if ($this->autoWriteTimestamp && $this->updateTime) { + if ($this->autoWriteTimestamp && $this->updateTime && !isset($this->data[$this->updateTime])) { $this->setAttr($this->updateTime, null); } + // 事件回调 + if (false === $this->trigger('before_write', $this)) { + return false; + } + $db = $this->db(); $db->startTrans(); + $pk = $this->getPk(); try { if ($this->isUpdate) { // 自动写入 @@ -186,19 +193,23 @@ class Merge extends Model $where = $this->updateWhere; } - if (!empty($where)) { - $pk = $this->getPk(); - - if (isset($this->mapFields[$pk])) { - $pk = $this->mapFields[$pk]; - } - if (isset($where[$pk])) { - unset($where[$pk]); + // 获取有更新的数据 + $data = $this->getChangedData(); + // 保留主键数据 + foreach ($this->data as $key => $val) { + if ($this->isPk($key)) { + $data[$key] = $val; } } - // 处理模型数据 - $data = $this->parseData($this->name, $this->data); + $data = $this->parseData($this->name, $data); + if (is_string($pk) && isset($data[$pk])) { + if (!isset($where[$pk])) { + unset($where); + $where[$pk] = $data[$pk]; + } + unset($data[$pk]); + } // 写入主表数据 $result = $db->strict(false)->where($where)->update($data); @@ -207,14 +218,12 @@ class Merge extends Model $name = is_int($key) ? $model : $key; $table = is_int($key) ? $db->getTable($model) : $model; // 处理关联模型数据 - $data = $this->parseData($name, $this->data); - $query = clone $db; - if ($query->table($table)->strict(false)->where($this->fk, $this->data[$this->getPk()])->update($data)) { + $data = $this->parseData($name, $data); + if (Db::table($table)->strict(false)->where($this->fk, $this->data[$this->getPk()])->update($data)) { $result = 1; } } - // 清空change - $this->change = []; + // 新增回调 $this->trigger('after_update', $this); } else { @@ -222,7 +231,7 @@ class Merge extends Model $this->autoCompleteData($this->insert); // 自动写入创建时间 - if ($this->autoWriteTimestamp && $this->createTime) { + if ($this->autoWriteTimestamp && $this->createTime && !isset($this->data[$this->createTime])) { $this->setAttr($this->createTime, null); } @@ -231,19 +240,15 @@ class Merge extends Model } // 处理模型数据 - $data = $this->parseData($this->name, $this->data, true); + $data = $this->parseData($this->name, $this->data); // 写入主表数据 $result = $db->name($this->name)->strict(false)->insert($data); if ($result) { $insertId = $db->getLastInsID($sequence); // 写入外键数据 - $pk = $this->getPk(); if ($insertId) { if (is_string($pk)) { $this->data[$pk] = $insertId; - if ($this->fk == $pk) { - $this->change[] = $pk; - } } $this->data[$this->fk] = $insertId; } @@ -257,19 +262,20 @@ class Merge extends Model $name = is_int($key) ? $model : $key; $table = is_int($key) ? $db->getTable($model) : $model; // 处理关联模型数据 - $data = $this->parseData($name, $source, true); - $query = clone $db; - $query->table($table)->strict(false)->insert($data); + $data = $this->parseData($name, $source); + Db::table($table)->strict(false)->insert($data); } } // 标记为更新 $this->isUpdate = true; - // 清空change - $this->change = []; // 新增回调 $this->trigger('after_insert', $this); } $db->commit(); + // 写入回调 + $this->trigger('after_write', $this); + + $this->origin = $this->data; return $result; } catch (\Exception $e) { $db->rollback(); @@ -280,7 +286,8 @@ class Merge extends Model /** * 删除当前的记录 并删除关联数据 * @access public - * @return integer + * @return int + * @throws \Exception */ public function delete() { @@ -299,7 +306,7 @@ class Merge extends Model // 删除关联数据 foreach ($this->relationModel as $key => $model) { $table = is_int($key) ? $db->getTable($model) : $model; - $query = clone $db; + $query = new Query; $query->table($table)->where($this->fk, $pk)->delete(); } } diff --git a/library/think/model/Pivot.php b/library/think/model/Pivot.php index 24034b07c91c4376668e3a352eb71c17b83c1854..13525cdd7eb2ef9e1ff495b21981570f433c9503 100644 --- a/library/think/model/Pivot.php +++ b/library/think/model/Pivot.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2016 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- @@ -16,21 +16,27 @@ use think\Model; class Pivot extends Model { + /** @var Model */ + public $parent; + + protected $autoWriteTimestamp = false; + /** * 架构函数 * @access public - * @param array|object $data 数据 - * @param string $table 中间数据表名 + * @param array|object $data 数据 + * @param Model $parent 上级模型 + * @param string $table 中间数据表名 */ - public function __construct($data = [], $table = '') + public function __construct($data = [], Model $parent = null, $table = '') { - if (is_object($data)) { - $this->data = get_object_vars($data); - } else { - $this->data = $data; + $this->parent = $parent; + + if (is_null($this->name)) { + $this->name = $table; } - $this->table = $table; + parent::__construct($data); } } diff --git a/library/think/model/Relation.php b/library/think/model/Relation.php index 22670bdd55ef9feb4cd9246b661626b1a9de6f0f..25fe88dbde69d54699895b19aba4dd388692842c 100644 --- a/library/think/model/Relation.php +++ b/library/think/model/Relation.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2016 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- @@ -11,691 +11,145 @@ namespace think\model; -use think\Db; +use think\db\Query; use think\Exception; -use think\Loader; use think\Model; -use think\model\Pivot; -class Relation +/** + * Class Relation + * @package think\model + * + * @mixin Query + */ +abstract class Relation { - const HAS_ONE = 1; - const HAS_MANY = 2; - const HAS_MANY_THROUGH = 5; - const BELONGS_TO = 3; - const BELONGS_TO_MANY = 4; - // 父模型对象 protected $parent; /** @var Model 当前关联的模型类 */ protected $model; - // 中间表模型 - protected $middle; - // 当前关联类型 - protected $type; + /** @var Query 关联模型查询对象 */ + protected $query; // 关联表外键 protected $foreignKey; - // 中间关联表外键 - protected $throughKey; // 关联表主键 protected $localKey; - // 数据表别名 - protected $alias; - // 当前关联的JOIN类型 - protected $joinType; - // 关联模型查询对象 - protected $query; - // 关联查询条件 - protected $where; - - /** - * 架构函数 - * @access public - * @param Model $model 上级模型对象 - */ - public function __construct(Model $model) - { - $this->parent = $model; - } - - /** - * 获取当前关联信息 - * @access public - * @param string $name 关联信息 - * @return array|string|integer - */ - public function getRelationInfo($name = '') - { - $info = [ - 'type' => $this->type, - 'model' => $this->model, - 'middle' => $this->middle, - 'foreignKey' => $this->foreignKey, - 'localKey' => $this->localKey, - 'alias' => $this->alias, - 'joinType' => $this->joinType, - ]; - return $name ? $info[$name] : $info; - } - - // 获取关联数据 - public function getRelation($name) - { - // 执行关联定义方法 - $relation = $this->parent->$name(); - $foreignKey = $this->foreignKey; - $localKey = $this->localKey; - - // 判断关联类型执行查询 - switch ($this->type) { - case self::HAS_ONE: - $result = $relation->where($foreignKey, $this->parent->$localKey)->find(); - break; - case self::BELONGS_TO: - $result = $relation->where($localKey, $this->parent->$foreignKey)->find(); - break; - case self::HAS_MANY: - $result = $relation->select(); - break; - case self::HAS_MANY_THROUGH: - $result = $relation->select(); - break; - case self::BELONGS_TO_MANY: - // 关联查询 - $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->getData() as $key => $val) { - if (strpos($key, '__')) { - list($name, $attr) = explode('__', $key, 2); - if ('pivot' == $name) { - $pivot[$attr] = $val; - unset($set->$key); - } - } - } - $set->pivot = new Pivot($pivot, $this->middle); - } - break; - default: - // 直接返回 - $result = $relation; - } - return $result; - } + // 基础查询 + protected $baseQuery; + // 是否为自关联 + protected $selfRelation; /** - * 预载入关联查询 返回数据集 + * 获取关联的所属模型 * @access public - * @param array $resultSet 数据集 - * @param string $relation 关联名 - * @param string $class 数据集对象名 为空表示数组 - * @return array - */ - public function eagerlyResultSet($resultSet, $relation, $class = '') - { - /** @var array $relations */ - $relations = is_string($relation) ? explode(',', $relation) : $relation; - - foreach ($relations as $key => $relation) { - $subRelation = ''; - $closure = false; - if ($relation instanceof \Closure) { - $closure = $relation; - $relation = $key; - } - if (strpos($relation, '.')) { - list($relation, $subRelation) = explode('.', $relation); - } - // 执行关联方法 - $model = $this->parent->$relation(); - // 获取关联信息 - $localKey = $this->localKey; - $foreignKey = $this->foreignKey; - switch ($this->type) { - case self::HAS_ONE: - case self::BELONGS_TO: - foreach ($resultSet as $result) { - // 模型关联组装 - $this->match($this->model, $relation, $result); - } - break; - case self::HAS_MANY: - $range = []; - foreach ($resultSet as $result) { - // 获取关联外键列表 - if (isset($result->$localKey)) { - $range[] = $result->$localKey; - } - } - - if (!empty($range)) { - $this->where[$foreignKey] = ['in', $range]; - $data = $this->eagerlyOneToMany($model, [ - $foreignKey => [ - 'in', - $range, - ], - ], $relation, $subRelation, $closure); - - // 关联数据封装 - foreach ($resultSet as $result) { - if (!isset($data[$result->$localKey])) { - $data[$result->$localKey] = []; - } - $result->setAttr($relation, $this->resultSetBuild($data[$result->$localKey], $class)); - } - } - break; - case self::BELONGS_TO_MANY: - $pk = $resultSet[0]->getPk(); - $range = []; - foreach ($resultSet as $result) { - // 获取关联外键列表 - if (isset($result->$pk)) { - $range[] = $result->$pk; - } - } - - if (!empty($range)) { - // 查询关联数据 - $data = $this->eagerlyManyToMany($model, [ - 'pivot.' . $localKey => [ - 'in', - $range, - ], - ], $relation, $subRelation); - - // 关联数据封装 - foreach ($resultSet as $result) { - if (!isset($data[$result->$pk])) { - $data[$result->$pk] = []; - } - - $result->setAttr($relation, $this->resultSetBuild($data[$result->$pk], $class)); - } - } - break; - } - } - return $resultSet; - } - - /** - * 封装关联数据集 - * @access public - * @param array $resultSet 数据集 - * @param string $class 数据集类名 - * @return mixed - */ - protected function resultSetBuild($resultSet, $class = '') - { - return $class ? new $class($resultSet) : $resultSet; - } - - /** - * 预载入关联查询 返回模型对象 - * @access public - * @param Model $result 数据对象 - * @param string $relation 关联名 - * @param string $class 数据集对象名 为空表示数组 * @return Model */ - public function eagerlyResult($result, $relation, $class = '') + public function getParent() { - $relations = is_string($relation) ? explode(',', $relation) : $relation; - - foreach ($relations as $key => $relation) { - $subRelation = ''; - $closure = false; - if ($relation instanceof \Closure) { - $closure = $relation; - $relation = $key; - } - if (strpos($relation, '.')) { - list($relation, $subRelation) = explode('.', $relation); - } - // 执行关联方法 - $model = $this->parent->$relation(); - $localKey = $this->localKey; - $foreignKey = $this->foreignKey; - switch ($this->type) { - case self::HAS_ONE: - case self::BELONGS_TO: - // 模型关联组装 - $this->match($this->model, $relation, $result); - break; - case self::HAS_MANY: - if (isset($result->$localKey)) { - $data = $this->eagerlyOneToMany($model, [$foreignKey => $result->$localKey], $relation, $subRelation, $closure); - // 关联数据封装 - if (!isset($data[$result->$localKey])) { - $data[$result->$localKey] = []; - } - $result->setAttr($relation, $this->resultSetBuild($data[$result->$localKey], $class)); - } - break; - case self::BELONGS_TO_MANY: - $pk = $result->getPk(); - if (isset($result->$pk)) { - $pk = $result->$pk; - // 查询管理数据 - $data = $this->eagerlyManyToMany($model, ['pivot.' . $localKey => $pk], $relation, $subRelation); - - // 关联数据封装 - if (!isset($data[$pk])) { - $data[$pk] = []; - } - $result->setAttr($relation, $this->resultSetBuild($data[$pk], $class)); - } - break; - - } - } - return $result; - } - - /** - * 一对一 关联模型预查询拼装 - * @access public - * @param string $model 模型名称 - * @param string $relation 关联名 - * @param Model $result 模型对象实例 - * @return void - */ - protected function match($model, $relation, &$result) - { - // 重新组装模型数据 - foreach ($result->getData() as $key => $val) { - if (strpos($key, '__')) { - list($name, $attr) = explode('__', $key, 2); - if ($name == $relation) { - $list[$name][$attr] = $val; - unset($result->$key); - } - } - } - - $result->setAttr($relation, !isset($list[$relation]) ? null : (new $model($list[$relation]))->isUpdate(true)); + return $this->parent; } /** - * 一对多 关联模型预查询 + * 获取当前的关联模型对象实例 * @access public - * @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) - { - $foreignKey = $this->foreignKey; - // 预载入关联查询 支持嵌套预载入 - if ($closure) { - call_user_func_array($closure, [ & $model]); - } - $list = $model->where($where)->with($subRelation)->select(); - - // 组装模型数据 - $data = []; - foreach ($list as $set) { - $data[$set->$foreignKey][] = $set; - } - return $data; - } - - /** - * 多对多 关联模型预查询 - * @access public - * @param object $model 关联模型对象 - * @param array $where 关联预查询条件 - * @param string $relation 关联名 - * @param string $subRelation 子关联 - * @return array + * @return Model */ - protected function eagerlyManyToMany($model, $where, $relation, $subRelation = '') + public function getModel() { - $foreignKey = $this->foreignKey; - $localKey = $this->localKey; - // 预载入关联查询 支持嵌套预载入 - $list = $this->belongsToManyQuery($model, $this->middle, $foreignKey, $localKey, $where)->with($subRelation)->select(); - - // 组装模型数据 - $data = []; - foreach ($list as $set) { - $pivot = []; - foreach ($set->getData() as $key => $val) { - if (strpos($key, '__')) { - list($name, $attr) = explode('__', $key, 2); - if ('pivot' == $name) { - $pivot[$attr] = $val; - unset($set->$key); - } - } - } - $set->pivot = new Pivot($pivot, $this->middle); - $data[$pivot[$localKey]][] = $set; - } - return $data; + return $this->query->getModel(); } /** - * 设置当前关联定义的数据表别名 + * 获取关联的查询对象 * @access public - * @param array $alias 别名定义 - * @return $this + * @return Query */ - public function setAlias($alias) + public function getQuery() { - $this->alias = $alias; - return $this; + return $this->query; } /** - * HAS ONE 关联定义 + * 设置当前关联为自关联 * @access public - * @param string $model 模型名 - * @param string $foreignKey 关联外键 - * @param string $localKey 关联主键 - * @param array $alias 别名定义 - * @param string $joinType JOIN类型 + * @param bool $self 是否自关联 * @return $this */ - public function hasOne($model, $foreignKey, $localKey, $alias = [], $joinType = 'INNER') + public function selfRelation($self = true) { - $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(); - // 返回关联的模型对象 + $this->selfRelation = $self; return $this; } /** - * BELONGS TO 关联定义 + * 当前关联是否为自关联 * @access public - * @param string $model 模型名 - * @param string $foreignKey 关联外键 - * @param string $otherKey 关联主键 - * @param array $alias 别名定义 - * @param string $joinType JOIN类型 - * @return $this + * @return bool */ - public function belongsTo($model, $foreignKey, $otherKey, $alias = [], $joinType = 'INNER') + public function isSelfRelation() { - // 记录当前关联信息 - $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; + return $this->selfRelation; } /** - * HAS MANY 关联定义 + * 封装关联数据集 * @access public - * @param string $model 模型名 - * @param string $foreignKey 关联外键 - * @param string $localKey 关联主键 - * @param array $alias 别名定义 - * @return $this + * @param array $resultSet 数据集 + * @return mixed */ - public function hasMany($model, $foreignKey, $localKey, $alias) + protected function resultSetBuild($resultSet) { - // 记录当前关联信息 - $this->type = self::HAS_MANY; - $this->model = $model; - $this->foreignKey = $foreignKey; - $this->localKey = $localKey; - $this->alias = $alias; - $this->query = (new $model)->db(); - // 返回关联的模型对象 - return $this; + return (new $this->model)->toCollection($resultSet); } - /** - * 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) + protected function getQueryFields($model) { - // 记录当前关联信息 - $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; + $fields = $this->query->getOptions('field'); + return $this->getRelationQueryFields($fields, $model); } - /** - * BELONGS TO MANY 关联定义 - * @access public - * @param string $model 模型名 - * @param string $table 中间表名 - * @param string $foreignKey 关联模型外键 - * @param string $localKey 当前模型关联键 - * @param array $alias 别名定义 - * @return $this - */ - public function belongsToMany($model, $table, $foreignKey, $localKey, $alias) + protected function getRelationQueryFields($fields, $model) { - // 记录当前关联信息 - $this->type = self::BELONGS_TO_MANY; - $this->model = $model; - $this->foreignKey = $foreignKey; - $this->localKey = $localKey; - $this->middle = $table; - $this->alias = $alias; - $this->query = (new $model)->db(); - // 返回关联的模型对象 - return $this; - } + if ($fields) { - /** - * BELONGS TO MANY 关联查询 - * @access public - * @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 = []) - { - // 关联查询封装 - $tableName = $model->getTable(); - $relationFk = $model->getPk(); - return $model::field($tableName . '.*') - ->field(true, false, $table, 'pivot', 'pivot__') - ->join($table . ' pivot', 'pivot.' . $foreignKey . '=' . $tableName . '.' . $relationFk) - ->where($condition); - } + if (is_string($fields)) { + $fields = explode(',', $fields); + } - /** - * 保存(新增)当前关联数据对象 - * @access public - * @param mixed $data 数据 可以使用数组 关联模型对象 和 关联对象的主键 - * @param array $pivot 中间表额外数据 - * @return integer - */ - public function save($data, array $pivot = []) - { - // 判断关联类型 - switch ($this->type) { - case self::HAS_ONE: - case self::BELONGS_TO: - case self::HAS_MANY: - if ($data instanceof Model) { - $data = $data->getData(); + foreach ($fields as &$field) { + if (false === strpos($field, '.')) { + $field = $model . '.' . $field; } - // 保存关联表数据 - $data[$this->foreignKey] = $this->parent->{$this->localKey}; - $model = new $this->model; - return $model->save($data); - case self::BELONGS_TO_MANY: - // 保存关联表/中间表数据 - return $this->attach($data, $pivot); - } - } - - /** - * 批量保存当前关联数据对象 - * @access public - * @param array $dataSet 数据集 - * @param array $pivot 中间表额外数据 - * @return integer - */ - public function saveAll(array $dataSet, array $pivot = []) - { - $result = false; - foreach ($dataSet as $key => $data) { - // 判断关联类型 - switch ($this->type) { - case self::HAS_MANY: - $data[$this->foreignKey] = $this->parent->{$this->localKey}; - $result = $this->save($data); - break; - case self::BELONGS_TO_MANY: - // TODO - $result = $this->attach($data, !empty($pivot) ? $pivot[$key] : []); - break; } - } - return $result; - } - - /** - * 附加关联的一个中间表数据 - * @access public - * @param mixed $data 数据 可以使用数组、关联模型对象 或者 关联对象的主键 - * @param array $pivot 中间表额外数据 - * @return integer - */ - public function attach($data, $pivot = []) - { - if (is_array($data)) { - // 保存关联表数据 - $model = new $this->model; - $model->save($data); - $id = $model->getLastInsID(); - } elseif (is_numeric($data) || is_string($data)) { - // 根据关联表主键直接写入中间表 - $id = $data; - } elseif ($data instanceof Model) { - // 根据关联表主键直接写入中间表 - $relationFk = $data->getPk(); - $id = $data->$relationFk; - } - - if ($id) { - // 保存中间表数据 - $pk = $this->parent->getPk(); - $pivot[$this->localKey] = $this->parent->$pk; - $pivot[$this->foreignKey] = $id; - $query = clone $this->parent->db(); - return $query->table($this->middle)->insert($pivot); } else { - throw new Exception('miss relation data'); + $fields = $model . '.*'; } + + return $fields; } /** - * 解除关联的一个中间表数据 - * @access public - * @param integer|array $data 数据 可以使用关联对象的主键 - * @param bool $relationDel 是否同时删除关联表数据 - * @return integer + * 执行基础查询(仅执行一次) + * @access protected + * @return void */ - public function detach($data, $relationDel = false) - { - if (is_array($data)) { - $id = $data; - } elseif (is_numeric($data) || is_string($data)) { - // 根据关联表主键直接写入中间表 - $id = $data; - } elseif ($data instanceof Model) { - // 根据关联表主键直接写入中间表 - $relationFk = $data->getPk(); - $id = $data->$relationFk; - } - // 删除中间表数据 - $pk = $this->parent->getPk(); - $pivot[$this->localKey] = $this->parent->$pk; - if (isset($id)) { - $pivot[$this->foreignKey] = is_array($id) ? ['in', $id] : $id; - } - $query = clone $this->parent->db(); - $query->table($this->middle)->where($pivot)->delete(); - - // 删除关联表数据 - if (isset($id) && $relationDel) { - $model = $this->model; - $model::destroy($id); - } - } + protected function baseQuery() + {} public function __call($method, $args) { if ($this->query) { - switch ($this->type) { - case self::HAS_MANY: - if (isset($this->where)) { - $this->query->where($this->where); - } elseif (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(); - $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; - case self::BELONGS_TO_MANY: - // TODO + // 执行基础查询 + $this->baseQuery(); - } $result = call_user_func_array([$this->query, $method], $args); - if ($result instanceof \think\db\Query) { + if ($result instanceof Query) { return $this; } else { + $this->baseQuery = false; return $result; } } else { throw new Exception('method not exists:' . __CLASS__ . '->' . $method); } } - } diff --git a/library/think/model/relation/BelongsTo.php b/library/think/model/relation/BelongsTo.php new file mode 100644 index 0000000000000000000000000000000000000000..c1cbab97df8c893f8d1ac547782ab82e6bdca799 --- /dev/null +++ b/library/think/model/relation/BelongsTo.php @@ -0,0 +1,243 @@ + +// +---------------------------------------------------------------------- + +namespace think\model\relation; + +use think\db\Query; +use think\Loader; +use think\Model; + +class BelongsTo extends OneToOne +{ + /** + * 构造函数 + * @access public + * @param Model $parent 上级模型对象 + * @param string $model 模型名 + * @param string $foreignKey 关联外键 + * @param string $localKey 关联主键 + * @param string $joinType JOIN类型 + * @param string $relation 关联名 + */ + public function __construct(Model $parent, $model, $foreignKey, $localKey, $joinType = 'INNER', $relation = null) + { + $this->parent = $parent; + $this->model = $model; + $this->foreignKey = $foreignKey; + $this->localKey = $localKey; + $this->joinType = $joinType; + $this->query = (new $model)->db(); + $this->relation = $relation; + } + + /** + * 延迟获取关联数据 + * @param string $subRelation 子关联名 + * @param \Closure $closure 闭包查询条件 + * @access public + * @return array|false|\PDOStatement|string|Model + */ + public function getRelation($subRelation = '', $closure = null) + { + $foreignKey = $this->foreignKey; + if ($closure) { + call_user_func_array($closure, [ & $this->query]); + } + $relationModel = $this->query + ->removeWhereField($this->localKey) + ->where($this->localKey, $this->parent->$foreignKey) + ->relation($subRelation) + ->find(); + + if ($relationModel) { + $relationModel->setParent(clone $this->parent); + } + + return $relationModel; + } + + /** + * 根据关联条件查询当前模型 + * @access public + * @param string $operator 比较操作符 + * @param integer $count 个数 + * @param string $id 关联表的统计字段 + * @return Query + */ + public function has($operator = '>=', $count = 1, $id = '*') + { + return $this->parent; + } + + /** + * 根据关联条件查询当前模型 + * @access public + * @param mixed $where 查询条件(数组或者闭包) + * @param mixed $fields 字段 + * @return Query + */ + public function hasWhere($where = [], $fields = null) + { + $table = $this->query->getTable(); + $model = basename(str_replace('\\', '/', get_class($this->parent))); + $relation = basename(str_replace('\\', '/', $this->model)); + + if (is_array($where)) { + foreach ($where as $key => $val) { + if (false === strpos($key, '.')) { + $where[$relation . '.' . $key] = $val; + unset($where[$key]); + } + } + } + $fields = $this->getRelationQueryFields($fields, $model); + + return $this->parent->db()->alias($model) + ->field($fields) + ->join([$table => $relation], $model . '.' . $this->foreignKey . '=' . $relation . '.' . $this->localKey, $this->joinType) + ->where($where); + } + + /** + * 预载入关联查询(数据集) + * @access public + * @param array $resultSet 数据集 + * @param string $relation 当前关联名 + * @param string $subRelation 子关联名 + * @param \Closure $closure 闭包 + * @return void + */ + protected function eagerlySet(&$resultSet, $relation, $subRelation, $closure) + { + $localKey = $this->localKey; + $foreignKey = $this->foreignKey; + + $range = []; + foreach ($resultSet as $result) { + // 获取关联外键列表 + if (isset($result->$foreignKey)) { + $range[] = $result->$foreignKey; + } + } + + if (!empty($range)) { + $this->query->removeWhereField($localKey); + $data = $this->eagerlyWhere($this->query, [ + $localKey => [ + 'in', + $range, + ], + ], $localKey, $relation, $subRelation, $closure); + // 关联属性名 + $attr = Loader::parseName($relation); + // 关联数据封装 + foreach ($resultSet as $result) { + // 关联模型 + if (!isset($data[$result->$foreignKey])) { + $relationModel = null; + } else { + $relationModel = $data[$result->$foreignKey]; + $relationModel->setParent(clone $result); + $relationModel->isUpdate(true); + } + + if (!empty($this->bindAttr)) { + // 绑定关联属性 + $this->bindAttr($relationModel, $result, $this->bindAttr); + } else { + // 设置关联属性 + $result->setRelation($attr, $relationModel); + } + } + } + } + + /** + * 预载入关联查询(数据) + * @access public + * @param Model $result 数据对象 + * @param string $relation 当前关联名 + * @param string $subRelation 子关联名 + * @param \Closure $closure 闭包 + * @return void + */ + protected function eagerlyOne(&$result, $relation, $subRelation, $closure) + { + $localKey = $this->localKey; + $foreignKey = $this->foreignKey; + $this->query->removeWhereField($localKey); + $data = $this->eagerlyWhere($this->query, [$localKey => $result->$foreignKey], $localKey, $relation, $subRelation, $closure); + // 关联模型 + if (!isset($data[$result->$foreignKey])) { + $relationModel = null; + } else { + $relationModel = $data[$result->$foreignKey]; + $relationModel->setParent(clone $result); + $relationModel->isUpdate(true); + } + if (!empty($this->bindAttr)) { + // 绑定关联属性 + $this->bindAttr($relationModel, $result, $this->bindAttr); + } else { + // 设置关联属性 + $result->setRelation(Loader::parseName($relation), $relationModel); + } + } + + /** + * 添加关联数据 + * @access public + * @param Model $model 关联模型对象 + * @return Model + */ + public function associate($model) + { + $foreignKey = $this->foreignKey; + $pk = $model->getPk(); + + $this->parent->setAttr($foreignKey, $model->$pk); + $this->parent->save(); + + return $this->parent->setRelation($this->relation, $model); + } + + /** + * 注销关联数据 + * @access public + * @return Model + */ + public function dissociate() + { + $foreignKey = $this->foreignKey; + + $this->parent->setAttr($foreignKey, null); + $this->parent->save(); + + return $this->parent->setRelation($this->relation, null); + } + + /** + * 执行基础查询(仅执行一次) + * @access protected + * @return void + */ + protected function baseQuery() + { + if (empty($this->baseQuery)) { + if (isset($this->parent->{$this->foreignKey})) { + // 关联查询带入关联条件 + $this->query->where($this->localKey, '=', $this->parent->{$this->foreignKey}); + } + + $this->baseQuery = true; + } + } +} diff --git a/library/think/model/relation/BelongsToMany.php b/library/think/model/relation/BelongsToMany.php new file mode 100644 index 0000000000000000000000000000000000000000..a41c45cee0747369b9afcd24ad3179e2b0ecb6b9 --- /dev/null +++ b/library/think/model/relation/BelongsToMany.php @@ -0,0 +1,644 @@ + +// +---------------------------------------------------------------------- + +namespace think\model\relation; + +use think\Collection; +use think\Db; +use think\db\Query; +use think\Exception; +use think\Loader; +use think\Model; +use think\model\Pivot; +use think\model\Relation; +use think\Paginator; + +class BelongsToMany extends Relation +{ + // 中间表表名 + protected $middle; + // 中间表模型名称 + protected $pivotName; + // 中间表模型对象 + protected $pivot; + // 中间表数据名称 + protected $pivotDataName = 'pivot'; + + /** + * 构造函数 + * @access public + * @param Model $parent 上级模型对象 + * @param string $model 模型名 + * @param string $table 中间表名 + * @param string $foreignKey 关联模型外键 + * @param string $localKey 当前模型关联键 + */ + public function __construct(Model $parent, $model, $table, $foreignKey, $localKey) + { + $this->parent = $parent; + $this->model = $model; + $this->foreignKey = $foreignKey; + $this->localKey = $localKey; + if (false !== strpos($table, '\\')) { + $this->pivotName = $table; + $this->middle = basename(str_replace('\\', '/', $table)); + } else { + $this->middle = $table; + } + $this->query = (new $model)->db(); + $this->pivot = $this->newPivot(); + + if ('think\model\Pivot' == get_class($this->pivot)) { + $this->pivot->name($this->middle); + } + } + + /** + * 设置中间表模型 + * @param $pivot + * @return $this + */ + public function pivot($pivot) + { + $this->pivotName = $pivot; + return $this; + } + + /** + * 设置中间表数据名称 + * @access public + * @param string $name + * @return $this + */ + public function pivotDataName($name) + { + $this->pivotDataName = $name; + return $this; + } + + /** + * 获取中间表更新条件 + * @param $data + * @return array + */ + protected function getUpdateWhere($data) + { + return [ + $this->localKey => $data[$this->localKey], + $this->foreignKey => $data[$this->foreignKey], + ]; + } + + /** + * 实例化中间表模型 + * @param array $data + * @param bool $isUpdate + * @return Pivot + * @throws Exception + */ + protected function newPivot($data = [], $isUpdate = false) + { + $class = $this->pivotName ?: '\\think\\model\\Pivot'; + $pivot = new $class($data, $this->parent, $this->middle); + if ($pivot instanceof Pivot) { + return $isUpdate ? $pivot->isUpdate(true, $this->getUpdateWhere($data)) : $pivot; + } else { + throw new Exception('pivot model must extends: \think\model\Pivot'); + } + } + + /** + * 合成中间表模型 + * @param array|Collection|Paginator $models + */ + protected function hydratePivot($models) + { + foreach ($models as $model) { + $pivot = []; + foreach ($model->getData() as $key => $val) { + if (strpos($key, '__')) { + list($name, $attr) = explode('__', $key, 2); + if ('pivot' == $name) { + $pivot[$attr] = $val; + unset($model->$key); + } + } + } + $model->setRelation($this->pivotDataName, $this->newPivot($pivot, true)); + } + } + + /** + * 创建关联查询Query对象 + * @return Query + */ + protected function buildQuery() + { + $foreignKey = $this->foreignKey; + $localKey = $this->localKey; + $pk = $this->parent->getPk(); + // 关联查询 + $condition['pivot.' . $localKey] = $this->parent->$pk; + return $this->belongsToManyQuery($foreignKey, $localKey, $condition); + } + + /** + * 延迟获取关联数据 + * @param string $subRelation 子关联名 + * @param \Closure $closure 闭包查询条件 + * @return false|\PDOStatement|string|\think\Collection + */ + public function getRelation($subRelation = '', $closure = null) + { + if ($closure) { + call_user_func_array($closure, [ & $this->query]); + } + $result = $this->buildQuery()->relation($subRelation)->select(); + $this->hydratePivot($result); + return $result; + } + + /** + * 重载select方法 + * @param null $data + * @return false|\PDOStatement|string|Collection + */ + public function select($data = null) + { + $result = $this->buildQuery()->select($data); + $this->hydratePivot($result); + return $result; + } + + /** + * 重载paginate方法 + * @param null $listRows + * @param bool $simple + * @param array $config + * @return Paginator + */ + public function paginate($listRows = null, $simple = false, $config = []) + { + $result = $this->buildQuery()->paginate($listRows, $simple, $config); + $this->hydratePivot($result); + return $result; + } + + /** + * 重载find方法 + * @param null $data + * @return array|false|\PDOStatement|string|Model + */ + public function find($data = null) + { + $result = $this->buildQuery()->find($data); + if ($result) { + $this->hydratePivot([$result]); + } + return $result; + } + + /** + * 查找多条记录 如果不存在则抛出异常 + * @access public + * @param array|string|Query|\Closure $data + * @return array|\PDOStatement|string|Model + */ + public function selectOrFail($data = null) + { + return $this->failException(true)->select($data); + } + + /** + * 查找单条记录 如果不存在则抛出异常 + * @access public + * @param array|string|Query|\Closure $data + * @return array|\PDOStatement|string|Model + */ + public function findOrFail($data = null) + { + return $this->failException(true)->find($data); + } + + /** + * 根据关联条件查询当前模型 + * @access public + * @param string $operator 比较操作符 + * @param integer $count 个数 + * @param string $id 关联表的统计字段 + * @param string $joinType JOIN类型 + * @return Query + */ + public function has($operator = '>=', $count = 1, $id = '*', $joinType = 'INNER') + { + return $this->parent; + } + + /** + * 根据关联条件查询当前模型 + * @access public + * @param mixed $where 查询条件(数组或者闭包) + * @param mixed $fields 字段 + * @return Query + * @throws Exception + */ + public function hasWhere($where = [], $fields = null) + { + throw new Exception('relation not support: hasWhere'); + } + + /** + * 设置中间表的查询条件 + * @param $field + * @param null $op + * @param null $condition + * @return $this + */ + public function wherePivot($field, $op = null, $condition = null) + { + $field = 'pivot.' . $field; + $this->query->where($field, $op, $condition); + return $this; + } + + /** + * 预载入关联查询(数据集) + * @access public + * @param array $resultSet 数据集 + * @param string $relation 当前关联名 + * @param string $subRelation 子关联名 + * @param \Closure $closure 闭包 + * @return void + */ + public function eagerlyResultSet(&$resultSet, $relation, $subRelation, $closure) + { + $localKey = $this->localKey; + $foreignKey = $this->foreignKey; + + $pk = $resultSet[0]->getPk(); + $range = []; + foreach ($resultSet as $result) { + // 获取关联外键列表 + if (isset($result->$pk)) { + $range[] = $result->$pk; + } + } + + if (!empty($range)) { + // 查询关联数据 + $data = $this->eagerlyManyToMany([ + 'pivot.' . $localKey => [ + 'in', + $range, + ], + ], $relation, $subRelation); + // 关联属性名 + $attr = Loader::parseName($relation); + // 关联数据封装 + foreach ($resultSet as $result) { + if (!isset($data[$result->$pk])) { + $data[$result->$pk] = []; + } + + $result->setRelation($attr, $this->resultSetBuild($data[$result->$pk])); + } + } + } + + /** + * 预载入关联查询(单个数据) + * @access public + * @param Model $result 数据对象 + * @param string $relation 当前关联名 + * @param string $subRelation 子关联名 + * @param \Closure $closure 闭包 + * @return void + */ + public function eagerlyResult(&$result, $relation, $subRelation, $closure) + { + $pk = $result->getPk(); + if (isset($result->$pk)) { + $pk = $result->$pk; + // 查询管理数据 + $data = $this->eagerlyManyToMany(['pivot.' . $this->localKey => $pk], $relation, $subRelation); + + // 关联数据封装 + if (!isset($data[$pk])) { + $data[$pk] = []; + } + $result->setRelation(Loader::parseName($relation), $this->resultSetBuild($data[$pk])); + } + } + + /** + * 关联统计 + * @access public + * @param Model $result 数据对象 + * @param \Closure $closure 闭包 + * @return integer + */ + public function relationCount($result, $closure) + { + $pk = $result->getPk(); + $count = 0; + if (isset($result->$pk)) { + $pk = $result->$pk; + $count = $this->belongsToManyQuery($this->foreignKey, $this->localKey, ['pivot.' . $this->localKey => $pk])->count(); + } + return $count; + } + + /** + * 获取关联统计子查询 + * @access public + * @param \Closure $closure 闭包 + * @param string $name 统计数据别名 + * @return string + */ + public function getRelationCountQuery($closure, &$name = null) + { + if ($closure) { + $return = call_user_func_array($closure, [ & $this->query]); + if ($return && is_string($return)) { + $name = $return; + } + } + + return $this->belongsToManyQuery($this->foreignKey, $this->localKey, [ + 'pivot.' . $this->localKey => [ + 'exp', + Db::raw('=' . $this->parent->getTable() . '.' . $this->parent->getPk()), + ], + ])->fetchSql()->count(); + } + + /** + * 多对多 关联模型预查询 + * @access public + * @param array $where 关联预查询条件 + * @param string $relation 关联名 + * @param string $subRelation 子关联 + * @return array + */ + protected function eagerlyManyToMany($where, $relation, $subRelation = '') + { + // 预载入关联查询 支持嵌套预载入 + $list = $this->belongsToManyQuery($this->foreignKey, $this->localKey, $where)->with($subRelation)->select(); + + // 组装模型数据 + $data = []; + foreach ($list as $set) { + $pivot = []; + foreach ($set->getData() as $key => $val) { + if (strpos($key, '__')) { + list($name, $attr) = explode('__', $key, 2); + if ('pivot' == $name) { + $pivot[$attr] = $val; + unset($set->$key); + } + } + } + $set->setRelation($this->pivotDataName, $this->newPivot($pivot, true)); + $data[$pivot[$this->localKey]][] = $set; + } + return $data; + } + + /** + * BELONGS TO MANY 关联查询 + * @access public + * @param string $foreignKey 关联模型关联键 + * @param string $localKey 当前模型关联键 + * @param array $condition 关联查询条件 + * @return Query + */ + protected function belongsToManyQuery($foreignKey, $localKey, $condition = []) + { + // 关联查询封装 + $tableName = $this->query->getTable(); + $table = $this->pivot->getTable(); + $fields = $this->getQueryFields($tableName); + + $query = $this->query->field($fields) + ->field(true, false, $table, 'pivot', 'pivot__'); + + if (empty($this->baseQuery)) { + $relationFk = $this->query->getPk(); + $query->join([$table => 'pivot'], 'pivot.' . $foreignKey . '=' . $tableName . '.' . $relationFk) + ->where($condition); + } + return $query; + } + + /** + * 保存(新增)当前关联数据对象 + * @access public + * @param mixed $data 数据 可以使用数组 关联模型对象 和 关联对象的主键 + * @param array $pivot 中间表额外数据 + * @return integer + */ + public function save($data, array $pivot = []) + { + // 保存关联表/中间表数据 + return $this->attach($data, $pivot); + } + + /** + * 批量保存当前关联数据对象 + * @access public + * @param array $dataSet 数据集 + * @param array $pivot 中间表额外数据 + * @param bool $samePivot 额外数据是否相同 + * @return integer + */ + public function saveAll(array $dataSet, array $pivot = [], $samePivot = false) + { + $result = false; + foreach ($dataSet as $key => $data) { + if (!$samePivot) { + $pivotData = isset($pivot[$key]) ? $pivot[$key] : []; + } else { + $pivotData = $pivot; + } + $result = $this->attach($data, $pivotData); + } + return $result; + } + + /** + * 附加关联的一个中间表数据 + * @access public + * @param mixed $data 数据 可以使用数组、关联模型对象 或者 关联对象的主键 + * @param array $pivot 中间表额外数据 + * @return array|Pivot + * @throws Exception + */ + public function attach($data, $pivot = []) + { + if (is_array($data)) { + if (key($data) === 0) { + $id = $data; + } else { + // 保存关联表数据 + $model = new $this->model; + $model->save($data); + $id = $model->getLastInsID(); + } + } elseif (is_numeric($data) || is_string($data)) { + // 根据关联表主键直接写入中间表 + $id = $data; + } elseif ($data instanceof Model) { + // 根据关联表主键直接写入中间表 + $relationFk = $data->getPk(); + $id = $data->$relationFk; + } + + if ($id) { + // 保存中间表数据 + $pk = $this->parent->getPk(); + $pivot[$this->localKey] = $this->parent->$pk; + $ids = (array) $id; + foreach ($ids as $id) { + $pivot[$this->foreignKey] = $id; + $this->pivot->insert($pivot, true); + $result[] = $this->newPivot($pivot, true); + } + if (count($result) == 1) { + // 返回中间表模型对象 + $result = $result[0]; + } + return $result; + } else { + throw new Exception('miss relation data'); + } + } + + /** + * 判断是否存在关联数据 + * @access public + * @param mixed $data 数据 可以使用关联模型对象 或者 关联对象的主键 + * @return Pivot + * @throws Exception + */ + public function attached($data) + { + if ($data instanceof Model) { + $relationFk = $data->getPk(); + $id = $data->$relationFk; + } else { + $id = $data; + } + + $pk = $this->parent->getPk(); + + $pivot = $this->pivot->where($this->localKey, $this->parent->$pk)->where($this->foreignKey, $id)->find(); + + return $pivot ?: false; + } + + /** + * 解除关联的一个中间表数据 + * @access public + * @param integer|array $data 数据 可以使用关联对象的主键 + * @param bool $relationDel 是否同时删除关联表数据 + * @return integer + */ + public function detach($data = null, $relationDel = false) + { + if (is_array($data)) { + $id = $data; + } elseif (is_numeric($data) || is_string($data)) { + // 根据关联表主键直接写入中间表 + $id = $data; + } elseif ($data instanceof Model) { + // 根据关联表主键直接写入中间表 + $relationFk = $data->getPk(); + $id = $data->$relationFk; + } + // 删除中间表数据 + $pk = $this->parent->getPk(); + $pivot[$this->localKey] = $this->parent->$pk; + if (isset($id)) { + $pivot[$this->foreignKey] = is_array($id) ? ['in', $id] : $id; + } + $this->pivot->where($pivot)->delete(); + // 删除关联表数据 + if (isset($id) && $relationDel) { + $model = $this->model; + $model::destroy($id); + } + } + + /** + * 数据同步 + * @param array $ids + * @param bool $detaching + * @return array + */ + public function sync($ids, $detaching = true) + { + $changes = [ + 'attached' => [], + 'detached' => [], + 'updated' => [], + ]; + $pk = $this->parent->getPk(); + $current = $this->pivot->where($this->localKey, $this->parent->$pk) + ->column($this->foreignKey); + $records = []; + + foreach ($ids as $key => $value) { + if (!is_array($value)) { + $records[$value] = []; + } else { + $records[$key] = $value; + } + } + + $detach = array_diff($current, array_keys($records)); + + if ($detaching && count($detach) > 0) { + $this->detach($detach); + + $changes['detached'] = $detach; + } + + foreach ($records as $id => $attributes) { + if (!in_array($id, $current)) { + $this->attach($id, $attributes); + $changes['attached'][] = $id; + } elseif (count($attributes) > 0 && + $this->attach($id, $attributes) + ) { + $changes['updated'][] = $id; + } + } + + return $changes; + + } + + /** + * 执行基础查询(进执行一次) + * @access protected + * @return void + */ + protected function baseQuery() + { + if (empty($this->baseQuery) && $this->parent->getData()) { + $pk = $this->parent->getPk(); + $table = $this->pivot->getTable(); + $this->query->join([$table => 'pivot'], 'pivot.' . $this->foreignKey . '=' . $this->query->getTable() . '.' . $this->query->getPk())->where('pivot.' . $this->localKey, $this->parent->$pk); + $this->baseQuery = true; + } + } + +} diff --git a/library/think/model/relation/HasMany.php b/library/think/model/relation/HasMany.php new file mode 100644 index 0000000000000000000000000000000000000000..ebab051add290d62c01f2be4b2abb8bdf6c2a362 --- /dev/null +++ b/library/think/model/relation/HasMany.php @@ -0,0 +1,318 @@ + +// +---------------------------------------------------------------------- + +namespace think\model\relation; + +use think\db\Query; +use think\Loader; +use think\Model; +use think\model\Relation; + +class HasMany extends Relation +{ + /** + * 构造函数 + * @access public + * @param Model $parent 上级模型对象 + * @param string $model 模型名 + * @param string $foreignKey 关联外键 + * @param string $localKey 当前模型主键 + */ + public function __construct(Model $parent, $model, $foreignKey, $localKey) + { + $this->parent = $parent; + $this->model = $model; + $this->foreignKey = $foreignKey; + $this->localKey = $localKey; + $this->query = (new $model)->db(); + } + + /** + * 延迟获取关联数据 + * @param string $subRelation 子关联名 + * @param \Closure $closure 闭包查询条件 + * @return false|\PDOStatement|string|\think\Collection + */ + public function getRelation($subRelation = '', $closure = null) + { + if ($closure) { + call_user_func_array($closure, [ & $this->query]); + } + $list = $this->relation($subRelation)->select(); + $parent = clone $this->parent; + + foreach ($list as &$model) { + $model->setParent($parent); + } + + return $list; + } + + /** + * 预载入关联查询 + * @access public + * @param array $resultSet 数据集 + * @param string $relation 当前关联名 + * @param string $subRelation 子关联名 + * @param \Closure $closure 闭包 + * @return void + */ + public function eagerlyResultSet(&$resultSet, $relation, $subRelation, $closure) + { + $localKey = $this->localKey; + $range = []; + foreach ($resultSet as $result) { + // 获取关联外键列表 + if (isset($result->$localKey)) { + $range[] = $result->$localKey; + } + } + + if (!empty($range)) { + $data = $this->eagerlyOneToMany($this->query, [ + $this->foreignKey => [ + 'in', + $range, + ], + ], $relation, $subRelation, $closure); + // 关联属性名 + $attr = Loader::parseName($relation); + // 关联数据封装 + foreach ($resultSet as $result) { + if (!isset($data[$result->$localKey])) { + $data[$result->$localKey] = []; + } + + foreach ($data[$result->$localKey] as &$relationModel) { + $relationModel->setParent(clone $result); + } + + $result->setRelation($attr, $this->resultSetBuild($data[$result->$localKey])); + } + } + } + + /** + * 预载入关联查询 + * @access public + * @param Model $result 数据对象 + * @param string $relation 当前关联名 + * @param string $subRelation 子关联名 + * @param \Closure $closure 闭包 + * @return void + */ + public function eagerlyResult(&$result, $relation, $subRelation, $closure) + { + $localKey = $this->localKey; + + if (isset($result->$localKey)) { + $data = $this->eagerlyOneToMany($this->query, [$this->foreignKey => $result->$localKey], $relation, $subRelation, $closure); + // 关联数据封装 + if (!isset($data[$result->$localKey])) { + $data[$result->$localKey] = []; + } + + foreach ($data[$result->$localKey] as &$relationModel) { + $relationModel->setParent(clone $result); + } + + $result->setRelation(Loader::parseName($relation), $this->resultSetBuild($data[$result->$localKey])); + } + } + + /** + * 关联统计 + * @access public + * @param Model $result 数据对象 + * @param \Closure $closure 闭包 + * @return integer + */ + public function relationCount($result, $closure) + { + $localKey = $this->localKey; + $count = 0; + if (isset($result->$localKey)) { + if ($closure) { + call_user_func_array($closure, [ & $this->query]); + } + $count = $this->query->where($this->foreignKey, $result->$localKey)->count(); + } + return $count; + } + + /** + * 创建关联统计子查询 + * @access public + * @param \Closure $closure 闭包 + * @param string $name 统计数据别名 + * @return string + */ + public function getRelationCountQuery($closure, &$name = null) + { + if ($closure) { + $return = call_user_func_array($closure, [ & $this->query]); + if ($return && is_string($return)) { + $name = $return; + } + } + $localKey = $this->localKey ?: $this->parent->getPk(); + return $this->query->whereExp($this->foreignKey, '=' . $this->parent->getTable() . '.' . $localKey)->fetchSql()->count(); + } + + /** + * 一对多 关联模型预查询 + * @access public + * @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) + { + $foreignKey = $this->foreignKey; + // 预载入关联查询 支持嵌套预载入 + if ($closure) { + call_user_func_array($closure, [ & $model]); + } + $list = $model->removeWhereField($foreignKey)->where($where)->with($subRelation)->select(); + + // 组装模型数据 + $data = []; + foreach ($list as $set) { + $data[$set->$foreignKey][] = $set; + } + return $data; + } + + /** + * 保存(新增)当前关联数据对象 + * @access public + * @param mixed $data 数据 可以使用数组 关联模型对象 和 关联对象的主键 + * @return Model|false + */ + public function save($data) + { + if ($data instanceof Model) { + $data = $data->getData(); + } + + // 保存关联表数据 + $data[$this->foreignKey] = $this->parent->{$this->localKey}; + + $model = new $this->model(); + return $model->save($data) ? $model : false; + } + + /** + * 创建关联对象实例 + * @param array $data + * @return Model + */ + public function make($data = []) + { + if ($data instanceof Model) { + $data = $data->getData(); + } + + // 保存关联表数据 + $data[$this->foreignKey] = $this->parent->{$this->localKey}; + + return new $this->model($data); + } + + /** + * 批量保存当前关联数据对象 + * @access public + * @param array $dataSet 数据集 + * @return integer + */ + public function saveAll(array $dataSet) + { + $result = false; + foreach ($dataSet as $key => $data) { + $result = $this->save($data); + } + return $result; + } + + /** + * 根据关联条件查询当前模型 + * @access public + * @param string $operator 比较操作符 + * @param integer $count 个数 + * @param string $id 关联表的统计字段 + * @param string $joinType JOIN类型 + * @return Query + */ + public function has($operator = '>=', $count = 1, $id = '*', $joinType = 'INNER') + { + $table = $this->query->getTable(); + $model = basename(str_replace('\\', '/', get_class($this->parent))); + $relation = basename(str_replace('\\', '/', $this->model)); + + return $this->parent->db() + ->alias($model) + ->field($model . '.*') + ->join([$table => $relation], $model . '.' . $this->localKey . '=' . $relation . '.' . $this->foreignKey, $joinType) + ->group($relation . '.' . $this->foreignKey) + ->having('count(' . $id . ')' . $operator . $count); + } + + /** + * 根据关联条件查询当前模型 + * @access public + * @param mixed $where 查询条件(数组或者闭包) + * @param mixed $fields 字段 + * @return Query + */ + public function hasWhere($where = [], $fields = null) + { + $table = $this->query->getTable(); + $model = basename(str_replace('\\', '/', get_class($this->parent))); + $relation = basename(str_replace('\\', '/', $this->model)); + + if (is_array($where)) { + foreach ($where as $key => $val) { + if (false === strpos($key, '.')) { + $where[$relation . '.' . $key] = $val; + unset($where[$key]); + } + } + } + + $fields = $this->getRelationQueryFields($fields, $model); + + return $this->parent->db()->alias($model) + ->field($fields) + ->group($model . '.' . $this->localKey) + ->join([$table => $relation], $model . '.' . $this->localKey . '=' . $relation . '.' . $this->foreignKey) + ->where($where); + } + + /** + * 执行基础查询(进执行一次) + * @access protected + * @return void + */ + protected function baseQuery() + { + if (empty($this->baseQuery)) { + if (isset($this->parent->{$this->localKey})) { + // 关联查询带入关联条件 + $this->query->where($this->foreignKey, $this->parent->{$this->localKey}); + } + $this->baseQuery = true; + } + } + +} diff --git a/library/think/model/relation/HasManyThrough.php b/library/think/model/relation/HasManyThrough.php new file mode 100644 index 0000000000000000000000000000000000000000..3a9a5482e07cd487b1b05fda485b17cf964addb2 --- /dev/null +++ b/library/think/model/relation/HasManyThrough.php @@ -0,0 +1,157 @@ + +// +---------------------------------------------------------------------- + +namespace think\model\relation; + +use think\db\Query; +use think\Exception; +use think\Loader; +use think\Model; +use think\model\Relation; + +class HasManyThrough extends Relation +{ + // 中间关联表外键 + protected $throughKey; + // 中间表模型 + protected $through; + + /** + * 构造函数 + * @access public + * @param Model $parent 上级模型对象 + * @param string $model 模型名 + * @param string $through 中间模型名 + * @param string $foreignKey 关联外键 + * @param string $throughKey 关联外键 + * @param string $localKey 关联主键 + */ + public function __construct(Model $parent, $model, $through, $foreignKey, $throughKey, $localKey) + { + $this->parent = $parent; + $this->model = $model; + $this->through = $through; + $this->foreignKey = $foreignKey; + $this->throughKey = $throughKey; + $this->localKey = $localKey; + $this->query = (new $model)->db(); + } + + /** + * 延迟获取关联数据 + * @param string $subRelation 子关联名 + * @param \Closure $closure 闭包查询条件 + * @return false|\PDOStatement|string|\think\Collection + */ + public function getRelation($subRelation = '', $closure = null) + { + if ($closure) { + call_user_func_array($closure, [ & $this->query]); + } + + return $this->relation($subRelation)->select(); + } + + /** + * 根据关联条件查询当前模型 + * @access public + * @param string $operator 比较操作符 + * @param integer $count 个数 + * @param string $id 关联表的统计字段 + * @param string $joinType JOIN类型 + * @return Query + */ + public function has($operator = '>=', $count = 1, $id = '*', $joinType = 'INNER') + { + return $this->parent; + } + + /** + * 根据关联条件查询当前模型 + * @access public + * @param mixed $where 查询条件(数组或者闭包) + * @param mixed $fields 字段 + * @return Query + */ + public function hasWhere($where = [], $fields = null) + { + throw new Exception('relation not support: hasWhere'); + } + + /** + * 预载入关联查询 + * @access public + * @param array $resultSet 数据集 + * @param string $relation 当前关联名 + * @param string $subRelation 子关联名 + * @param \Closure $closure 闭包 + * @return void + */ + public function eagerlyResultSet(&$resultSet, $relation, $subRelation, $closure) + {} + + /** + * 预载入关联查询 返回模型对象 + * @access public + * @param Model $result 数据对象 + * @param string $relation 当前关联名 + * @param string $subRelation 子关联名 + * @param \Closure $closure 闭包 + * @return void + */ + public function eagerlyResult(&$result, $relation, $subRelation, $closure) + {} + + /** + * 关联统计 + * @access public + * @param Model $result 数据对象 + * @param \Closure $closure 闭包 + * @return integer + */ + public function relationCount($result, $closure) + {} + + /** + * 创建关联统计子查询 + * @access public + * @param \Closure $closure 闭包 + * @param string $name 统计数据别名 + * @return string + */ + public function getRelationCountQuery($closure, &$name = null) + { + throw new Exception('relation not support: withCount'); + } + + /** + * 执行基础查询(进执行一次) + * @access protected + * @return void + */ + protected function baseQuery() + { + if (empty($this->baseQuery) && $this->parent->getData()) { + $through = $this->through; + $alias = Loader::parseName(basename(str_replace('\\', '/', $this->model))); + $throughTable = $through::getTable(); + $pk = (new $through)->getPk(); + $throughKey = $this->throughKey; + $modelTable = $this->parent->getTable(); + $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}); + $this->baseQuery = true; + } + } + +} diff --git a/library/think/model/relation/HasOne.php b/library/think/model/relation/HasOne.php new file mode 100644 index 0000000000000000000000000000000000000000..db74e4a634b12c5bd322a539071e2ab532f46b81 --- /dev/null +++ b/library/think/model/relation/HasOne.php @@ -0,0 +1,215 @@ + +// +---------------------------------------------------------------------- + +namespace think\model\relation; + +use think\db\Query; +use think\Loader; +use think\Model; + +class HasOne extends OneToOne +{ + /** + * 构造函数 + * @access public + * @param Model $parent 上级模型对象 + * @param string $model 模型名 + * @param string $foreignKey 关联外键 + * @param string $localKey 当前模型主键 + * @param string $joinType JOIN类型 + */ + public function __construct(Model $parent, $model, $foreignKey, $localKey, $joinType = 'INNER') + { + $this->parent = $parent; + $this->model = $model; + $this->foreignKey = $foreignKey; + $this->localKey = $localKey; + $this->joinType = $joinType; + $this->query = (new $model)->db(); + } + + /** + * 延迟获取关联数据 + * @param string $subRelation 子关联名 + * @param \Closure $closure 闭包查询条件 + * @return array|false|\PDOStatement|string|Model + */ + public function getRelation($subRelation = '', $closure = null) + { + // 执行关联定义方法 + $localKey = $this->localKey; + if ($closure) { + call_user_func_array($closure, [ & $this->query]); + } + // 判断关联类型执行查询 + $relationModel = $this->query + ->removeWhereField($this->foreignKey) + ->where($this->foreignKey, $this->parent->$localKey) + ->relation($subRelation) + ->find(); + + if ($relationModel) { + $relationModel->setParent(clone $this->parent); + } + + return $relationModel; + } + + /** + * 根据关联条件查询当前模型 + * @access public + * @return Query + */ + public function has() + { + $table = $this->query->getTable(); + $model = basename(str_replace('\\', '/', get_class($this->parent))); + $relation = basename(str_replace('\\', '/', $this->model)); + $localKey = $this->localKey; + $foreignKey = $this->foreignKey; + return $this->parent->db() + ->alias($model) + ->whereExists(function ($query) use ($table, $model, $relation, $localKey, $foreignKey) { + $query->table([$table => $relation])->field($relation . '.' . $foreignKey)->whereExp($model . '.' . $localKey, '=' . $relation . '.' . $foreignKey); + }); + } + + /** + * 根据关联条件查询当前模型 + * @access public + * @param mixed $where 查询条件(数组或者闭包) + * @param mixed $fields 字段 + * @return Query + */ + public function hasWhere($where = [], $fields = null) + { + $table = $this->query->getTable(); + $model = basename(str_replace('\\', '/', get_class($this->parent))); + $relation = basename(str_replace('\\', '/', $this->model)); + + if (is_array($where)) { + foreach ($where as $key => $val) { + if (false === strpos($key, '.')) { + $where[$relation . '.' . $key] = $val; + unset($where[$key]); + } + } + } + $fields = $this->getRelationQueryFields($fields, $model); + + return $this->parent->db()->alias($model) + ->field($fields) + ->join([$table => $relation], $model . '.' . $this->localKey . '=' . $relation . '.' . $this->foreignKey, $this->joinType) + ->where($where); + } + + /** + * 预载入关联查询(数据集) + * @access public + * @param array $resultSet 数据集 + * @param string $relation 当前关联名 + * @param string $subRelation 子关联名 + * @param \Closure $closure 闭包 + * @return void + */ + protected function eagerlySet(&$resultSet, $relation, $subRelation, $closure) + { + $localKey = $this->localKey; + $foreignKey = $this->foreignKey; + + $range = []; + foreach ($resultSet as $result) { + // 获取关联外键列表 + if (isset($result->$localKey)) { + $range[] = $result->$localKey; + } + } + + if (!empty($range)) { + $this->query->removeWhereField($foreignKey); + $data = $this->eagerlyWhere($this->query, [ + $foreignKey => [ + 'in', + $range, + ], + ], $foreignKey, $relation, $subRelation, $closure); + // 关联属性名 + $attr = Loader::parseName($relation); + // 关联数据封装 + foreach ($resultSet as $result) { + // 关联模型 + if (!isset($data[$result->$localKey])) { + $relationModel = null; + } else { + $relationModel = $data[$result->$localKey]; + $relationModel->setParent(clone $result); + $relationModel->isUpdate(true); + } + if (!empty($this->bindAttr)) { + // 绑定关联属性 + $this->bindAttr($relationModel, $result, $this->bindAttr); + } else { + // 设置关联属性 + $result->setRelation($attr, $relationModel); + } + } + } + } + + /** + * 预载入关联查询(数据) + * @access public + * @param Model $result 数据对象 + * @param string $relation 当前关联名 + * @param string $subRelation 子关联名 + * @param \Closure $closure 闭包 + * @return void + */ + protected function eagerlyOne(&$result, $relation, $subRelation, $closure) + { + $localKey = $this->localKey; + $foreignKey = $this->foreignKey; + $this->query->removeWhereField($foreignKey); + $data = $this->eagerlyWhere($this->query, [$foreignKey => $result->$localKey], $foreignKey, $relation, $subRelation, $closure); + + // 关联模型 + if (!isset($data[$result->$localKey])) { + $relationModel = null; + } else { + $relationModel = $data[$result->$localKey]; + $relationModel->setParent(clone $result); + $relationModel->isUpdate(true); + } + if (!empty($this->bindAttr)) { + // 绑定关联属性 + $this->bindAttr($relationModel, $result, $this->bindAttr); + } else { + $result->setRelation(Loader::parseName($relation), $relationModel); + } + } + + /** + * 执行基础查询(仅执行一次) + * @access protected + * @return void + */ + protected function baseQuery() + { + if (empty($this->baseQuery)) { + if (isset($this->parent->{$this->localKey})) { + // 关联查询带入关联条件 + $this->query->where($this->foreignKey, '=', $this->parent->{$this->localKey}); + } + + $this->baseQuery = true; + } + } +} diff --git a/library/think/model/relation/MorphMany.php b/library/think/model/relation/MorphMany.php new file mode 100644 index 0000000000000000000000000000000000000000..2755d57533db82f72d66ab8960b934639211f234 --- /dev/null +++ b/library/think/model/relation/MorphMany.php @@ -0,0 +1,314 @@ + +// +---------------------------------------------------------------------- + +namespace think\model\relation; + +use think\Db; +use think\db\Query; +use think\Exception; +use think\Loader; +use think\Model; +use think\model\Relation; + +class MorphMany extends Relation +{ + // 多态字段 + protected $morphKey; + protected $morphType; + // 多态类型 + protected $type; + + /** + * 构造函数 + * @access public + * @param Model $parent 上级模型对象 + * @param string $model 模型名 + * @param string $morphKey 关联外键 + * @param string $morphType 多态字段名 + * @param string $type 多态类型 + */ + public function __construct(Model $parent, $model, $morphKey, $morphType, $type) + { + $this->parent = $parent; + $this->model = $model; + $this->type = $type; + $this->morphKey = $morphKey; + $this->morphType = $morphType; + $this->query = (new $model)->db(); + } + + /** + * 延迟获取关联数据 + * @param string $subRelation 子关联名 + * @param \Closure $closure 闭包查询条件 + * @return false|\PDOStatement|string|\think\Collection + */ + public function getRelation($subRelation = '', $closure = null) + { + if ($closure) { + call_user_func_array($closure, [ & $this->query]); + } + $list = $this->relation($subRelation)->select(); + $parent = clone $this->parent; + + foreach ($list as &$model) { + $model->setParent($parent); + } + + return $list; + } + + /** + * 根据关联条件查询当前模型 + * @access public + * @param string $operator 比较操作符 + * @param integer $count 个数 + * @param string $id 关联表的统计字段 + * @param string $joinType JOIN类型 + * @return Query + */ + public function has($operator = '>=', $count = 1, $id = '*', $joinType = 'INNER') + { + throw new Exception('relation not support: has'); + } + + /** + * 根据关联条件查询当前模型 + * @access public + * @param mixed $where 查询条件(数组或者闭包) + * @param mixed $fields 字段 + * @return Query + */ + public function hasWhere($where = [], $fields = null) + { + throw new Exception('relation not support: hasWhere'); + } + + /** + * 预载入关联查询 + * @access public + * @param array $resultSet 数据集 + * @param string $relation 当前关联名 + * @param string $subRelation 子关联名 + * @param \Closure $closure 闭包 + * @return void + */ + public function eagerlyResultSet(&$resultSet, $relation, $subRelation, $closure) + { + $morphType = $this->morphType; + $morphKey = $this->morphKey; + $type = $this->type; + $range = []; + foreach ($resultSet as $result) { + $pk = $result->getPk(); + // 获取关联外键列表 + if (isset($result->$pk)) { + $range[] = $result->$pk; + } + } + + if (!empty($range)) { + $data = $this->eagerlyMorphToMany([ + $morphKey => ['in', $range], + $morphType => $type, + ], $relation, $subRelation, $closure); + // 关联属性名 + $attr = Loader::parseName($relation); + // 关联数据封装 + foreach ($resultSet as $result) { + if (!isset($data[$result->$pk])) { + $data[$result->$pk] = []; + } + foreach ($data[$result->$pk] as &$relationModel) { + $relationModel->setParent(clone $result); + $relationModel->isUpdate(true); + } + $result->setRelation($attr, $this->resultSetBuild($data[$result->$pk])); + } + } + } + + /** + * 预载入关联查询 + * @access public + * @param Model $result 数据对象 + * @param string $relation 当前关联名 + * @param string $subRelation 子关联名 + * @param \Closure $closure 闭包 + * @return void + */ + public function eagerlyResult(&$result, $relation, $subRelation, $closure) + { + $pk = $result->getPk(); + if (isset($result->$pk)) { + $data = $this->eagerlyMorphToMany([ + $this->morphKey => $result->$pk, + $this->morphType => $this->type, + ], $relation, $subRelation, $closure); + + if (!isset($data[$result->$pk])) { + $data[$result->$pk] = []; + } + + foreach ($data[$result->$pk] as &$relationModel) { + $relationModel->setParent(clone $result); + $relationModel->isUpdate(true); + } + + $result->setRelation(Loader::parseName($relation), $this->resultSetBuild($data[$result->$pk])); + } + } + + /** + * 关联统计 + * @access public + * @param Model $result 数据对象 + * @param \Closure $closure 闭包 + * @return integer + */ + public function relationCount($result, $closure) + { + $pk = $result->getPk(); + $count = 0; + if (isset($result->$pk)) { + if ($closure) { + call_user_func_array($closure, [ & $this->query]); + } + $count = $this->query->where([$this->morphKey => $result->$pk, $this->morphType => $this->type])->count(); + } + return $count; + } + + /** + * 创建关联统计子查询 + * @access public + * @param \Closure $closure 闭包 + * @param string $name 统计数据别名 + * @return string + */ + public function getRelationCountQuery($closure, &$name = null) + { + if ($closure) { + $return = call_user_func_array($closure, [ & $this->query]); + if ($return && is_string($return)) { + $name = $return; + } + } + + return $this->query->where([ + $this->morphKey => [ + 'exp', + Db::raw('=' . $this->parent->getTable() . '.' . $this->parent->getPk()), + ], + $this->morphType => $this->type, + ])->fetchSql()->count(); + } + + /** + * 多态一对多 关联模型预查询 + * @access public + * @param array $where 关联预查询条件 + * @param string $relation 关联名 + * @param string $subRelation 子关联 + * @param bool|\Closure $closure 闭包 + * @return array + */ + protected function eagerlyMorphToMany($where, $relation, $subRelation = '', $closure = false) + { + // 预载入关联查询 支持嵌套预载入 + if ($closure) { + call_user_func_array($closure, [ & $this]); + } + $list = $this->query->where($where)->with($subRelation)->select(); + $morphKey = $this->morphKey; + // 组装模型数据 + $data = []; + foreach ($list as $set) { + $data[$set->$morphKey][] = $set; + } + return $data; + } + + /** + * 保存(新增)当前关联数据对象 + * @access public + * @param mixed $data 数据 可以使用数组 关联模型对象 和 关联对象的主键 + * @return Model|false + */ + public function save($data) + { + if ($data instanceof Model) { + $data = $data->getData(); + } + + // 保存关联表数据 + $pk = $this->parent->getPk(); + + $data[$this->morphKey] = $this->parent->$pk; + $data[$this->morphType] = $this->type; + + $model = new $this->model(); + + return $model->save() ? $model : false; + } + + /** + * 创建关联对象实例 + * @param array $data + * @return Model + */ + public function make($data = []) + { + if ($data instanceof Model) { + $data = $data->getData(); + } + + // 保存关联表数据 + $pk = $this->parent->getPk(); + + $data[$this->morphKey] = $this->parent->$pk; + $data[$this->morphType] = $this->type; + + return new $this->model($data); + } + + /** + * 批量保存当前关联数据对象 + * @access public + * @param array $dataSet 数据集 + * @return integer + */ + public function saveAll(array $dataSet) + { + $result = false; + foreach ($dataSet as $key => $data) { + $result = $this->save($data); + } + return $result; + } + + /** + * 执行基础查询(进执行一次) + * @access protected + * @return void + */ + protected function baseQuery() + { + if (empty($this->baseQuery) && $this->parent->getData()) { + $pk = $this->parent->getPk(); + $map[$this->morphKey] = $this->parent->$pk; + $map[$this->morphType] = $this->type; + $this->query->where($map); + $this->baseQuery = true; + } + } + +} diff --git a/library/think/model/relation/MorphOne.php b/library/think/model/relation/MorphOne.php new file mode 100644 index 0000000000000000000000000000000000000000..5ec717248390158cf83855f2e0c924c3673e3835 --- /dev/null +++ b/library/think/model/relation/MorphOne.php @@ -0,0 +1,263 @@ + +// +---------------------------------------------------------------------- + +namespace think\model\relation; + +use think\db\Query; +use think\Exception; +use think\Loader; +use think\Model; +use think\model\Relation; + +class MorphOne extends Relation +{ + // 多态字段 + protected $morphKey; + protected $morphType; + // 多态类型 + protected $type; + + /** + * 构造函数 + * @access public + * @param Model $parent 上级模型对象 + * @param string $model 模型名 + * @param string $morphKey 关联外键 + * @param string $morphType 多态字段名 + * @param string $type 多态类型 + */ + public function __construct(Model $parent, $model, $morphKey, $morphType, $type) + { + $this->parent = $parent; + $this->model = $model; + $this->type = $type; + $this->morphKey = $morphKey; + $this->morphType = $morphType; + $this->query = (new $model)->db(); + } + + /** + * 延迟获取关联数据 + * @param string $subRelation 子关联名 + * @param \Closure $closure 闭包查询条件 + * @return false|\PDOStatement|string|\think\Collection + */ + public function getRelation($subRelation = '', $closure = null) + { + if ($closure) { + call_user_func_array($closure, [ & $this->query]); + } + $relationModel = $this->relation($subRelation)->find(); + + if ($relationModel) { + $relationModel->setParent(clone $this->parent); + } + + return $relationModel; + } + + /** + * 根据关联条件查询当前模型 + * @access public + * @param string $operator 比较操作符 + * @param integer $count 个数 + * @param string $id 关联表的统计字段 + * @param string $joinType JOIN类型 + * @return Query + */ + public function has($operator = '>=', $count = 1, $id = '*', $joinType = 'INNER') + { + return $this->parent; + } + + /** + * 根据关联条件查询当前模型 + * @access public + * @param mixed $where 查询条件(数组或者闭包) + * @param mixed $fields 字段 + * @return Query + */ + public function hasWhere($where = [], $fields = null) + { + throw new Exception('relation not support: hasWhere'); + } + + /** + * 预载入关联查询 + * @access public + * @param array $resultSet 数据集 + * @param string $relation 当前关联名 + * @param string $subRelation 子关联名 + * @param \Closure $closure 闭包 + * @return void + */ + public function eagerlyResultSet(&$resultSet, $relation, $subRelation, $closure) + { + $morphType = $this->morphType; + $morphKey = $this->morphKey; + $type = $this->type; + $range = []; + foreach ($resultSet as $result) { + $pk = $result->getPk(); + // 获取关联外键列表 + if (isset($result->$pk)) { + $range[] = $result->$pk; + } + } + + if (!empty($range)) { + $data = $this->eagerlyMorphToOne([ + $morphKey => ['in', $range], + $morphType => $type, + ], $relation, $subRelation, $closure); + // 关联属性名 + $attr = Loader::parseName($relation); + // 关联数据封装 + foreach ($resultSet as $result) { + if (!isset($data[$result->$pk])) { + $relationModel = null; + } else { + $relationModel = $data[$result->$pk]; + $relationModel->setParent(clone $result); + $relationModel->isUpdate(true); + } + + $result->setRelation($attr, $relationModel); + } + } + } + + /** + * 预载入关联查询 + * @access public + * @param Model $result 数据对象 + * @param string $relation 当前关联名 + * @param string $subRelation 子关联名 + * @param \Closure $closure 闭包 + * @return void + */ + public function eagerlyResult(&$result, $relation, $subRelation, $closure) + { + $pk = $result->getPk(); + if (isset($result->$pk)) { + $pk = $result->$pk; + $data = $this->eagerlyMorphToOne([ + $this->morphKey => $pk, + $this->morphType => $this->type, + ], $relation, $subRelation, $closure); + + if (isset($data[$pk])) { + $relationModel = $data[$pk]; + $relationModel->setParent(clone $result); + $relationModel->isUpdate(true); + } else { + $relationModel = null; + } + + $result->setRelation(Loader::parseName($relation), $relationModel); + } + } + + /** + * 多态一对一 关联模型预查询 + * @access public + * @param array $where 关联预查询条件 + * @param string $relation 关联名 + * @param string $subRelation 子关联 + * @param bool|\Closure $closure 闭包 + * @return array + */ + protected function eagerlyMorphToOne($where, $relation, $subRelation = '', $closure = false) + { + // 预载入关联查询 支持嵌套预载入 + if ($closure) { + call_user_func_array($closure, [ & $this]); + } + $list = $this->query->where($where)->with($subRelation)->find(); + $morphKey = $this->morphKey; + // 组装模型数据 + $data = []; + foreach ($list as $set) { + $data[$set->$morphKey][] = $set; + } + return $data; + } + + /** + * 保存(新增)当前关联数据对象 + * @access public + * @param mixed $data 数据 可以使用数组 关联模型对象 和 关联对象的主键 + * @return Model|false + */ + public function save($data) + { + if ($data instanceof Model) { + $data = $data->getData(); + } + + // 保存关联表数据 + $pk = $this->parent->getPk(); + + $data[$this->morphKey] = $this->parent->$pk; + $data[$this->morphType] = $this->type; + + $model = new $this->model(); + + return $model->save() ? $model : false; + } + + /** + * 创建关联对象实例 + * @param array $data + * @return Model + */ + public function make($data = []) + { + if ($data instanceof Model) { + $data = $data->getData(); + } + // 保存关联表数据 + $pk = $this->parent->getPk(); + + $data[$this->morphKey] = $this->parent->$pk; + $data[$this->morphType] = $this->type; + + return new $this->model($data); + } + + /** + * 执行基础查询(进执行一次) + * @access protected + * @return void + */ + protected function baseQuery() + { + if (empty($this->baseQuery) && $this->parent->getData()) { + $pk = $this->parent->getPk(); + $map[$this->morphKey] = $this->parent->$pk; + $map[$this->morphType] = $this->type; + $this->query->where($map); + $this->baseQuery = true; + } + } + + /** + * 创建关联统计子查询 + * @access public + * @param \Closure $closure 闭包 + * @param string $name 统计数据别名 + * @return string + */ + public function getRelationCountQuery($closure, &$name = null) + { + throw new Exception('relation not support: withCount'); + } +} diff --git a/library/think/model/relation/MorphTo.php b/library/think/model/relation/MorphTo.php new file mode 100644 index 0000000000000000000000000000000000000000..7d4526517fe886e9cbf55e61144e93da659df40e --- /dev/null +++ b/library/think/model/relation/MorphTo.php @@ -0,0 +1,299 @@ + +// +---------------------------------------------------------------------- + +namespace think\model\relation; + +use think\Exception; +use think\Loader; +use think\Model; +use think\model\Relation; + +class MorphTo extends Relation +{ + // 多态字段 + protected $morphKey; + protected $morphType; + // 多态别名 + protected $alias; + protected $relation; + + /** + * 构造函数 + * @access public + * @param Model $parent 上级模型对象 + * @param string $morphType 多态字段名 + * @param string $morphKey 外键名 + * @param array $alias 多态别名定义 + * @param string $relation 关联名 + */ + public function __construct(Model $parent, $morphType, $morphKey, $alias = [], $relation = null) + { + $this->parent = $parent; + $this->morphType = $morphType; + $this->morphKey = $morphKey; + $this->alias = $alias; + $this->relation = $relation; + } + + /** + * 获取当前的关联模型类的实例 + * @access public + * @return Model + */ + public function getModel() + { + $morphType = $this->morphType; + $model = $this->parseModel($this->parent->$morphType); + return (new $model); + } + + /** + * 延迟获取关联数据 + * @param string $subRelation 子关联名 + * @param \Closure $closure 闭包查询条件 + * @return mixed + */ + public function getRelation($subRelation = '', $closure = null) + { + $morphKey = $this->morphKey; + $morphType = $this->morphType; + // 多态模型 + $model = $this->parseModel($this->parent->$morphType); + // 主键数据 + $pk = $this->parent->$morphKey; + $relationModel = (new $model)->relation($subRelation)->find($pk); + + if ($relationModel) { + $relationModel->setParent(clone $this->parent); + } + return $relationModel; + } + + /** + * 根据关联条件查询当前模型 + * @access public + * @param string $operator 比较操作符 + * @param integer $count 个数 + * @param string $id 关联表的统计字段 + * @param string $joinType JOIN类型 + * @return Query + */ + public function has($operator = '>=', $count = 1, $id = '*', $joinType = 'INNER') + { + return $this->parent; + } + + /** + * 根据关联条件查询当前模型 + * @access public + * @param mixed $where 查询条件(数组或者闭包) + * @param mixed $fields 字段 + * @return Query + */ + public function hasWhere($where = [], $fields = null) + { + throw new Exception('relation not support: hasWhere'); + } + + /** + * 解析模型的完整命名空间 + * @access protected + * @param string $model 模型名(或者完整类名) + * @return string + */ + protected function parseModel($model) + { + if (isset($this->alias[$model])) { + $model = $this->alias[$model]; + } + if (false === strpos($model, '\\')) { + $path = explode('\\', get_class($this->parent)); + array_pop($path); + array_push($path, Loader::parseName($model, 1)); + $model = implode('\\', $path); + } + return $model; + } + + /** + * 设置多态别名 + * @access public + * @param array $alias 别名定义 + * @return $this + */ + public function setAlias($alias) + { + $this->alias = $alias; + return $this; + } + + /** + * 移除关联查询参数 + * @access public + * @return $this + */ + public function removeOption() + { + return $this; + } + + /** + * 预载入关联查询 + * @access public + * @param array $resultSet 数据集 + * @param string $relation 当前关联名 + * @param string $subRelation 子关联名 + * @param \Closure $closure 闭包 + * @return void + * @throws Exception + */ + public function eagerlyResultSet(&$resultSet, $relation, $subRelation, $closure) + { + $morphKey = $this->morphKey; + $morphType = $this->morphType; + $range = []; + foreach ($resultSet as $result) { + // 获取关联外键列表 + if (!empty($result->$morphKey)) { + $range[$result->$morphType][] = $result->$morphKey; + } + } + + if (!empty($range)) { + // 关联属性名 + $attr = Loader::parseName($relation); + foreach ($range as $key => $val) { + // 多态类型映射 + $model = $this->parseModel($key); + $obj = new $model; + $pk = $obj->getPk(); + $list = $obj->all($val, $subRelation); + $data = []; + foreach ($list as $k => $vo) { + $data[$vo->$pk] = $vo; + } + foreach ($resultSet as $result) { + if ($key == $result->$morphType) { + // 关联模型 + if (!isset($data[$result->$morphKey])) { + throw new Exception('relation data not exists :' . $this->model); + } else { + $relationModel = $data[$result->$morphKey]; + $relationModel->setParent(clone $result); + $relationModel->isUpdate(true); + + $result->setRelation($attr, $relationModel); + } + } + } + } + } + } + + /** + * 预载入关联查询 + * @access public + * @param Model $result 数据对象 + * @param string $relation 当前关联名 + * @param string $subRelation 子关联名 + * @param \Closure $closure 闭包 + * @return void + */ + public function eagerlyResult(&$result, $relation, $subRelation, $closure) + { + $morphKey = $this->morphKey; + $morphType = $this->morphType; + // 多态类型映射 + $model = $this->parseModel($result->{$this->morphType}); + $this->eagerlyMorphToOne($model, $relation, $result, $subRelation); + } + + /** + * 关联统计 + * @access public + * @param Model $result 数据对象 + * @param \Closure $closure 闭包 + * @return integer + */ + public function relationCount($result, $closure) + { + } + + /** + * 多态MorphTo 关联模型预查询 + * @access public + * @param object $model 关联模型对象 + * @param string $relation 关联名 + * @param $result + * @param string $subRelation 子关联 + * @return void + */ + protected function eagerlyMorphToOne($model, $relation, &$result, $subRelation = '') + { + // 预载入关联查询 支持嵌套预载入 + $pk = $this->parent->{$this->morphKey}; + $data = (new $model)->with($subRelation)->find($pk); + if ($data) { + $data->setParent(clone $result); + $data->isUpdate(true); + } + $result->setRelation(Loader::parseName($relation), $data ?: null); + } + + /** + * 添加关联数据 + * @access public + * @param Model $model 关联模型对象 + * @param string $type 多态类型 + * @return Model + */ + public function associate($model, $type = '') + { + $morphKey = $this->morphKey; + $morphType = $this->morphType; + $pk = $model->getPk(); + + $this->parent->setAttr($morphKey, $model->$pk); + $this->parent->setAttr($morphType, $type ?: get_class($model)); + $this->parent->save(); + + return $this->parent->setRelation($this->relation, $model); + } + + /** + * 注销关联数据 + * @access public + * @return Model + */ + public function dissociate() + { + $morphKey = $this->morphKey; + $morphType = $this->morphType; + + $this->parent->setAttr($morphKey, null); + $this->parent->setAttr($morphType, null); + $this->parent->save(); + + return $this->parent->setRelation($this->relation, null); + } + + /** + * 创建关联统计子查询 + * @access public + * @param \Closure $closure 闭包 + * @param string $name 统计数据别名 + * @return string + */ + public function getRelationCountQuery($closure, &$name = null) + { + throw new Exception('relation not support: withCount'); + } +} diff --git a/library/think/model/relation/OneToOne.php b/library/think/model/relation/OneToOne.php new file mode 100644 index 0000000000000000000000000000000000000000..353ce21b4fef13e357f6b6df53ac130ce9623216 --- /dev/null +++ b/library/think/model/relation/OneToOne.php @@ -0,0 +1,337 @@ + +// +---------------------------------------------------------------------- + +namespace think\model\relation; + +use think\db\Query; +use think\Exception; +use think\Loader; +use think\Model; +use think\model\Relation; + +/** + * Class OneToOne + * @package think\model\relation + * + */ +abstract class OneToOne extends Relation +{ + // 预载入方式 0 -JOIN 1 -IN + protected $eagerlyType = 1; + // 当前关联的JOIN类型 + protected $joinType; + // 要绑定的属性 + protected $bindAttr = []; + // 关联方法名 + protected $relation; + + /** + * 设置join类型 + * @access public + * @param string $type JOIN类型 + * @return $this + */ + public function joinType($type) + { + $this->joinType = $type; + return $this; + } + + /** + * 预载入关联查询(JOIN方式) + * @access public + * @param Query $query 查询对象 + * @param string $relation 关联名 + * @param string $subRelation 子关联 + * @param \Closure $closure 闭包条件 + * @param bool $first + * @return void + */ + public function eagerly(Query $query, $relation, $subRelation, $closure, $first) + { + $name = Loader::parseName(basename(str_replace('\\', '/', get_class($query->getModel())))); + + if ($first) { + $table = $query->getTable(); + $query->table([$table => $name]); + if ($query->getOptions('field')) { + $field = $query->getOptions('field'); + $query->removeOption('field'); + } else { + $field = true; + } + $query->field($field, false, $table, $name); + $field = null; + } + + // 预载入封装 + $joinTable = $this->query->getTable(); + $joinAlias = $relation; + $query->via($joinAlias); + + if ($this instanceof BelongsTo) { + $query->join([$joinTable => $joinAlias], $name . '.' . $this->foreignKey . '=' . $joinAlias . '.' . $this->localKey, $this->joinType); + } else { + $query->join([$joinTable => $joinAlias], $name . '.' . $this->localKey . '=' . $joinAlias . '.' . $this->foreignKey, $this->joinType); + } + + if ($closure) { + // 执行闭包查询 + call_user_func_array($closure, [ & $query]); + // 使用withField指定获取关联的字段,如 + // $query->where(['id'=>1])->withField('id,name'); + if ($query->getOptions('with_field')) { + $field = $query->getOptions('with_field'); + $query->removeOption('with_field'); + } + } elseif (isset($this->option['field'])) { + $field = $this->option['field']; + } + $query->field(isset($field) ? $field : true, false, $joinTable, $joinAlias, $relation . '__'); + } + + /** + * 预载入关联查询(数据集) + * @param array $resultSet + * @param string $relation + * @param string $subRelation + * @param \Closure $closure + * @return mixed + */ + abstract protected function eagerlySet(&$resultSet, $relation, $subRelation, $closure); + + /** + * 预载入关联查询(数据) + * @param Model $result + * @param string $relation + * @param string $subRelation + * @param \Closure $closure + * @return mixed + */ + abstract protected function eagerlyOne(&$result, $relation, $subRelation, $closure); + + /** + * 预载入关联查询(数据集) + * @access public + * @param array $resultSet 数据集 + * @param string $relation 当前关联名 + * @param string $subRelation 子关联名 + * @param \Closure $closure 闭包 + * @return void + */ + public function eagerlyResultSet(&$resultSet, $relation, $subRelation, $closure) + { + if (1 == $this->eagerlyType) { + // IN查询 + $this->eagerlySet($resultSet, $relation, $subRelation, $closure); + } else { + // 模型关联组装 + foreach ($resultSet as $result) { + $this->match($this->model, $relation, $result); + } + } + } + + /** + * 预载入关联查询(数据) + * @access public + * @param Model $result 数据对象 + * @param string $relation 当前关联名 + * @param string $subRelation 子关联名 + * @param \Closure $closure 闭包 + * @return void + */ + public function eagerlyResult(&$result, $relation, $subRelation, $closure) + { + if (1 == $this->eagerlyType) { + // IN查询 + $this->eagerlyOne($result, $relation, $subRelation, $closure); + } else { + // 模型关联组装 + $this->match($this->model, $relation, $result); + } + } + + /** + * 保存(新增)当前关联数据对象 + * @access public + * @param mixed $data 数据 可以使用数组 关联模型对象 和 关联对象的主键 + * @return Model|false + */ + public function save($data) + { + if ($data instanceof Model) { + $data = $data->getData(); + } + $model = new $this->model; + // 保存关联表数据 + $data[$this->foreignKey] = $this->parent->{$this->localKey}; + return $model->save($data) ? $model : false; + } + + /** + * 设置预载入方式 + * @access public + * @param integer $type 预载入方式 0 JOIN查询 1 IN查询 + * @return $this + */ + public function setEagerlyType($type) + { + $this->eagerlyType = $type; + return $this; + } + + /** + * 获取预载入方式 + * @access public + * @return integer + */ + public function getEagerlyType() + { + return $this->eagerlyType; + } + + /** + * 绑定关联表的属性到父模型属性 + * @access public + * @param mixed $attr 要绑定的属性列表 + * @return $this + */ + public function bind($attr) + { + if (is_string($attr)) { + $attr = explode(',', $attr); + } + $this->bindAttr = $attr; + return $this; + } + + /** + * 获取绑定属性 + * @access public + * @return array + */ + public function getBindAttr() + { + return $this->bindAttr; + } + + /** + * 关联统计 + * @access public + * @param Model $result 数据对象 + * @param \Closure $closure 闭包 + * @return integer + */ + public function relationCount($result, $closure) + { + } + + /** + * 一对一 关联模型预查询拼装 + * @access public + * @param string $model 模型名称 + * @param string $relation 关联名 + * @param Model $result 模型对象实例 + * @return void + */ + protected function match($model, $relation, &$result) + { + // 重新组装模型数据 + foreach ($result->getData() as $key => $val) { + if (strpos($key, '__')) { + list($name, $attr) = explode('__', $key, 2); + if ($name == $relation) { + $list[$name][$attr] = $val; + unset($result->$key); + } + } + } + + if (isset($list[$relation])) { + $relationModel = new $model($list[$relation]); + $relationModel->setParent(clone $result); + $relationModel->isUpdate(true); + + if (!empty($this->bindAttr)) { + $this->bindAttr($relationModel, $result, $this->bindAttr); + } + } else { + $relationModel = null; + } + $result->setRelation(Loader::parseName($relation), $relationModel); + } + + /** + * 绑定关联属性到父模型 + * @access protected + * @param Model $model 关联模型对象 + * @param Model $result 父模型对象 + * @param array $bindAttr 绑定属性 + * @return void + * @throws Exception + */ + protected function bindAttr($model, &$result, $bindAttr) + { + foreach ($bindAttr as $key => $attr) { + $key = is_numeric($key) ? $attr : $key; + if (isset($result->$key)) { + throw new Exception('bind attr has exists:' . $key); + } else { + $result->setAttr($key, $model ? $model->$attr : null); + } + } + } + + /** + * 一对一 关联模型预查询(IN方式) + * @access public + * @param object $model 关联模型对象 + * @param array $where 关联预查询条件 + * @param string $key 关联键名 + * @param string $relation 关联名 + * @param string $subRelation 子关联 + * @param bool|\Closure $closure + * @return array + */ + protected function eagerlyWhere($model, $where, $key, $relation, $subRelation = '', $closure = false) + { + $this->baseQuery = true; + + // 预载入关联查询 支持嵌套预载入 + if ($closure) { + call_user_func_array($closure, [ & $model]); + if ($field = $model->getOptions('with_field')) { + $model->field($field)->removeOption('with_field'); + } + } + $list = $model->where($where)->with($subRelation)->select(); + + // 组装模型数据 + $data = []; + foreach ($list as $set) { + $data[$set->$key] = $set; + } + return $data; + } + + /** + * 创建关联统计子查询 + * @access public + * @param \Closure $closure 闭包 + * @param string $name 统计数据别名 + * @return string + */ + public function getRelationCountQuery($closure, &$name = null) + { + throw new Exception('relation not support: withCount'); + } +} diff --git a/library/think/paginator/Collection.php b/library/think/paginator/Collection.php deleted file mode 100644 index 82e88272875dea5bfd3435b1e9f81067b7a71a3c..0000000000000000000000000000000000000000 --- a/library/think/paginator/Collection.php +++ /dev/null @@ -1,74 +0,0 @@ - -// +---------------------------------------------------------------------- - -namespace think\paginator; - -use Exception; -use think\Paginator; - -/** - * Class Collection - * @package think\paginator - * @method integer total() - * @method integer listRows() - * @method integer currentPage() - * @method string render() - * @method Paginator fragment($fragment) - * @method Paginator appends($key, $value) - * @method integer lastPage() - * @method boolean hasPages() - */ -class Collection extends \think\Collection -{ - - /** @var Paginator */ - protected $paginator; - - public function __construct($items = [], Paginator $paginator = null) - { - $this->paginator = $paginator; - parent::__construct($items); - } - - public static function make($items = [], Paginator $paginator = null) - { - return new static($items, $paginator); - } - - public function toArray() - { - 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() - ]; - } else { - return parent::toArray(); - } - } - - public function __call($method, $args) - { - if ($this->paginator && method_exists($this->paginator, $method)) { - return call_user_func_array([$this->paginator, $method], $args); - } else { - 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 87e1fe489e76dcb24beb5af05574aee0a4ac27b1..c5ac60db80278dcd406291bca9ab2e00ef82e053 100644 --- a/library/think/paginator/driver/Bootstrap.php +++ b/library/think/paginator/driver/Bootstrap.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2016 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- @@ -102,7 +102,6 @@ class Bootstrap extends Paginator return $html; } - /** * 渲染分页html * @return mixed @@ -127,7 +126,6 @@ class Bootstrap extends Paginator } } - /** * 生成一个可点击的按钮 * @@ -204,4 +202,4 @@ class Bootstrap extends Paginator return $this->getAvailablePageWrapper($url, $page); } -} \ No newline at end of file +} diff --git a/library/think/process/Builder.php b/library/think/process/Builder.php index 826e6745f1be7a1172de30adfac603ba5a0f8abb..da5616397258db3d3862f51016600ce084cdb5e2 100644 --- a/library/think/process/Builder.php +++ b/library/think/process/Builder.php @@ -15,10 +15,9 @@ use think\Process; class Builder { - private $arguments; private $cwd; - private $env = null; + private $env = null; private $input; private $timeout = 60; private $options = []; @@ -155,7 +154,7 @@ class Builder return $this; } - $timeout = (float)$timeout; + $timeout = (float) $timeout; if ($timeout < 0) { throw new \InvalidArgumentException('The timeout value must be a valid positive integer or float number.'); @@ -231,4 +230,4 @@ class Builder return $process; } -} \ No newline at end of file +} diff --git a/library/think/process/Utils.php b/library/think/process/Utils.php index 3ed136a5f4a8943bfcb0495b0dfd2ef622ef0c76..f94c6488e436306a28e1235b6e5003195fd7d7d9 100644 --- a/library/think/process/Utils.php +++ b/library/think/process/Utils.php @@ -60,7 +60,7 @@ class Utils return $input; } if (is_scalar($input)) { - return (string)$input; + return (string) $input; } throw new \InvalidArgumentException(sprintf('%s only accepts strings or stream resources.', $caller)); } @@ -72,4 +72,4 @@ class Utils return 2 < strlen($arg) && $char === $arg[0] && $char === $arg[strlen($arg) - 1]; } -} \ No newline at end of file +} diff --git a/library/think/process/exception/Faild.php b/library/think/process/exception/Failed.php similarity index 99% rename from library/think/process/exception/Faild.php rename to library/think/process/exception/Failed.php index 23df369dee184b301db078d848f1d341f8aec3bb..5295082325cf1514440e217130e44aa77ef6b980 100644 --- a/library/think/process/exception/Faild.php +++ b/library/think/process/exception/Failed.php @@ -11,7 +11,6 @@ namespace think\process\exception; - use think\Process; class Failed extends \RuntimeException diff --git a/library/think/process/exception/Timeout.php b/library/think/process/exception/Timeout.php index c085ee8e52724b36318d83612b10c220143d673e..d5f1162f4392ce382d2f4e3f512d6e31db3625a5 100644 --- a/library/think/process/exception/Timeout.php +++ b/library/think/process/exception/Timeout.php @@ -58,4 +58,4 @@ class Timeout extends \RuntimeException throw new \LogicException(sprintf('Unknown timeout type "%d".', $this->timeoutType)); } } -} \ No newline at end of file +} diff --git a/library/think/process/pipes/Pipes.php b/library/think/process/pipes/Pipes.php index 51da44f31e99805aab4b399d2c82c407b20a9753..82396b8ff8b2318bce56e9f55369780dc053e347 100644 --- a/library/think/process/pipes/Pipes.php +++ b/library/think/process/pipes/Pipes.php @@ -53,7 +53,6 @@ abstract class Pipes */ abstract public function areOpen(); - /** * {@inheritdoc} */ @@ -91,4 +90,4 @@ abstract class Pipes $this->blocked = false; } -} \ No newline at end of file +} diff --git a/library/think/process/pipes/Unix.php b/library/think/process/pipes/Unix.php index abfe61b2be4b64336a8a1fecd87469833bdcea5b..fd99a5d67eba451ea46caef249d613e396fbef66 100644 --- a/library/think/process/pipes/Unix.php +++ b/library/think/process/pipes/Unix.php @@ -25,14 +25,14 @@ class Unix extends Pipes public function __construct($ttyMode, $ptyMode, $input, $disableOutput) { - $this->ttyMode = (bool)$ttyMode; - $this->ptyMode = (bool)$ptyMode; - $this->disableOutput = (bool)$disableOutput; + $this->ttyMode = (bool) $ttyMode; + $this->ptyMode = (bool) $ptyMode; + $this->disableOutput = (bool) $disableOutput; if (is_resource($input)) { $this->input = $input; } else { - $this->inputBuffer = (string)$input; + $this->inputBuffer = (string) $input; } } @@ -134,12 +134,12 @@ class Unix extends Pipes $type = (false !== $found = array_search($pipe, $this->pipes)) ? $found : 'input'; $data = ''; - while ('' !== $dataread = (string)fread($pipe, self::CHUNK_SIZE)) { + while ('' !== $dataread = (string) fread($pipe, self::CHUNK_SIZE)) { $data .= $dataread; } if ('' !== $data) { - if ($type === 'input') { + if ('input' === $type) { $this->inputBuffer .= $data; } else { $read[$type] = $data; @@ -147,7 +147,7 @@ class Unix extends Pipes } if (false === $data || (true === $close && feof($pipe) && '' === $data)) { - if ($type === 'input') { + if ('input' === $type) { $this->input = null; } else { fclose($this->pipes[$type]); @@ -160,7 +160,7 @@ class Unix extends Pipes while (strlen($this->inputBuffer)) { $written = fwrite($w[0], $this->inputBuffer, 2 << 18); // write 512k if ($written > 0) { - $this->inputBuffer = (string)substr($this->inputBuffer, $written); + $this->inputBuffer = (string) substr($this->inputBuffer, $written); } else { break; } @@ -180,7 +180,7 @@ class Unix extends Pipes */ public function areOpen() { - return (bool)$this->pipes; + return (bool) $this->pipes; } /** @@ -193,4 +193,4 @@ class Unix extends Pipes { return new static($process->isTty(), $process->isPty(), $input, $process->isOutputDisabled()); } -} \ No newline at end of file +} diff --git a/library/think/process/pipes/Windows.php b/library/think/process/pipes/Windows.php index 2cc44ca70473b20e5700d1be5335ab6f45a3bbd6..bba7e9b869966bbe917d195acdba035b7bd9aef7 100644 --- a/library/think/process/pipes/Windows.php +++ b/library/think/process/pipes/Windows.php @@ -30,7 +30,7 @@ class Windows extends Pipes public function __construct($disableOutput, $input) { - $this->disableOutput = (bool)$disableOutput; + $this->disableOutput = (bool) $disableOutput; if (!$this->disableOutput) { @@ -128,7 +128,7 @@ class Windows extends Pipes */ public function areOpen() { - return (bool)$this->pipes && (bool)$this->fileHandles; + return (bool) $this->pipes && (bool) $this->fileHandles; } /** @@ -213,7 +213,7 @@ class Windows extends Pipes while (strlen($this->inputBuffer)) { $written = fwrite($w[0], $this->inputBuffer, 2 << 18); if ($written > 0) { - $this->inputBuffer = (string)substr($this->inputBuffer, $written); + $this->inputBuffer = (string) substr($this->inputBuffer, $written); } else { break; } @@ -225,4 +225,4 @@ class Windows extends Pipes unset($this->pipes[0]); } } -} \ No newline at end of file +} diff --git a/library/think/response/Json.php b/library/think/response/Json.php index a137f45397f2c13fc69c2c296b163497e40a7c8f..c906bfc180ed06200047e8b8967efc678898b1de 100644 --- a/library/think/response/Json.php +++ b/library/think/response/Json.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2016 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- @@ -27,17 +27,25 @@ class Json extends Response * @access protected * @param mixed $data 要处理的数据 * @return mixed + * @throws \Exception */ protected function output($data) { - // 返回JSON数据格式到客户端 包含状态信息 - $data = json_encode($data, $this->options['json_encode_param']); - - if ($data === false) { - throw new \InvalidArgumentException(json_last_error_msg()); + try { + // 返回JSON数据格式到客户端 包含状态信息 + $data = json_encode($data, $this->options['json_encode_param']); + + if ($data === false) { + throw new \InvalidArgumentException(json_last_error_msg()); + } + + return $data; + } catch (\Exception $e) { + if ($e->getPrevious()) { + throw $e->getPrevious(); + } + throw $e; } - - return $data; } } diff --git a/library/think/response/Jsonp.php b/library/think/response/Jsonp.php index fda1183a8f9e82d77babbddc1b2961a7c47a28a7..404bacbe6052141fb11b9f37594d7977bd407ac6 100644 --- a/library/think/response/Jsonp.php +++ b/library/think/response/Jsonp.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2016 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- @@ -30,21 +30,29 @@ class Jsonp extends Response * @access protected * @param mixed $data 要处理的数据 * @return mixed + * @throws \Exception */ 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()); + try { + // 返回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; + } catch (\Exception $e) { + if ($e->getPrevious()) { + throw $e->getPrevious(); + } + throw $e; } - - $data = $handler . '(' . $data . ');'; - return $data; } } diff --git a/library/think/response/Redirect.php b/library/think/response/Redirect.php index 82a5fe3274fb7cb9f3e8b6d49ecba41460873ebc..91694cb2cbb81c6af653a2b442bee24bc2cbf587 100644 --- a/library/think/response/Redirect.php +++ b/library/think/response/Redirect.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2016 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- @@ -53,10 +53,10 @@ class Redirect extends Response { if (is_array($name)) { foreach ($name as $key => $val) { - Session::set($key, $val); + Session::flash($key, $val); } } else { - Session::set($name, $value); + Session::flash($name, $value); } return $this; } @@ -67,7 +67,11 @@ class Redirect extends Response */ public function getTargetUrl() { - return (strpos($this->data, '://') || 0 === strpos($this->data, '/')) ? $this->data : Url::build($this->data, $this->params); + if (strpos($this->data, '://') || (0 === strpos($this->data, '/') && empty($this->params))) { + return $this->data; + } else { + return Url::build($this->data, $this->params); + } } public function params($params = []) @@ -78,14 +82,17 @@ class Redirect extends Response /** * 记住当前url后跳转 + * @return $this */ public function remember() { Session::set('redirect_url', Request::instance()->url()); + return $this; } /** * 跳转到上次记住的url + * @return $this */ public function restore() { @@ -93,5 +100,6 @@ class Redirect extends Response $this->data = Session::get('redirect_url'); Session::delete('redirect_url'); } + return $this; } } diff --git a/library/think/response/View.php b/library/think/response/View.php index 3a5f3660c1791533d1c47183461e9d30a3c2718b..48f944a7117dc6db763fed9abb9aeac6e1d7a295 100644 --- a/library/think/response/View.php +++ b/library/think/response/View.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2016 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- diff --git a/library/think/response/Xml.php b/library/think/response/Xml.php index 3a2c59bdc6c67076890728a4966856257f9edbed..3bdc052a2036310fbcf420150a38dd83add6c0a7 100644 --- a/library/think/response/Xml.php +++ b/library/think/response/Xml.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2016 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- @@ -11,6 +11,8 @@ namespace think\response; +use think\Collection; +use think\Model; use think\Response; class Xml extends Response @@ -81,6 +83,11 @@ class Xml extends Response protected function dataToXml($data, $item, $id) { $xml = $attr = ''; + + if ($data instanceof Collection || $data instanceof Model) { + $data = $data->toArray(); + } + foreach ($data as $key => $val) { if (is_numeric($key)) { $id && $attr = " {$id}=\"{$key}\""; diff --git a/library/think/session/driver/Memcache.php b/library/think/session/driver/Memcache.php index 2c040271bcad5bb5b86faa3fa2588f1326076be7..877d7bd770e1466b428ffbf517d6241cd9bdb3b0 100644 --- a/library/think/session/driver/Memcache.php +++ b/library/think/session/driver/Memcache.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2016 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- @@ -79,7 +79,7 @@ class Memcache extends SessionHandler */ public function read($sessID) { - return $this->handler->get($this->config['session_name'] . $sessID); + return (string) $this->handler->get($this->config['session_name'] . $sessID); } /** @@ -87,6 +87,7 @@ class Memcache extends SessionHandler * @access public * @param string $sessID * @param String $sessData + * @return bool */ public function write($sessID, $sessData) { @@ -97,6 +98,7 @@ class Memcache extends SessionHandler * 删除Session * @access public * @param string $sessID + * @return bool */ public function destroy($sessID) { @@ -107,6 +109,7 @@ class Memcache extends SessionHandler * Session 垃圾回收 * @access public * @param string $sessMaxLifeTime + * @return true */ public function gc($sessMaxLifeTime) { diff --git a/library/think/session/driver/Memcached.php b/library/think/session/driver/Memcached.php index 4187204a5ed6b7401ef8e8bf00224ec40816f26e..2994a07c0c320cdf1521241876c7bde06c619c75 100644 --- a/library/think/session/driver/Memcached.php +++ b/library/think/session/driver/Memcached.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2016 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- @@ -87,7 +87,7 @@ class Memcached extends SessionHandler */ public function read($sessID) { - return $this->handler->get($this->config['session_name'] . $sessID); + return (string) $this->handler->get($this->config['session_name'] . $sessID); } /** @@ -95,6 +95,7 @@ class Memcached extends SessionHandler * @access public * @param string $sessID * @param String $sessData + * @return bool */ public function write($sessID, $sessData) { @@ -105,6 +106,7 @@ class Memcached extends SessionHandler * 删除Session * @access public * @param string $sessID + * @return bool */ public function destroy($sessID) { @@ -115,6 +117,7 @@ class Memcached extends SessionHandler * Session 垃圾回收 * @access public * @param string $sessMaxLifeTime + * @return true */ public function gc($sessMaxLifeTime) { diff --git a/library/think/session/driver/Redis.php b/library/think/session/driver/Redis.php index e3dc998312fd972a6a3b3242f71119b3d109efab..8d05126bfb9fb2c800d3f5f0c5f854424d321add 100644 --- a/library/think/session/driver/Redis.php +++ b/library/think/session/driver/Redis.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2016 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- @@ -81,11 +81,11 @@ class Redis extends SessionHandler * 读取Session * @access public * @param string $sessID - * @return bool|string + * @return string */ public function read($sessID) { - return $this->handler->get($this->config['session_name'] . $sessID); + return (string) $this->handler->get($this->config['session_name'] . $sessID); } /** @@ -108,11 +108,11 @@ class Redis extends SessionHandler * 删除Session * @access public * @param string $sessID - * @return bool|void + * @return bool */ public function destroy($sessID) { - $this->handler->delete($this->config['session_name'] . $sessID); + return $this->handler->delete($this->config['session_name'] . $sessID) > 0; } /** diff --git a/library/think/template/TagLib.php b/library/think/template/TagLib.php index 101593bfa02270417177bbeb731b7f7c5e862da4..c5b72f916887301cd2753daf359e87f587baf798 100644 --- a/library/think/template/TagLib.php +++ b/library/think/template/TagLib.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2016 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- @@ -68,7 +68,7 @@ class TagLib protected $comparison = [' nheq ' => ' !== ', ' heq ' => ' === ', ' neq ' => ' != ', ' eq ' => ' == ', ' egt ' => ' >= ', ' gt ' => ' > ', ' elt ' => ' <= ', ' lt ' => ' < ']; /** - * 架构函数 + * 构造函数 * @access public * @param \stdClass $template 模板引擎对象 */ @@ -190,7 +190,7 @@ class TagLib * @param boolean $close 是否为闭合标签 * @return string */ - private function getRegex($tags, $close) + public function getRegex($tags, $close) { $begin = $this->tpl->config('taglib_begin'); $end = $this->tpl->config('taglib_end'); @@ -264,8 +264,8 @@ class TagLib if (!empty($this->tags[$name]['expression'])) { static $_taglibs; if (!isset($_taglibs[$name])) { - $_taglibs[$name][0] = strlen(ltrim($this->tpl->config('taglib_begin'), '\\') . $name); - $_taglibs[$name][1] = strlen(ltrim($this->tpl->config('taglib_end'), '\\')); + $_taglibs[$name][0] = strlen($this->tpl->config('taglib_begin_origin') . $name); + $_taglibs[$name][1] = strlen($this->tpl->config('taglib_end_origin')); } $result['expression'] = substr($str, $_taglibs[$name][0], -$_taglibs[$name][1]); // 清除自闭合标签尾部/ diff --git a/library/think/template/driver/File.php b/library/think/template/driver/File.php index 1cd041afd33befffb575320f5ff1bc150057cb1c..a9a86bf2a3a34c428e617044c5c69e5fd39e6a29 100644 --- a/library/think/template/driver/File.php +++ b/library/think/template/driver/File.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2016 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- @@ -15,6 +15,8 @@ use think\Exception; class File { + protected $cacheFile; + /** * 写入编译缓存 * @param string $cacheFile 缓存的文件名 @@ -42,12 +44,13 @@ class File */ public function read($cacheFile, $vars = []) { + $this->cacheFile = $cacheFile; if (!empty($vars) && is_array($vars)) { // 模板阵列变量分解成为独立变量 extract($vars, EXTR_OVERWRITE); } //载入模版缓存文件 - include $cacheFile; + include $this->cacheFile; } /** diff --git a/library/think/template/taglib/Cx.php b/library/think/template/taglib/Cx.php index b15c7bba0e05b6e74dda3275f318c407b4027e65..31e0698dacf8ebaa52248c1a26812d3e832e317a 100644 --- a/library/think/template/taglib/Cx.php +++ b/library/think/template/taglib/Cx.php @@ -98,7 +98,7 @@ class Cx extends Taglib $name = $this->autoBuildVar($name); } - $parseStr .= 'if(is_array(' . $name . ') || ' . $name . ' instanceof \think\Collection): $' . $key . ' = 0;'; + $parseStr .= 'if(is_array(' . $name . ') || ' . $name . ' instanceof \think\Collection || ' . $name . ' instanceof \think\Paginator): $' . $key . ' = 0;'; // 设置了输出数组长度 if (0 != $offset || 'null' != $length) { $parseStr .= '$__LIST__ = is_array(' . $name . ') ? array_slice(' . $name . ',' . $offset . ',' . $length . ', true) : ' . $name . '->slice(' . $offset . ',' . $length . ', true); '; @@ -158,7 +158,7 @@ class Cx extends Taglib } else { $name = $this->autoBuildVar($name); } - $parseStr .= 'if(is_array(' . $name . ') || ' . $name . ' instanceof \think\Collection): '; + $parseStr .= 'if(is_array(' . $name . ') || ' . $name . ' instanceof \think\Collection || ' . $name . ' instanceof \think\Paginator): '; // 设置了输出数组长度 if (0 != $offset || 'null' != $length) { if (!isset($var)) { @@ -278,7 +278,7 @@ class Cx extends Taglib */ public function tagCase($tag, $content) { - $value = !empty($tag['expression']) ? $tag['expression'] : $tag['value']; + $value = isset($tag['expression']) ? $tag['expression'] : $tag['value']; $flag = substr($value, 0, 1); if ('$' == $flag || ':' == $flag) { $value = $this->autoBuildVar($value); @@ -431,7 +431,7 @@ class Cx extends Taglib { $name = $tag['name']; $name = $this->autoBuildVar($name); - $parseStr = 'isEmpty())): ?>' . $content . ''; + $parseStr = 'isEmpty())): ?>' . $content . ''; return $parseStr; } @@ -448,7 +448,7 @@ class Cx extends Taglib { $name = $tag['name']; $name = $this->autoBuildVar($name); - $parseStr = 'isEmpty()))): ?>' . $content . ''; + $parseStr = 'isEmpty()))): ?>' . $content . ''; return $parseStr; } @@ -620,8 +620,8 @@ class Cx extends Taglib return $parseStr; } - /** - * U函数的tag标签 + /** + * url函数的tag标签 * 格式:{url link="模块/控制器/方法" vars="参数" suffix="true或者false 是否带有后缀" domain="true或者false 是否携带域名" /} * @access public * @param array $tag 标签属性 diff --git a/library/think/view/driver/Php.php b/library/think/view/driver/Php.php index 31fe78076c27357cfd94e1dba68c78e47e0e97dd..f594a4384bd4451f86ed656a21e5bc73601f0555 100644 --- a/library/think/view/driver/Php.php +++ b/library/think/view/driver/Php.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2016 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- @@ -21,13 +21,19 @@ class Php { // 模板引擎参数 protected $config = [ + // 视图基础目录(集中式) + 'view_base' => '', // 模板起始路径 'view_path' => '', // 模板文件后缀 'view_suffix' => 'php', // 模板文件名分隔符 'view_depr' => DS, + // 默认模板渲染规则 1 解析为小写+下划线 2 全部转换小写 + 'auto_rule' => 1, ]; + protected $template; + protected $content; public function __construct($config = []) { @@ -66,16 +72,12 @@ class Php if (!is_file($template)) { throw new TemplateNotFoundException('template not exists:' . $template, $template); } + $this->template = $template; // 记录视图信息 App::$debug && Log::record('[ VIEW ] ' . $template . ' [ ' . var_export(array_keys($data), true) . ' ]', 'info'); - if (isset($data['template'])) { - $__template__ = $template; - extract($data, EXTR_OVERWRITE); - include $__template__; - } else { - extract($data, EXTR_OVERWRITE); - include $template; - } + + extract($data, EXTR_OVERWRITE); + include $this->template; } /** @@ -87,14 +89,10 @@ class Php */ public function display($content, $data = []) { - if (isset($data['content'])) { - $__content__ = $content; - extract($data, EXTR_OVERWRITE); - eval('?>' . $__content__); - } else { - extract($data, EXTR_OVERWRITE); - eval('?>' . $content); - } + $this->content = $content; + + extract($data, EXTR_OVERWRITE); + eval('?>' . $this->content); } /** @@ -109,25 +107,34 @@ class Php $this->config['view_path'] = App::$modulePath . 'view' . DS; } + $request = Request::instance(); + // 获取视图根目录 if (strpos($template, '@')) { + // 跨模块调用 list($module, $template) = explode('@', $template); - $path = APP_PATH . $module . DS . 'view' . DS; + } + if ($this->config['view_base']) { + // 基础视图目录 + $module = isset($module) ? $module : $request->module(); + $path = $this->config['view_base'] . ($module ? $module . DS : ''); } else { - $path = $this->config['view_path']; + $path = isset($module) ? APP_PATH . $module . DS . 'view' . DS : $this->config['view_path']; } - // 分析模板文件规则 - $request = Request::instance(); - $controller = Loader::parseName($request->controller()); - if ($controller && 0 !== strpos($template, '/')) { - $depr = $this->config['view_depr']; - $template = str_replace(['/', ':'], $depr, $template); - if ('' == $template) { - // 如果模板文件名为空 按照默认规则定位 - $template = str_replace('.', DS, $controller) . $depr . $request->action(); - } elseif (false === strpos($template, $depr)) { - $template = str_replace('.', DS, $controller) . $depr . $template; + $depr = $this->config['view_depr']; + if (0 !== strpos($template, '/')) { + $template = str_replace(['/', ':'], $depr, $template); + $controller = Loader::parseName($request->controller()); + if ($controller) { + if ('' == $template) { + // 如果模板文件名为空 按照默认规则定位 + $template = str_replace('.', DS, $controller) . $depr . (1 == $this->config['auto_rule'] ? Loader::parseName($request->action(true)) : $request->action()); + } elseif (false === strpos($template, $depr)) { + $template = str_replace('.', DS, $controller) . $depr . $template; + } } + } else { + $template = str_replace(['/', ':'], $depr, substr($template, 1)); } 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 33729b442d6adda714933634e34ac2fda9b83704..a314ad60650903c047742f02f9c58ac99f485971 100644 --- a/library/think/view/driver/Think.php +++ b/library/think/view/driver/Think.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2016 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- @@ -24,6 +24,8 @@ class Think private $template; // 模板引擎参数 protected $config = [ + // 视图基础目录(集中式) + 'view_base' => '', // 模板起始路径 'view_path' => '', // 模板文件后缀 @@ -32,6 +34,8 @@ class Think 'view_depr' => DS, // 是否开启模板编译缓存,设为false则每次都会重新编译 'tpl_cache' => true, + // 默认模板渲染规则 1 解析为小写+下划线 2 全部转换小写 + 'auto_rule' => 1, ]; public function __construct($config = []) @@ -103,28 +107,35 @@ class Think */ private function parseTemplate($template) { + // 分析模板文件规则 + $request = Request::instance(); // 获取视图根目录 if (strpos($template, '@')) { // 跨模块调用 list($module, $template) = explode('@', $template); - $path = APP_PATH . $module . DS . 'view' . DS; + } + if ($this->config['view_base']) { + // 基础视图目录 + $module = isset($module) ? $module : $request->module(); + $path = $this->config['view_base'] . ($module ? $module . DS : ''); } else { - // 当前视图目录 - $path = $this->config['view_path']; + $path = isset($module) ? APP_PATH . $module . DS . 'view' . DS : $this->config['view_path']; } - // 分析模板文件规则 - $request = Request::instance(); - $controller = Loader::parseName($request->controller()); - if ($controller && 0 !== strpos($template, '/')) { - $depr = $this->config['view_depr']; - $template = str_replace(['/', ':'], $depr, $template); - if ('' == $template) { - // 如果模板文件名为空 按照默认规则定位 - $template = str_replace('.', DS, $controller) . $depr . $request->action(); - } elseif (false === strpos($template, $depr)) { - $template = str_replace('.', DS, $controller) . $depr . $template; + $depr = $this->config['view_depr']; + if (0 !== strpos($template, '/')) { + $template = str_replace(['/', ':'], $depr, $template); + $controller = Loader::parseName($request->controller()); + if ($controller) { + if ('' == $template) { + // 如果模板文件名为空 按照默认规则定位 + $template = str_replace('.', DS, $controller) . $depr . (1 == $this->config['auto_rule'] ? Loader::parseName($request->action(true)) : $request->action()); + } elseif (false === strpos($template, $depr)) { + $template = str_replace('.', DS, $controller) . $depr . $template; + } } + } else { + $template = str_replace(['/', ':'], $depr, substr($template, 1)); } return $path . ltrim($template, '/') . '.' . ltrim($this->config['view_suffix'], '.'); } diff --git a/library/traits/controller/Jump.php b/library/traits/controller/Jump.php index 0fad996dad48ae44c6a7596902e71bdea50800a5..6a572246aafc2e81f9838a5241b27e91723a8762 100644 --- a/library/traits/controller/Jump.php +++ b/library/traits/controller/Jump.php @@ -1,5 +1,4 @@ server('HTTP_REFERER'))) { + $url = Request::instance()->server('HTTP_REFERER'); + } elseif ('' !== $url && !strpos($url, '://') && 0 !== strpos($url, '/')) { + $url = Url::build($url); } + + $type = $this->getResponseType(); $result = [ - 'code' => $code, + 'code' => 1, 'msg' => $msg, 'data' => $data, 'url' => $url, 'wait' => $wait, ]; - $type = $this->getResponseType(); if ('html' == strtolower($type)) { - $result = ViewTemplate::instance(Config::get('template'), Config::get('view_replace_str')) + $template = Config::get('template'); + $view = Config::get('view_replace_str'); + + $result = ViewTemplate::instance($template, $view) ->fetch(Config::get('dispatch_success_tmpl'), $result); } + $response = Response::create($result, $type)->header($header); + throw new HttpResponseException($response); } /** * 操作错误跳转的快捷方法 * @access protected - * @param mixed $msg 提示信息 - * @param string $url 跳转的URL地址 - * @param mixed $data 返回的数据 - * @param integer $wait 跳转等待时间 - * @param array $header 发送的Header信息 + * @param mixed $msg 提示信息 + * @param string $url 跳转的 URL 地址 + * @param mixed $data 返回的数据 + * @param int $wait 跳转等待时间 + * @param array $header 发送的 Header 信息 * @return void + * @throws HttpResponseException */ protected function error($msg = '', $url = null, $data = '', $wait = 3, array $header = []) { - $code = 0; - if (is_numeric($msg)) { - $code = $msg; - $msg = ''; - } if (is_null($url)) { - $url = 'javascript:history.back(-1);'; - } elseif ('' !== $url) { - $url = (strpos($url, '://') || 0 === strpos($url, '/')) ? $url : Url::build($url); + $url = Request::instance()->isAjax() ? '' : 'javascript:history.back(-1);'; + } elseif ('' !== $url && !strpos($url, '://') && 0 !== strpos($url, '/')) { + $url = Url::build($url); } + + $type = $this->getResponseType(); $result = [ - 'code' => $code, + 'code' => 0, 'msg' => $msg, 'data' => $data, 'url' => $url, 'wait' => $wait, ]; - $type = $this->getResponseType(); if ('html' == strtolower($type)) { - $result = ViewTemplate::instance(Config::get('template'), Config::get('view_replace_str')) + $template = Config::get('template'); + $view = Config::get('view_replace_str'); + + $result = ViewTemplate::instance($template, $view) ->fetch(Config::get('dispatch_error_tmpl'), $result); } + $response = Response::create($result, $type)->header($header); + throw new HttpResponseException($response); } /** - * 返回封装后的API数据到客户端 + * 返回封装后的 API 数据到客户端 * @access protected - * @param mixed $data 要返回的数据 - * @param integer $code 返回的code - * @param mixed $msg 提示信息 - * @param string $type 返回数据格式 - * @param array $header 发送的Header信息 + * @param mixed $data 要返回的数据 + * @param int $code 返回的 code + * @param mixed $msg 提示信息 + * @param string $type 返回数据格式 + * @param array $header 发送的 Header 信息 * @return void + * @throws HttpResponseException */ protected function result($data, $code = 0, $msg = '', $type = '', array $header = []) { $result = [ 'code' => $code, 'msg' => $msg, - 'time' => $_SERVER['REQUEST_TIME'], + 'time' => Request::instance()->server('REQUEST_TIME'), 'data' => $data, ]; $type = $type ?: $this->getResponseType(); $response = Response::create($result, $type)->header($header); + throw new HttpResponseException($response); } /** - * URL重定向 + * URL 重定向 * @access protected - * @param string $url 跳转的URL表达式 - * @param array|integer $params 其它URL参数 - * @param integer $code http code + * @param string $url 跳转的 URL 表达式 + * @param array|int $params 其它 URL 参数 + * @param int $code http code + * @param array $with 隐式传参 * @return void + * @throws HttpResponseException */ - protected function redirect($url, $params = [], $code = 302) + protected function redirect($url, $params = [], $code = 302, $with = []) { - $response = new Redirect($url); if (is_integer($params)) { $code = $params; $params = []; } - $response->code($code)->params($params); + + $response = new Redirect($url); + $response->code($code)->params($params)->with($with); + throw new HttpResponseException($response); } /** - * 获取当前的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'); + return Request::instance()->isAjax() + ? Config::get('default_ajax_return') + : Config::get('default_return_type'); } } diff --git a/library/traits/model/SoftDelete.php b/library/traits/model/SoftDelete.php index e08d96aadd9a1798c5e17f627d8aaa69cf807e50..70f31ba29ff2c5963fbcba4783f8f031b7cd6f17 100644 --- a/library/traits/model/SoftDelete.php +++ b/library/traits/model/SoftDelete.php @@ -2,9 +2,15 @@ namespace traits\model; +use think\Collection; +use think\db\Query; +use think\Model; + +/** + * @mixin \Think\Model + */ trait SoftDelete { - /** * 判断当前实例是否被软删除 * @access public @@ -13,39 +19,44 @@ trait SoftDelete public function trashed() { $field = $this->getDeleteTimeField(); - if (!empty($this->data[$field])) { + + if ($field && !empty($this->data[$field])) { return true; } return false; } /** - * 查询软删除数据 + * 查询包含软删除的数据 * @access public - * @return \think\db\Query + * @return Query */ public static function withTrashed() { - $model = new static(); - return $model->db(); + return (new static )->getQuery(); } /** * 只查询软删除数据 * @access public - * @return \think\db\Query + * @return Query */ public static function onlyTrashed() { $model = new static(); - $field = $model->getDeleteTimeField(); - return $model->db()->where($field, 'exp', 'is not null'); + $field = $model->getDeleteTimeField(true); + + if ($field) { + return $model->getQuery()->useSoftDelete($field, ['not null', '']); + } else { + return $model->getQuery(); + } } /** * 删除当前的记录 * @access public - * @param bool $force 是否强制删除 + * @param bool $force 是否强制删除 * @return integer */ public function delete($force = false) @@ -53,49 +64,71 @@ trait SoftDelete if (false === $this->trigger('before_delete', $this)) { return false; } + $name = $this->getDeleteTimeField(); - if (!$force) { + if ($name && !$force) { // 软删除 - $this->change[] = $name; $this->data[$name] = $this->autoWriteTimestamp($name); $result = $this->isUpdate()->save(); } else { - $result = $this->db()->delete($this->data); + // 强制删除当前模型数据 + $result = $this->getQuery()->where($this->getWhere())->delete(); + } + + // 关联删除 + if (!empty($this->relationWrite)) { + foreach ($this->relationWrite as $key => $name) { + $name = is_numeric($key) ? $name : $key; + $result = $this->getRelation($name); + if ($result instanceof Model) { + $result->delete(); + } elseif ($result instanceof Collection || is_array($result)) { + foreach ($result as $model) { + $model->delete(); + } + } + } } $this->trigger('after_delete', $this); + + // 清空原始数据 + $this->origin = []; + return $result; } /** * 删除记录 * @access public - * @param mixed $data 主键列表 支持闭包查询条件 + * @param mixed $data 主键列表(支持闭包查询条件) * @param bool $force 是否强制删除 * @return integer 成功删除的记录数 */ public static function destroy($data, $force = false) { - $model = new static(); - $query = $model->db(); + if (is_null($data)) { + return 0; + } + + // 包含软删除数据 + $query = (new static())->db(false); if (is_array($data) && key($data) !== 0) { $query->where($data); $data = null; } elseif ($data instanceof \Closure) { call_user_func_array($data, [ & $query]); $data = null; - } elseif (is_null($data)) { - return 0; } - $resultSet = $query->select($data); - $count = 0; - if ($resultSet) { + $count = 0; + if ($resultSet = $query->select($data)) { foreach ($resultSet as $data) { $result = $data->delete($force); $count += $result; } } + return $count; } @@ -107,40 +140,61 @@ trait SoftDelete */ public function restore($where = []) { + if (empty($where)) { + $pk = $this->getPk(); + $where[$pk] = $this->getData($pk); + } + $name = $this->getDeleteTimeField(); - // 恢复删除 - return $this->isUpdate()->save([$name => null], $where); + if ($name) { + // 恢复删除 + return $this->getQuery() + ->useSoftDelete($name, ['not null', '']) + ->where($where) + ->update([$name => null]); + } else { + return 0; + } } /** * 查询默认不包含软删除数据 * @access protected - * @param \think\db\Query $query 查询对象 - * @return void + * @param Query $query 查询对象 + * @return Query */ protected function base($query) { $field = $this->getDeleteTimeField(true); - $query->where($field, 'null'); + return $field ? $query->useSoftDelete($field) : $query; } /** * 获取软删除字段 * @access public - * @param bool $read 是否查询操作 写操作的时候会自动去掉表别名 + * @param bool $read 是否查询操作(写操作的时候会自动去掉表别名) * @return string */ protected function getDeleteTimeField($read = false) { - if (isset($this->deleteTime)) { - $field = $this->deleteTime; - } else { - $field = 'delete_time'; + $field = property_exists($this, 'deleteTime') && isset($this->deleteTime) ? + $this->deleteTime : + 'delete_time'; + + if (false === $field) { + return false; } + + if (!strpos($field, '.')) { + $field = '__TABLE__.' . $field; + } + if (!$read && strpos($field, '.')) { - list($alias, $field) = explode('.', $field); + $array = explode('.', $field); + $field = array_pop($array); } + return $field; } } diff --git a/library/traits/think/Instance.php b/library/traits/think/Instance.php index 206a941235d8495d20f28cb8422fc5512c34d550..428c8fde6845d0c7820fc6ab3dbe60581f6202ed 100644 --- a/library/traits/think/Instance.php +++ b/library/traits/think/Instance.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2016 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- @@ -11,35 +11,44 @@ namespace traits\think; -use \think\Exception; +use think\Exception; trait Instance { + /** + * @var null|static 实例对象 + */ protected static $instance = null; /** - * @param array $options + * 获取示例 + * @param array $options 实例配置 * @return static */ public static function instance($options = []) { - if (is_null(self::$instance)) { - self::$instance = new self($options); - } + if (is_null(self::$instance)) self::$instance = new self($options); + return self::$instance; } - // 静态调用 - public static function __callStatic($method, $params) + /** + * 静态调用 + * @param string $method 调用方法 + * @param array $params 调用参数 + * @return mixed + * @throws Exception + */ + public static function __callStatic($method, array $params) { - if (is_null(self::$instance)) { - self::$instance = new self(); - } + if (is_null(self::$instance)) self::$instance = new self(); + $call = substr($method, 1); - if (0 === strpos($method, '_') && is_callable([self::$instance, $call])) { - return call_user_func_array([self::$instance, $call], $params); - } else { + + if (0 !== strpos($method, '_') || !is_callable([self::$instance, $call])) { throw new Exception("method not exists:" . $method); } + + return call_user_func_array([self::$instance, $call], $params); } } diff --git a/start.php b/start.php index 2d5179149ae1105d06a712e6a22e0c4161362935..adb1bc65ca994d2e0565a78a3f82ab82ec65bb18 100644 --- a/start.php +++ b/start.php @@ -2,7 +2,7 @@ // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- -// | Copyright (c) 2006~2016 http://thinkphp.cn All rights reserved. +// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- @@ -12,7 +12,8 @@ namespace think; // ThinkPHP 引导文件 -// 加载基础文件 +// 1. 加载基础文件 require __DIR__ . '/base.php'; -// 执行应用 + +// 2. 执行应用 App::run()->send(); diff --git a/tests/application/config.php b/tests/application/config.php index 7bf368d65f158172edb97e0d62a556820021ea04..d94a8641dc61402c42f22be5b5d579e2b5a87e50 100644 --- a/tests/application/config.php +++ b/tests/application/config.php @@ -13,7 +13,7 @@ return [ 'url_route_on' => true, 'log' => [ - 'type' => 'trace', // 支持 socket trace file + 'type' => 'file', // 支持 socket trace file ], 'view' => [ // 模板引擎 diff --git a/tests/thinkphp/library/think/display.html b/tests/application/views/display.html similarity index 100% rename from tests/thinkphp/library/think/display.html rename to tests/application/views/display.html diff --git a/tests/application/views/display.phtml b/tests/application/views/display.phtml new file mode 100644 index 0000000000000000000000000000000000000000..e99d3025e5f416b14f042714b3ca8091f5b0255b --- /dev/null +++ b/tests/application/views/display.phtml @@ -0,0 +1 @@ +{$name??'default'} \ No newline at end of file diff --git a/tests/thinkphp/library/think/extend.html b/tests/application/views/extend.html similarity index 100% rename from tests/thinkphp/library/think/extend.html rename to tests/application/views/extend.html diff --git a/tests/thinkphp/library/think/extend2.html b/tests/application/views/extend2.html similarity index 100% rename from tests/thinkphp/library/think/extend2.html rename to tests/application/views/extend2.html diff --git a/tests/thinkphp/library/think/include.html b/tests/application/views/include.html similarity index 100% rename from tests/thinkphp/library/think/include.html rename to tests/application/views/include.html diff --git a/tests/thinkphp/library/think/include2.html b/tests/application/views/include2.html similarity index 100% rename from tests/thinkphp/library/think/include2.html rename to tests/application/views/include2.html diff --git a/tests/thinkphp/library/think/layout.html b/tests/application/views/layout.html similarity index 100% rename from tests/thinkphp/library/think/layout.html rename to tests/application/views/layout.html diff --git a/tests/thinkphp/library/think/layout2.html b/tests/application/views/layout2.html similarity index 100% rename from tests/thinkphp/library/think/layout2.html rename to tests/application/views/layout2.html diff --git a/tests/conf/apcu.ini b/tests/conf/apcu.ini deleted file mode 100644 index 9c2abed2c98dc0fd355543383d574ea2de85e2ce..0000000000000000000000000000000000000000 --- a/tests/conf/apcu.ini +++ /dev/null @@ -1,3 +0,0 @@ -extension=apcu.so - -apc.enable_cli=1 diff --git a/tests/conf/apcu_bc.ini b/tests/conf/apcu_bc.ini deleted file mode 100644 index a7f6182da10b8bce0c638d0e69b330fb4c9540b7..0000000000000000000000000000000000000000 --- a/tests/conf/apcu_bc.ini +++ /dev/null @@ -1,4 +0,0 @@ -extension=apcu.so -extension=apc.so - -apc.enable_cli=1 diff --git a/tests/extensions/5.4/apcu.so b/tests/extensions/5.4/apcu.so deleted file mode 100755 index 42a24a660e00f2fc2f57494bc4f62172da7cddf9..0000000000000000000000000000000000000000 Binary files a/tests/extensions/5.4/apcu.so and /dev/null differ diff --git a/tests/extensions/5.5/apcu.so b/tests/extensions/5.5/apcu.so deleted file mode 100755 index 26e5228a6b22f4599222f0a76b5f6ecf4c6509b4..0000000000000000000000000000000000000000 Binary files a/tests/extensions/5.5/apcu.so and /dev/null differ diff --git a/tests/extensions/5.6/apcu.so b/tests/extensions/5.6/apcu.so deleted file mode 100755 index fe952969ee5e3b7dae3305c44929714a4780e7c0..0000000000000000000000000000000000000000 Binary files a/tests/extensions/5.6/apcu.so and /dev/null differ diff --git a/tests/extensions/7.0/apc.so b/tests/extensions/7.0/apc.so deleted file mode 100755 index 7948350dab56ee7e9976db1776d7e202b8e38ad6..0000000000000000000000000000000000000000 Binary files a/tests/extensions/7.0/apc.so and /dev/null differ diff --git a/tests/extensions/7.0/apcu.so b/tests/extensions/7.0/apcu.so deleted file mode 100755 index c2f0ad1402d0e709af094ee60ff617c1f0a34bd5..0000000000000000000000000000000000000000 Binary files a/tests/extensions/7.0/apcu.so and /dev/null differ diff --git a/tests/extensions/7.0/memcached.so b/tests/extensions/7.0/memcached.so deleted file mode 100755 index 544ba6d52ee89046ac2ddab0d4acfe2d95e8bfda..0000000000000000000000000000000000000000 Binary files a/tests/extensions/7.0/memcached.so and /dev/null differ diff --git a/tests/extensions/7.0/redis.so b/tests/extensions/7.0/redis.so deleted file mode 100755 index e3aea8fcb15ca3b61d31dcedc298f7ae33ff39ee..0000000000000000000000000000000000000000 Binary files a/tests/extensions/7.0/redis.so and /dev/null differ diff --git a/tests/mock.php b/tests/mock.php index 28471ee28bdd845d77c0b626bc95c1aba3d0fbaa..0c29e5962327020436fe493eacdd85b2be7369ab 100644 --- a/tests/mock.php +++ b/tests/mock.php @@ -1,20 +1,20 @@ - -// +---------------------------------------------------------------------- - -// 测试入口文件 -$_SERVER['REQUEST_METHOD'] = 'GET'; -// 定义项目测试基础路径 -define('TEST_PATH', __DIR__ . '/'); -// 定义项目路径 -define('APP_PATH', __DIR__ . '/application/'); -// 加载框架基础文件 -require __DIR__ . '/../base.php'; -\think\Loader::addNamespace('tests', TEST_PATH); + +// +---------------------------------------------------------------------- + +// 测试入口文件 +$_SERVER['REQUEST_METHOD'] = 'GET'; +// 定义项目测试基础路径 +define('TEST_PATH', __DIR__ . '/'); +// 定义项目路径 +define('APP_PATH', __DIR__ . '/application/'); +// 加载框架基础文件 +require __DIR__ . '/../base.php'; +\think\Loader::addNamespace('tests', TEST_PATH); diff --git a/tests/script/install.sh b/tests/script/install.sh index 0adee7ed10314694bc3b63c43746ed3a23f662fe..8344169c46f3e18998b3b46c163cb83e98562adb 100755 --- a/tests/script/install.sh +++ b/tests/script/install.sh @@ -3,12 +3,6 @@ if [ $(phpenv version-name) != "hhvm" ]; then cp tests/extensions/$(phpenv version-name)/*.so $(php-config --extension-dir) - if [ $(phpenv version-name) = "7.0" ]; then - phpenv config-add tests/conf/apcu_bc.ini - else - phpenv config-add tests/conf/apcu.ini - fi - phpenv config-add tests/conf/memcached.ini phpenv config-add tests/conf/redis.ini @@ -16,3 +10,4 @@ if [ $(phpenv version-name) != "hhvm" ]; then fi composer install --no-interaction --ignore-platform-reqs +composer update diff --git a/tests/thinkphp/library/think/appTest.php b/tests/thinkphp/library/think/appTest.php index 366b4f557b997121b242ba7cd711b140cbdb22db..016bbf9cab861511dbd94ef3647fcc13d43d464d 100644 --- a/tests/thinkphp/library/think/appTest.php +++ b/tests/thinkphp/library/think/appTest.php @@ -16,7 +16,6 @@ namespace tests\thinkphp\library\think; -use ReflectionClass; use think\App; use think\Config; use think\Request; diff --git a/tests/thinkphp/library/think/behavior/One.php b/tests/thinkphp/library/think/behavior/One.php index ae8fa49d17021a23a5fd9c3a728f0f3775f64fed..1ec6e66ed049efc38f3e87ea8b8dd11ad7dbdde2 100644 --- a/tests/thinkphp/library/think/behavior/One.php +++ b/tests/thinkphp/library/think/behavior/One.php @@ -3,12 +3,12 @@ namespace tests\thinkphp\library\think\behavior; class One { - public function run(&$data){ + public function run(&$data) { $data['id'] = 1; return true; } - public function test(&$data){ + public function test(&$data) { $data['name'] = 'test'; return false; } diff --git a/tests/thinkphp/library/think/behavior/Three.php b/tests/thinkphp/library/think/behavior/Three.php index 0f2e0495b4460aa3bd8fd628f76a42315511199d..c98b7aeb66aa3a53a55b55b0159b2898896731fb 100644 --- a/tests/thinkphp/library/think/behavior/Three.php +++ b/tests/thinkphp/library/think/behavior/Three.php @@ -3,7 +3,7 @@ namespace tests\thinkphp\library\think\behavior; class Three { - public function run(&$data){ + public function run(&$data) { $data['id'] = 3; } } diff --git a/tests/thinkphp/library/think/behavior/Two.php b/tests/thinkphp/library/think/behavior/Two.php index 3e194bf5397adda351625b13812ffb6b07893e2f..1e52a2b594fc1862efdfa09844ede75b1ab8be41 100644 --- a/tests/thinkphp/library/think/behavior/Two.php +++ b/tests/thinkphp/library/think/behavior/Two.php @@ -3,7 +3,7 @@ namespace tests\thinkphp\library\think\behavior; class Two { - public function run(&$data){ + public function run(&$data) { $data['id'] = 2; } } diff --git a/tests/thinkphp/library/think/cache/driver/cacheTestCase.php b/tests/thinkphp/library/think/cache/driver/cacheTestCase.php index ec8f9b6bb649ac0fd75372255283eaac707e255c..5b2f2a3ec3283d75d16d1ed0f465166d10958197 100644 --- a/tests/thinkphp/library/think/cache/driver/cacheTestCase.php +++ b/tests/thinkphp/library/think/cache/driver/cacheTestCase.php @@ -1,207 +1,207 @@ - -// +---------------------------------------------------------------------- - -/** - * 缓存抽象类,提供一些测试 - * @author simon - */ - -namespace tests\thinkphp\library\think\cache\driver; - -use think\Cache; - -abstract class cacheTestCase extends \PHPUnit_Framework_TestCase -{ - - /** - * 获取缓存句柄,子类必须有 - * @access protected - */ - abstract protected function getCacheInstance(); - - /** - * tearDown函数 - */ - protected function tearDown() - { - } - - /** - * 设定一组测试值,包括测试字符串、整数、数组和对象 - * @return mixed - * @access public - */ - public function prepare() - { - $cache = $this->getCacheInstance(); - $cache->clear(); - $cache->set('string_test', 'string_test'); - $cache->set('number_test', 11); - $cache->set('array_test', ['array_test' => 'array_test']); - return $cache; - } - - /** - * 测试缓存设置,包括测试字符串、整数、数组和对象 - * @return mixed - * @access public - */ - public function testSet() - { - $cache = $this->getCacheInstance(); - $this->assertTrue($cache->set('string_test', 'string_test')); - $this->assertTrue($cache->set('number_test', 11)); - $this->assertTrue($cache->set('array_test', ['array_test' => 'array_test'])); - } - - /** - * 测试缓存自增 - * @return mixed - * @access public - */ - public function testInc() - { - $cache = $this->getCacheInstance(); - $this->assertEquals(14, $cache->inc('number_test', 3)); - } - - /** - * 测试缓存自减 - * @return mixed - * @access public - */ - public function testDec() - { - $cache = $this->getCacheInstance(); - $this->assertEquals(8, $cache->dec('number_test', 6)); - } - - /** - * 测试缓存读取,包括测试字符串、整数、数组和对象 - * @return mixed - * @access public - */ - public function testGet() - { - $cache = $this->prepare(); - $this->assertEquals('string_test', $cache->get('string_test')); - $this->assertEquals(11, $cache->get('number_test')); - $array = $cache->get('array_test'); - $this->assertArrayHasKey('array_test', $array); - $this->assertEquals('array_test', $array['array_test']); - - $result = $cache->set('no_expire', 1, 0); - $this->assertTrue($result); - } - - /** - * 测试缓存存在情况,包括测试字符串、整数、数组和对象 - * @return mixed - * @access public - */ - public function testExists() - { - $cache = $this->prepare(); - $this->assertNotEmpty($cache->has('string_test')); - $this->assertNotEmpty($cache->has('number_test')); - $this->assertFalse($cache->has('not_exists')); - } - - /** - * 测试缓存不存在情况,包括测试字符串、整数、数组和对象 - * @return mixed - * @access public - */ - public function testGetNonExistent() - { - $cache = $this->getCacheInstance(); - $this->assertFalse($cache->get('non_existent_key', false)); - } - - /** - * 测试特殊值缓存,包括测试字符串、整数、数组和对象 - * @return mixed - * @access public - */ - public function testStoreSpecialValues() - { - $cache = $this->getCacheInstance(); - $cache->set('null_value', null); - //清空缓存后,返回null而不是false - $this->assertTrue(is_null($cache->get('null_value'))); - } - - /** - * 缓存过期测试 - * @return mixed - * @access public - */ - public function testExpire() - { - $cache = $this->getCacheInstance(); - $this->assertTrue($cache->set('expire_test', 'expire_test', 1)); - usleep(600000); - $this->assertEquals('expire_test', $cache->get('expire_test')); - usleep(800000); - $this->assertFalse($cache->get('expire_test')); - } - - /** - * 删除缓存测试 - * @return mixed - * @access public - */ - public function testDelete() - { - $cache = $this->prepare(); - $this->assertNotNull($cache->rm('number_test')); - $this->assertFalse($cache->get('number_test')); - } - - /** - * 获取并删除缓存测试 - * @return mixed - * @access public - */ - public function testPull() - { - $cache = $this->prepare(); - $this->assertEquals(11, $cache->pull('number_test')); - $this->assertFalse($cache->get('number_test')); - } - - /** - * 清空缓存测试 - * @return mixed - * @access public - */ - public function testClear() - { - $cache = $this->prepare(); - $this->assertTrue($cache->clear()); - $this->assertFalse($cache->get('number_test')); - } - - public function testStaticCall() - { - $this->assertTrue(Cache::set('a', 1)); - $this->assertEquals(1, Cache::get('a')); - $this->assertEquals(2, Cache::inc('a')); - $this->assertEquals(4, Cache::inc('a', 2)); - $this->assertEquals(4, Cache::get('a')); - $this->assertEquals(3, Cache::dec('a')); - $this->assertEquals(1, Cache::dec('a', 2)); - $this->assertEquals(1, Cache::get('a')); - $this->assertNotNull(Cache::rm('a')); - $this->assertNotNull(Cache::clear()); - } - -} + +// +---------------------------------------------------------------------- + +/** + * 缓存抽象类,提供一些测试 + * @author simon + */ + +namespace tests\thinkphp\library\think\cache\driver; + +use think\Cache; + +abstract class cacheTestCase extends \PHPUnit_Framework_TestCase +{ + + /** + * 获取缓存句柄,子类必须有 + * @access protected + */ + abstract protected function getCacheInstance(); + + /** + * tearDown函数 + */ + protected function tearDown() + { + } + + /** + * 设定一组测试值,包括测试字符串、整数、数组和对象 + * @return mixed + * @access public + */ + public function prepare() + { + $cache = $this->getCacheInstance(); + $cache->clear(); + $cache->set('string_test', 'string_test'); + $cache->set('number_test', 11); + $cache->set('array_test', ['array_test' => 'array_test']); + return $cache; + } + + /** + * 测试缓存设置,包括测试字符串、整数、数组和对象 + * @return mixed + * @access public + */ + public function testSet() + { + $cache = $this->getCacheInstance(); + $this->assertTrue($cache->set('string_test', 'string_test')); + $this->assertTrue($cache->set('number_test', 11)); + $this->assertTrue($cache->set('array_test', ['array_test' => 'array_test'])); + } + + /** + * 测试缓存自增 + * @return mixed + * @access public + */ + public function testInc() + { + $cache = $this->getCacheInstance(); + $this->assertEquals(14, $cache->inc('number_test', 3)); + } + + /** + * 测试缓存自减 + * @return mixed + * @access public + */ + public function testDec() + { + $cache = $this->getCacheInstance(); + $this->assertEquals(8, $cache->dec('number_test', 6)); + } + + /** + * 测试缓存读取,包括测试字符串、整数、数组和对象 + * @return mixed + * @access public + */ + public function testGet() + { + $cache = $this->prepare(); + $this->assertEquals('string_test', $cache->get('string_test')); + $this->assertEquals(11, $cache->get('number_test')); + $array = $cache->get('array_test'); + $this->assertArrayHasKey('array_test', $array); + $this->assertEquals('array_test', $array['array_test']); + + $result = $cache->set('no_expire', 1, 0); + $this->assertTrue($result); + } + + /** + * 测试缓存存在情况,包括测试字符串、整数、数组和对象 + * @return mixed + * @access public + */ + public function testExists() + { + $cache = $this->prepare(); + $this->assertNotEmpty($cache->has('string_test')); + $this->assertNotEmpty($cache->has('number_test')); + $this->assertFalse($cache->has('not_exists')); + } + + /** + * 测试缓存不存在情况,包括测试字符串、整数、数组和对象 + * @return mixed + * @access public + */ + public function testGetNonExistent() + { + $cache = $this->getCacheInstance(); + $this->assertFalse($cache->get('non_existent_key', false)); + } + + /** + * 测试特殊值缓存,包括测试字符串、整数、数组和对象 + * @return mixed + * @access public + */ + public function testStoreSpecialValues() + { + $cache = $this->getCacheInstance(); + $cache->set('null_value', null); + //清空缓存后,返回null而不是false + $this->assertTrue(is_null($cache->get('null_value'))); + } + + /** + * 缓存过期测试 + * @return mixed + * @access public + */ + public function testExpire() + { + $cache = $this->getCacheInstance(); + $this->assertTrue($cache->set('expire_test', 'expire_test', 1)); + usleep(600000); + $this->assertEquals('expire_test', $cache->get('expire_test')); + usleep(800000); + $this->assertFalse($cache->get('expire_test')); + } + + /** + * 删除缓存测试 + * @return mixed + * @access public + */ + public function testDelete() + { + $cache = $this->prepare(); + $this->assertNotNull($cache->rm('number_test')); + $this->assertFalse($cache->get('number_test')); + } + + /** + * 获取并删除缓存测试 + * @return mixed + * @access public + */ + public function testPull() + { + $cache = $this->prepare(); + $this->assertEquals(11, $cache->pull('number_test')); + $this->assertFalse($cache->get('number_test')); + } + + /** + * 清空缓存测试 + * @return mixed + * @access public + */ + public function testClear() + { + $cache = $this->prepare(); + $this->assertTrue($cache->clear()); + $this->assertFalse($cache->get('number_test')); + } + + public function testStaticCall() + { + $this->assertTrue(Cache::set('a', 1)); + $this->assertEquals(1, Cache::get('a')); + $this->assertEquals(2, Cache::inc('a')); + $this->assertEquals(4, Cache::inc('a', 2)); + $this->assertEquals(4, Cache::get('a')); + $this->assertEquals(3, Cache::dec('a')); + $this->assertEquals(1, Cache::dec('a', 2)); + $this->assertEquals(1, Cache::get('a')); + $this->assertNotNull(Cache::rm('a')); + $this->assertNotNull(Cache::clear()); + } + +} diff --git a/tests/thinkphp/library/think/cache/driver/fileTest.php b/tests/thinkphp/library/think/cache/driver/fileTest.php index aee0bd46f816e7631ba2e692bce8676301c59f11..eb2099ce5dc8c30fca53ac93ea5e2939761fbbdc 100644 --- a/tests/thinkphp/library/think/cache/driver/fileTest.php +++ b/tests/thinkphp/library/think/cache/driver/fileTest.php @@ -1,46 +1,46 @@ - -// +---------------------------------------------------------------------- - -/** - * File缓存驱动测试 - * @author 刘志淳 - */ - -namespace tests\thinkphp\library\think\cache\driver; - -class fileTest extends cacheTestCase -{ - private $_cacheInstance = null; - - /** - * 基境缓存类型 - */ - protected function setUp() - { - \think\Cache::connect(['type' => 'File', 'path' => CACHE_PATH]); - } - - /** - * @return FileCache - */ - protected function getCacheInstance() - { - if (null === $this->_cacheInstance) { - $this->_cacheInstance = new \think\cache\driver\File(); - } - return $this->_cacheInstance; - } - - // skip testExpire - public function testExpire() - { - } -} + +// +---------------------------------------------------------------------- + +/** + * File缓存驱动测试 + * @author 刘志淳 + */ + +namespace tests\thinkphp\library\think\cache\driver; + +class fileTest extends cacheTestCase +{ + private $_cacheInstance = null; + + /** + * 基境缓存类型 + */ + protected function setUp() + { + \think\Cache::connect(['type' => 'File', 'path' => CACHE_PATH]); + } + + /** + * @return FileCache + */ + protected function getCacheInstance() + { + if (null === $this->_cacheInstance) { + $this->_cacheInstance = new \think\cache\driver\File(); + } + return $this->_cacheInstance; + } + + // skip testExpire + public function testExpire() + { + } +} diff --git a/tests/thinkphp/library/think/cache/driver/liteTest.php b/tests/thinkphp/library/think/cache/driver/liteTest.php index b52bdff9f3771a02102bba1976808b016327519e..19cbb9efd7dc2426553ac73245407c742d49e61b 100644 --- a/tests/thinkphp/library/think/cache/driver/liteTest.php +++ b/tests/thinkphp/library/think/cache/driver/liteTest.php @@ -1,69 +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() - { - } -} + +// +---------------------------------------------------------------------- + +/** + * 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/memcacheTest.php b/tests/thinkphp/library/think/cache/driver/memcacheTest.php index 15dfaa5a182838ce11c73466c3830c63869e252d..8975fa200cce72c9388a5804f899e730a534cc50 100644 --- a/tests/thinkphp/library/think/cache/driver/memcacheTest.php +++ b/tests/thinkphp/library/think/cache/driver/memcacheTest.php @@ -1,49 +1,49 @@ - -// +---------------------------------------------------------------------- - -/** - * Memcache缓存驱动测试 - * @author 刘志淳 - */ - -namespace tests\thinkphp\library\think\cache\driver; - -class memcacheTest extends cacheTestCase -{ - private $_cacheInstance = null; - - /** - * 基境缓存类型 - */ - protected function setUp() - { - if (!extension_loaded('memcache')) { - $this->markTestSkipped("Memcache没有安装,已跳过测试!"); - } - \think\Cache::connect(['type' => 'memcache', 'expire' => 2]); - } - - /** - * @return ApcCache - */ - protected function getCacheInstance() - { - if (null === $this->_cacheInstance) { - $this->_cacheInstance = new \think\cache\driver\Memcache(['length' => 3]); - } - return $this->_cacheInstance; - } - - // skip testExpire - public function testExpire() - { - } -} + +// +---------------------------------------------------------------------- + +/** + * Memcache缓存驱动测试 + * @author 刘志淳 + */ + +namespace tests\thinkphp\library\think\cache\driver; + +class memcacheTest extends cacheTestCase +{ + private $_cacheInstance = null; + + /** + * 基境缓存类型 + */ + protected function setUp() + { + if (!extension_loaded('memcache')) { + $this->markTestSkipped("Memcache没有安装,已跳过测试!"); + } + \think\Cache::connect(['type' => 'memcache', 'expire' => 2]); + } + + /** + * @return ApcCache + */ + protected function getCacheInstance() + { + if (null === $this->_cacheInstance) { + $this->_cacheInstance = new \think\cache\driver\Memcache(['length' => 3]); + } + return $this->_cacheInstance; + } + + // skip testExpire + public function testExpire() + { + } +} diff --git a/tests/thinkphp/library/think/cache/driver/memcachedTest.php b/tests/thinkphp/library/think/cache/driver/memcachedTest.php index 58bcbd88d5586e8aff5899d793a3323a2807e56c..d528dd224261720d568362c3b823bf3d255a9f62 100644 --- a/tests/thinkphp/library/think/cache/driver/memcachedTest.php +++ b/tests/thinkphp/library/think/cache/driver/memcachedTest.php @@ -1,72 +1,72 @@ - -// +---------------------------------------------------------------------- - -/** - * Memcached缓存驱动测试 - * @author 7IN0SAN9 - */ - -namespace tests\thinkphp\library\think\cache\driver; - -class memcachedTest extends cacheTestCase -{ - private $_cacheInstance = null; - /** - * 基境缓存类型 - */ - protected function setUp() - { - if (!extension_loaded("memcached") && !extension_loaded('memcache')) { - $this->markTestSkipped("Memcached或Memcache没有安装,已跳过测试!"); - } - \think\Cache::connect(array('type' => 'memcached', 'expire' => 2)); - } - /** - * @return ApcCache - */ - protected function getCacheInstance() - { - if (null === $this->_cacheInstance) { - $this->_cacheInstance = new \think\cache\driver\Memcached(['length' => 3]); - } - return $this->_cacheInstance; - } - /** - * 缓存过期测试《提出来测试,因为目前看通不过缓存过期测试,所以还需研究》 - * @return mixed - * @access public - */ - public function testExpire() - { - } - - public function testStaticCall() - { - } - - /** - * 测试缓存自增 - * @return mixed - * @access public - */ - public function testInc() - { - } - - /** - * 测试缓存自减 - * @return mixed - * @access public - */ - public function testDec() - { - } -} + +// +---------------------------------------------------------------------- + +/** + * Memcached缓存驱动测试 + * @author 7IN0SAN9 + */ + +namespace tests\thinkphp\library\think\cache\driver; + +class memcachedTest extends cacheTestCase +{ + private $_cacheInstance = null; + /** + * 基境缓存类型 + */ + protected function setUp() + { + if (!extension_loaded("memcached") && !extension_loaded('memcache')) { + $this->markTestSkipped("Memcached或Memcache没有安装,已跳过测试!"); + } + \think\Cache::connect(array('type' => 'memcached', 'expire' => 2)); + } + /** + * @return ApcCache + */ + protected function getCacheInstance() + { + if (null === $this->_cacheInstance) { + $this->_cacheInstance = new \think\cache\driver\Memcached(['length' => 3]); + } + return $this->_cacheInstance; + } + /** + * 缓存过期测试《提出来测试,因为目前看通不过缓存过期测试,所以还需研究》 + * @return mixed + * @access public + */ + public function testExpire() + { + } + + public function testStaticCall() + { + } + + /** + * 测试缓存自增 + * @return mixed + * @access public + */ + public function testInc() + { + } + + /** + * 测试缓存自减 + * @return mixed + * @access public + */ + public function testDec() + { + } +} diff --git a/tests/thinkphp/library/think/cache/driver/redisTest.php b/tests/thinkphp/library/think/cache/driver/redisTest.php index 839e5165a46406da62c87021eaebd0bd86bec6dc..2000ee4363020dd316178ae9b2589128c1ac1dca 100644 --- a/tests/thinkphp/library/think/cache/driver/redisTest.php +++ b/tests/thinkphp/library/think/cache/driver/redisTest.php @@ -41,7 +41,7 @@ class redisTest extends cacheTestCase $cache = $this->prepare(); $this->assertEquals('string_test', $cache->get('string_test')); $this->assertEquals(11, $cache->get('number_test')); - $result = $cache->get('array_test'); + $result = $cache->get('array_test'); $this->assertEquals('array_test', $result['array_test']); } @@ -54,7 +54,7 @@ class redisTest extends cacheTestCase $redis->handler()->setnx('key', 'value'); $value = $redis->handler()->get('key'); $this->assertEquals('value', $value); - + $redis->handler()->hset('hash', 'key', 'value'); $value = $redis->handler()->hget('hash', 'key'); $this->assertEquals('value', $value); diff --git a/tests/thinkphp/library/think/cacheTest.php b/tests/thinkphp/library/think/cacheTest.php new file mode 100644 index 0000000000000000000000000000000000000000..dbcd705372a523ae90c547a03ef94e65e7d64e3b --- /dev/null +++ b/tests/thinkphp/library/think/cacheTest.php @@ -0,0 +1,315 @@ + +// +---------------------------------------------------------------------- + +namespace tests\thinkphp\library\think; + +use tests\thinkphp\library\think\config\ConfigInitTrait; +use think\Cache; +use think\Config; + +class cacheTest extends \PHPUnit_Framework_TestCase +{ + use ConfigInitTrait { + ConfigInitTrait::tearDown as ConfigTearDown; + } + + public function tearDown() + { + $this->ConfigTearDown(); + + call_user_func(\Closure::bind(function () { + Cache::$handler = null; + Cache::$instance = []; + Cache::$readTimes = 0; + Cache::$writeTimes = 0; + }, null, '\think\Cache')); + } + + /** + * @dataProvider provideTestConnect + */ + public function testConnect($options, $expected) + { + $connection = Cache::connect($options); + $this->assertInstanceOf($expected, $connection); + $this->assertSame($connection, Cache::connect($options)); + + $instance = $this->getPropertyVal('instance'); + $this->assertArrayHasKey(md5(serialize($options)), $instance); + + $newConnection = Cache::connect($options, true); + $newInstance = $this->getPropertyVal('instance'); + $this->assertInstanceOf($expected, $connection); + $this->assertNotSame($connection, $newConnection); + $this->assertEquals($instance, $newInstance); + } + + /** + * @dataProvider provideTestInit + */ + public function testInit($options, $expected) + { + $connection = Cache::init($options); + $this->assertInstanceOf($expected, $connection); + + $connectionNew = Cache::init(['type' => 'foo']); + $this->assertSame($connection, $connectionNew); + } + + public function testStore() + { + Config::set('cache.redis', ['type' => 'redis']); + + $connectionDefault = Cache::store(); + $this->assertInstanceOf('\think\cache\driver\File', $connectionDefault); + + Config::set('cache.type', false); + $connectionNotRedis = Cache::store('redis'); + $this->assertSame($connectionDefault, $connectionNotRedis); + + Config::set('cache.type', 'complex'); + $connectionRedis = Cache::store('redis'); + $this->assertNotSame($connectionNotRedis, $connectionRedis); + $this->assertInstanceOf('\think\cache\driver\Redis', $connectionRedis); + + // 即便成功切换到其他存储类型,也不影响原先的操作句柄 + $this->assertSame($connectionDefault, Cache::store()); + } + + public function testHas() + { + $key = $this->buildTestKey('Has'); + + $this->assertFalse(Cache::has($key)); + + Cache::set($key, 5); + $this->assertTrue(Cache::has($key)); + } + + public function testGet() + { + $key = $this->buildTestKey('Get'); + + $this->assertFalse(Cache::get($key)); + + $this->assertEquals('default', Cache::get($key, 'default')); + + Cache::set($key, 5); + $this->assertSame(5, Cache::get($key)); + } + + public function testSet() + { + $key = $this->buildTestKey('Set'); + + $this->assertTrue(Cache::set(null, null)); + $this->assertTrue(Cache::set($key, 'ThinkPHP3.2')); + $this->assertTrue(Cache::set($key, 'ThinkPHP5.0', null)); + $this->assertTrue(Cache::set($key, 'ThinkPHP5.0', -1)); + $this->assertTrue(Cache::set($key, 'ThinkPHP5.0', 0)); + $this->assertTrue(Cache::set($key, 'ThinkPHP5.0', 7200)); + } + + public function testInc() + { + $key = $this->buildTestKey('Inc'); + + Cache::inc($key); + $this->assertEquals(1, Cache::get($key)); + + Cache::inc($key, '2'); + $this->assertEquals(3, Cache::get($key)); + + Cache::inc($key, -1); + $this->assertEquals(2, Cache::get($key)); + + Cache::inc($key, null); + $this->assertEquals(2, Cache::get($key)); + + Cache::inc($key, true); + $this->assertEquals(3, Cache::get($key)); + + Cache::inc($key, false); + $this->assertEquals(3, Cache::get($key)); + + Cache::inc($key, 0.789); + $this->assertEquals(3.789, Cache::get($key)); + } + public function testDec() + { + $key = $this->buildTestKey('Dec'); + + Cache::dec($key); + $this->assertEquals(-1, Cache::get($key)); + + Cache::dec($key, '2'); + $this->assertEquals(-3, Cache::get($key)); + + Cache::dec($key, -1); + $this->assertEquals(-2, Cache::get($key)); + + Cache::dec($key, null); + $this->assertEquals(-2, Cache::get($key)); + + Cache::dec($key, true); + $this->assertEquals(-3, Cache::get($key)); + + Cache::dec($key, false); + $this->assertEquals(-3, Cache::get($key)); + + Cache::dec($key, 0.359); + $this->assertEquals(-3.359, Cache::get($key)); + } + + public function testRm() + { + $key = $this->buildTestKey('Rm'); + + $this->assertFalse(Cache::rm($key)); + + Cache::set($key, 'ThinkPHP'); + $this->assertTrue(Cache::rm($key)); + } + + public function testClear() + { + $key1 = $this->buildTestKey('Clear1'); + $key2 = $this->buildTestKey('Clear2'); + + Cache::set($key1, 'ThinkPHP3.2'); + Cache::set($key2, 'ThinkPHP5.0'); + + $this->assertEquals('ThinkPHP3.2', Cache::get($key1)); + $this->assertEquals('ThinkPHP5.0', Cache::get($key2)); + Cache::clear(); + $this->assertFalse(Cache::get($key1)); + $this->assertFalse(Cache::get($key2)); + } + + public function testPull() + { + $key = $this->buildTestKey('Pull'); + + $this->assertNull(Cache::pull($key)); + + Cache::set($key, 'ThinkPHP'); + $this->assertEquals('ThinkPHP', Cache::pull($key)); + $this->assertFalse(Cache::get($key)); + } + + public function testRemember() + { + $key1 = $this->buildTestKey('Remember1'); + $key2 = $this->buildTestKey('Remember2'); + + $this->assertEquals('ThinkPHP3.2', Cache::remember($key1, 'ThinkPHP3.2')); + $this->assertEquals('ThinkPHP3.2', Cache::remember($key1, 'ThinkPHP5.0')); + + $this->assertEquals('ThinkPHP5.0', Cache::remember($key2, function () { + return 'ThinkPHP5.0'; + })); + $this->assertEquals('ThinkPHP5.0', Cache::remember($key2, function () { + return 'ThinkPHP3.2'; + })); + } + + public function testTag() + { + $key = $this->buildTestKey('Tag'); + + $cacheTagWithoutName = Cache::tag(null); + $this->assertInstanceOf('think\cache\Driver', $cacheTagWithoutName); + + $cacheTagWithKey = Cache::tag($key, [1, 2, 3]); + $this->assertSame($cacheTagWithoutName, $cacheTagWithKey); + } + + protected function getPropertyVal($name) + { + static $reflectionClass; + if (empty($reflectionClass)) { + $reflectionClass = new \ReflectionClass('\think\Cache'); + } + + $property = $reflectionClass->getProperty($name); + $property->setAccessible(true); + + return $property->getValue(); + } + + public function provideTestConnect() + { + $provideData = []; + + $options = ['type' => null]; + $expected = '\think\cache\driver\File'; + $provideData[] = [$options, $expected]; + + $options = ['type' => 'File']; + $expected = '\think\cache\driver\File'; + $provideData[] = [$options, $expected]; + + $options = ['type' => 'Lite']; + $expected = '\think\cache\driver\Lite'; + $provideData[] = [$options, $expected]; + + $options = ['type' => 'Memcached']; + $expected = '\think\cache\driver\Memcached'; + $provideData[] = [$options, $expected]; + + $options = ['type' => 'Redis']; + $expected = '\think\cache\driver\Redis'; + $provideData[] = [$options, $expected]; + + // TODO + // $options = ['type' => 'Memcache']; + // $expected = '\think\cache\driver\Memcache'; + // $provideData[] = [$options, $expected]; + // + // $options = ['type' => 'Wincache']; + // $expected = '\think\cache\driver\Wincache'; + // $provideData[] = [$options, $expected]; + + // $options = ['type' => 'Sqlite']; + // $expected = '\think\cache\driver\Sqlite'; + // $provideData[] = [$options, $expected]; + // + // $options = ['type' => 'Xcache']; + // $expected = '\think\cache\driver\Xcache'; + // $provideData[] = [$options, $expected]; + + return $provideData; + } + + public function provideTestInit() + { + $provideData = []; + + $options = []; + $expected = '\think\cache\driver\File'; + $provideData[] = [$options, $expected]; + + $options = ['type' => 'File']; + $expected = '\think\cache\driver\File'; + $provideData[] = [$options, $expected]; + + $options = ['type' => 'Lite']; + $expected = '\think\cache\driver\Lite'; + $provideData[] = [$options, $expected]; + + return $provideData; + } + + protected function buildTestKey($tag) + { + return sprintf('ThinkPHP_Test_%s_%d_%d', $tag, time(), rand(0, 10000)); + } +} diff --git a/tests/thinkphp/library/think/config/ConfigInitTrait.php b/tests/thinkphp/library/think/config/ConfigInitTrait.php new file mode 100644 index 0000000000000000000000000000000000000000..ce2b3977fe7dbb115d5d57708c7233915937f8ed --- /dev/null +++ b/tests/thinkphp/library/think/config/ConfigInitTrait.php @@ -0,0 +1,52 @@ +1', 'xml'); diff --git a/tests/thinkphp/library/think/configTest.php b/tests/thinkphp/library/think/configTest.php index 166f0f8a8a07186cba80b5af76b1d71ab189fb5b..4c92df5d9052c6b89397fca9b673585751e6b3c8 100644 --- a/tests/thinkphp/library/think/configTest.php +++ b/tests/thinkphp/library/think/configTest.php @@ -16,27 +16,23 @@ namespace tests\thinkphp\library\think; -use ReflectionClass; +use tests\thinkphp\library\think\config\ConfigInitTrait; use think\Config; class configTest extends \PHPUnit_Framework_TestCase { + use ConfigInitTrait; + public function testRange() { - $reflectedClass = new ReflectionClass('\think\Config'); - $reflectedPropertyRange = $reflectedClass->getProperty('range'); - $reflectedPropertyRange->setAccessible(true); - $reflectedPropertyConfig = $reflectedClass->getProperty('config'); - $reflectedPropertyConfig->setAccessible(true); // test default range - $this->assertEquals('_sys_', $reflectedPropertyRange->getValue()); - $config = $reflectedPropertyConfig->getValue(); - $this->assertTrue(is_array($config)); + $this->assertEquals('_sys_', call_user_func(self::$internalRangeFoo)); + + $this->assertTrue(is_array(call_user_func(self::$internalConfigFoo))); // test range initialization Config::range('_test_'); - $this->assertEquals('_test_', $reflectedPropertyRange->getValue()); - $config = $reflectedPropertyConfig->getValue(); - $this->assertEquals([], $config['_test_']); + $this->assertEquals('_test_', call_user_func(self::$internalRangeFoo)); + $this->assertEquals([], call_user_func(self::$internalConfigFoo)['_test_']); } // public function testParse() @@ -51,11 +47,6 @@ class configTest extends \PHPUnit_Framework_TestCase $name = '_name_'; $range = '_test_'; - $reflectedClass = new ReflectionClass('\think\Config'); - $reflectedPropertyConfig = $reflectedClass->getProperty('config'); - $reflectedPropertyConfig->setAccessible(true); - $reflectedPropertyConfig->setValue([]); - $this->assertEquals($config, Config::load($file, $name, $range)); $this->assertNotEquals(null, Config::load($file, $name, $range)); } @@ -64,12 +55,8 @@ class configTest extends \PHPUnit_Framework_TestCase { $range = '_test_'; $this->assertFalse(Config::has('abcd', $range)); - $reflectedClass = new ReflectionClass('\think\Config'); - $reflectedPropertyConfig = $reflectedClass->getProperty('config'); - $reflectedPropertyConfig->setAccessible(true); - // if (!strpos($name, '.')): - $reflectedPropertyConfig->setValue([ + call_user_func(self::$internalConfigFoo, [ $range => ['abcd' => 'value'], ]); $this->assertTrue(Config::has('abcd', $range)); @@ -77,7 +64,7 @@ class configTest extends \PHPUnit_Framework_TestCase // else ... $this->assertFalse(Config::has('abcd.efg', $range)); - $reflectedPropertyConfig->setValue([ + call_user_func(self::$internalConfigFoo, [ $range => ['abcd' => ['efg' => 'value']], ]); $this->assertTrue(Config::has('abcd.efg', $range)); @@ -85,24 +72,24 @@ class configTest extends \PHPUnit_Framework_TestCase public function testGet() { - $range = '_test_'; - $reflectedClass = new ReflectionClass('\think\Config'); - $reflectedPropertyConfig = $reflectedClass->getProperty('config'); - $reflectedPropertyConfig->setAccessible(true); - // test all configurations - $reflectedPropertyConfig->setValue([$range => []]); + $range = '_test_'; + call_user_func(self::$internalConfigFoo, [ + $range => [] + ]); $this->assertEquals([], Config::get(null, $range)); $this->assertEquals(null, Config::get(null, 'does_not_exist')); $value = 'value'; // test getting configuration - $reflectedPropertyConfig->setValue([$range => ['abcd' => 'efg']]); + call_user_func(self::$internalConfigFoo, [ + $range => ['abcd' => 'efg'] + ]); $this->assertEquals('efg', Config::get('abcd', $range)); $this->assertEquals(null, Config::get('does_not_exist', $range)); $this->assertEquals(null, Config::get('abcd', 'does_not_exist')); // test getting configuration with dot syntax - $reflectedPropertyConfig->setValue([$range => [ - 'one' => ['two' => $value], - ]]); + call_user_func(self::$internalConfigFoo, [ + $range => ['one' => ['two' => $value]] + ]); $this->assertEquals($value, Config::get('one.two', $range)); $this->assertEquals(null, Config::get('one.does_not_exist', $range)); $this->assertEquals(null, Config::get('one.two', 'does_not_exist')); @@ -110,23 +97,19 @@ class configTest extends \PHPUnit_Framework_TestCase public function testSet() { - $range = '_test_'; - $reflectedClass = new ReflectionClass('\think\Config'); - $reflectedPropertyConfig = $reflectedClass->getProperty('config'); - $reflectedPropertyConfig->setAccessible(true); - $reflectedPropertyConfig->setValue([]); - // if (is_string($name)): + $range = '_test_'; + // without dot syntax $name = 'name'; $value = 'value'; Config::set($name, $value, $range); - $config = $reflectedPropertyConfig->getValue(); + $config = call_user_func(self::$internalConfigFoo); $this->assertEquals($value, $config[$range][$name]); // with dot syntax $name = 'one.two'; $value = 'dot value'; Config::set($name, $value, $range); - $config = $reflectedPropertyConfig->getValue(); + $config = call_user_func(self::$internalConfigFoo); $this->assertEquals($value, $config[$range]['one']['two']); // if (is_array($name)): // see testLoad() @@ -134,25 +117,22 @@ class configTest extends \PHPUnit_Framework_TestCase // test getting all configurations...? // return self::$config[$range]; ?? $value = ['all' => 'configuration']; - $reflectedPropertyConfig->setValue([$range => $value]); + call_user_func(self::$internalConfigFoo, [$range => $value]); $this->assertEquals($value, Config::set(null, null, $range)); $this->assertNotEquals(null, Config::set(null, null, $range)); } public function testReset() { - $range = '_test_'; - $reflectedClass = new ReflectionClass('\think\Config'); - $reflectedPropertyConfig = $reflectedClass->getProperty('config'); - $reflectedPropertyConfig->setAccessible(true); - $reflectedPropertyConfig->setValue([$range => ['abcd' => 'efg']]); + $range = '_test_'; + call_user_func(self::$internalConfigFoo, [$range => ['abcd' => 'efg']]); // clear all configurations Config::reset(true); - $config = $reflectedPropertyConfig->getValue(); + $config = call_user_func(self::$internalConfigFoo); $this->assertEquals([], $config); // clear the configuration in range of parameter. - $reflectedPropertyConfig->setValue([ + call_user_func(self::$internalConfigFoo, [ $range => [ 'abcd' => 'efg', 'hijk' => 'lmn', @@ -160,7 +140,7 @@ class configTest extends \PHPUnit_Framework_TestCase 'a' => 'b', ]); Config::reset($range); - $config = $reflectedPropertyConfig->getValue(); + $config = call_user_func(self::$internalConfigFoo); $this->assertEquals([ $range => [], 'a' => 'b', diff --git a/tests/thinkphp/library/think/controllerTest.php b/tests/thinkphp/library/think/controllerTest.php index ce5f49a06e9b816a32cdc9b000f3a0d331749bcb..4164770e32f1bca871c366ec063751fbd8482cf7 100644 --- a/tests/thinkphp/library/think/controllerTest.php +++ b/tests/thinkphp/library/think/controllerTest.php @@ -40,13 +40,13 @@ class Foo extends Controller public function fetchTest() { - $template = dirname(__FILE__) . '/display.html'; - return $this->fetch($template, ['name' => 'ThinkPHP']); + $template = APP_PATH . 'views' . DS .'display.html'; + return $this->fetch($template, ['name' => 'ThinkPHP']); } public function displayTest() { - $template = dirname(__FILE__) . '/display.html'; + $template = APP_PATH . 'views' . DS .'display.html'; return $this->display($template, ['name' => 'ThinkPHP']); } public function test() @@ -71,7 +71,7 @@ class Foo extends Controller ['sex', 'in:0,1', '性别只能为为男或女'], ['age', 'between:1,80', '年龄只能在10-80之间'], ]; - return $this->validate($data, $validate); + return $this->validate($data, $validate); } } @@ -161,7 +161,7 @@ class controllerTest extends \PHPUnit_Framework_TestCase { $controller = new Foo(Request::instance()); $view = $this->getView($controller); - $template = dirname(__FILE__) . '/display.html'; + $template = APP_PATH . 'views' . DS .'display.html'; $viewFetch = $view->fetch($template, ['name' => 'ThinkPHP']); $this->assertEquals($controller->fetchTest(), $viewFetch); } @@ -170,7 +170,7 @@ class controllerTest extends \PHPUnit_Framework_TestCase { $controller = new Foo; $view = $this->getView($controller); - $template = dirname(__FILE__) . '/display.html'; + $template = APP_PATH . 'views' . DS .'display.html'; $viewFetch = $view->display($template, ['name' => 'ThinkPHP']); $this->assertEquals($controller->displayTest(), $viewFetch); diff --git a/tests/thinkphp/library/think/dbTest.php b/tests/thinkphp/library/think/dbTest.php index 9019917e7d92d4e7d14e90aa3f7549d3ce9e06b5..5724ee214923423c1284f2a098a48d90d1de485f 100644 --- a/tests/thinkphp/library/think/dbTest.php +++ b/tests/thinkphp/library/think/dbTest.php @@ -11,17 +11,342 @@ /** * Db类测试 + * @author: 刘志淳 */ namespace tests\thinkphp\library\think; -use \think\Db; +use think\Db; class dbTest extends \PHPUnit_Framework_TestCase { + // 获取测试数据库配置 + private function getConfig() + { + return [ + // 数据库类型 + 'type' => 'mysql', + // 服务器地址 + 'hostname' => '127.0.0.1', + // 数据库名 + 'database' => 'test', + // 用户名 + 'username' => 'root', + // 密码 + 'password' => '', + // 端口 + 'hostport' => '', + // 连接dsn + 'dsn' => '', + // 数据库连接参数 + 'params' => [], + // 数据库编码默认采用utf8 + 'charset' => 'utf8', + // 数据库表前缀 + 'prefix' => 'tp_', + // 数据库调试模式 + 'debug' => true, + // 数据库部署方式:0 集中式(单一服务器),1 分布式(主从服务器) + 'deploy' => 0, + // 数据库读写是否分离 主从式有效 + 'rw_separate' => false, + // 读写分离后 主服务器数量 + 'master_num' => 1, + // 指定从服务器序号 + 'slave_no' => '', + // 是否严格检查字段是否存在 + 'fields_strict' => true, + // 数据集返回类型 array 数组 collection Collection对象 + 'resultset_type' => 'array', + // 是否自动写入时间戳字段 + 'auto_timestamp' => false, + // 是否需要进行SQL性能分析 + 'sql_explain' => false, + ]; + } + + // 获取创建数据库 SQL + private function getCreateTableSql() + { + $sql[] = <<execute('show databases'); + $config = $this->getConfig(); + $result = Db::connect($config)->execute('show databases'); + $this->assertNotEmpty($result); + } + + public function testExecute() + { + $config = $this->getConfig(); + $sql = $this->getCreateTableSql(); + foreach ($sql as $one) { + Db::connect($config)->execute($one); + } + $tableNum = Db::connect($config)->execute("show tables;"); + $this->assertEquals(4, $tableNum); + } + + public function testQuery() + { + $config = $this->getConfig(); + $sql = $this->getCreateTableSql(); + Db::connect($config)->batchQuery($sql); + + $tableQueryResult = Db::connect($config)->query("show tables;"); + + $this->assertTrue(is_array($tableQueryResult)); + + $tableNum = count($tableQueryResult); + $this->assertEquals(4, $tableNum); + } + + public function testBatchQuery() + { + $config = $this->getConfig(); + $sql = $this->getCreateTableSql(); + Db::connect($config)->batchQuery($sql); + + $tableNum = Db::connect($config)->execute("show tables;"); + $this->assertEquals(4, $tableNum); + } + + public function testTable() + { + $config = $this->getConfig(); + $tableName = 'tp_user'; + $result = Db::connect($config)->table($tableName); + $this->assertEquals($tableName, $result->getOptions()['table']); + } + + public function testName() + { + $config = $this->getConfig(); + $tableName = 'user'; + $result = Db::connect($config)->name($tableName); + $this->assertEquals($config['prefix'] . $tableName, $result->getTable()); + } + + public function testInsert() + { + $config = $this->getConfig(); + $data = [ + 'username' => 'chunice', + 'password' => md5('chunice'), + 'status' => 1, + 'create_time' => time(), + ]; + $result = Db::connect($config)->name('user')->insert($data); + $this->assertEquals(1, $result); + } + + public function testUpdate() + { + $config = $this->getConfig(); + $data = [ + 'username' => 'chunice_update', + 'password' => md5('chunice'), + 'status' => 1, + 'create_time' => time(), + ]; + $result = Db::connect($config)->name('user')->where('username', 'chunice')->update($data); + $this->assertEquals(1, $result); + } + + public function testFind() + { + $config = $this->getConfig(); + $mustFind = Db::connect($config)->name('user')->where('username', 'chunice_update')->find(); + $this->assertNotEmpty($mustFind); + $mustNotFind = Db::connect($config)->name('user')->where('username', 'chunice')->find(); + $this->assertEmpty($mustNotFind); + } + + public function testInsertAll() + { + $config = $this->getConfig(); + + $data = [ + ['username' => 'foo', 'password' => md5('foo'), 'status' => 1, 'create_time' => time()], + ['username' => 'bar', 'password' => md5('bar'), 'status' => 1, 'create_time' => time()], + ]; + + $insertNum = Db::connect($config)->name('user')->insertAll($data); + $this->assertEquals(count($data), $insertNum); + } + + public function testSelect() + { + $config = $this->getConfig(); + $mustFound = Db::connect($config)->name('user')->where('status', 1)->select(); + $this->assertNotEmpty($mustFound); + $mustNotFound = Db::connect($config)->name('user')->where('status', 0)->select(); + $this->assertEmpty($mustNotFound); + } + + public function testValue() + { + $config = $this->getConfig(); + $username = Db::connect($config)->name('user')->where('id', 1)->value('username'); + $this->assertEquals('chunice_update', $username); + $usernameNull = Db::connect($config)->name('user')->where('id', 0)->value('username'); + $this->assertEmpty($usernameNull); + } + + public function testColumn() + { + $config = $this->getConfig(); + $username = Db::connect($config)->name('user')->where('status', 1)->column('username'); + $this->assertNotEmpty($username); + $usernameNull = Db::connect($config)->name('user')->where('status', 0)->column('username'); + $this->assertEmpty($usernameNull); + + } + + public function testInsertGetId() + { + $config = $this->getConfig(); + $id = Db::connect($config)->name('user')->order('id', 'desc')->value('id'); + + $data = [ + 'username' => uniqid(), + 'password' => md5('chunice'), + 'status' => 1, + 'create_time' => time(), + ]; + $lastId = Db::connect($config)->name('user')->insertGetId($data); + $this->assertEquals($id + 1, $lastId); + + } + + public function testGetLastInsId() + { + $config = $this->getConfig(); + $data = [ + 'username' => uniqid(), + 'password' => md5('chunice'), + 'status' => 1, + 'create_time' => time(), + ]; + $lastId = Db::connect($config)->name('user')->insertGetId($data); + + $lastInsId = Db::connect($config)->name('user')->getLastInsID(); + $this->assertEquals($lastId, $lastInsId); + } + + public function testSetField() + { + $config = $this->getConfig(); + + $setFieldNum = Db::connect($config)->name('user')->where('id', 1)->setField('username', 'chunice_setField'); + $this->assertEquals(1, $setFieldNum); + + $setFieldNum = Db::connect($config)->name('user')->where('id', 1)->setField('username', 'chunice_setField'); + $this->assertEquals(0, $setFieldNum); + } + + public function testSetInc() + { + $config = $this->getConfig(); + $originCreateTime = Db::connect($config)->name('user')->where('id', 1)->value('create_time'); + Db::connect($config)->name('user')->where('id', 1)->setInc('create_time'); + $newCreateTime = Db::connect($config)->name('user')->where('id', 1)->value('create_time'); + $this->assertEquals($originCreateTime + 1, $newCreateTime); + + } + + public function testSetDec() + { + $config = $this->getConfig(); + $originCreateTime = Db::connect($config)->name('user')->where('id', 1)->value('create_time'); + Db::connect($config)->name('user')->where('id', 1)->setDec('create_time'); + $newCreateTime = Db::connect($config)->name('user')->where('id', 1)->value('create_time'); + $this->assertEquals($originCreateTime - 1, $newCreateTime); + } + + public function testDelete() + { + $config = $this->getConfig(); + Db::connect($config)->name('user')->where('id', 1)->delete(); + $result = Db::connect($config)->name('user')->where('id', 1)->find(); + $this->assertEmpty($result); + } + + public function testChunk() + { + // todo 暂未想到测试方法 + } + + public function testCache() + { + $config = $this->getConfig(); + $result = Db::connect($config)->name('user')->where('id', 1)->cache('key', 60)->find(); + $cache = \think\Cache::get('key'); + $this->assertEquals($result, $cache); + + $updateCache = Db::connect($config)->name('user')->cache('key')->find(1); + $this->assertEquals($cache, $updateCache); } } diff --git a/tests/thinkphp/library/think/debugTest.php b/tests/thinkphp/library/think/debugTest.php index 12ff815f24e5d09a846e5f5273985c917b747756..36e1f049eb39bb6c851e6ec6c70092d08d20a9f7 100644 --- a/tests/thinkphp/library/think/debugTest.php +++ b/tests/thinkphp/library/think/debugTest.php @@ -1,179 +1,220 @@ - -// +---------------------------------------------------------------------- - -/** - * Debug测试 - * @author 大漠 - */ - -namespace tests\thinkphp\library\think; - -use think\Debug; - -class debugTest extends \PHPUnit_Framework_TestCase -{ - - /** - * - * @var Debug - */ - protected $object; - - /** - * Sets up the fixture, for example, opens a network connection. - * This method is called before a test is executed. - */ - protected function setUp() - { - $this->object = new Debug(); - } - - /** - * Tears down the fixture, for example, closes a network connection. - * This method is called after a test is executed. - */ - protected function tearDown() - {} - - /** - * @covers think\Debug::remark - * @todo Implement testRemark(). - */ - public function testRemark() - { - $name = "testremarkkey"; - Debug::remark($name); - } - - /** - * @covers think\Debug::getRangeTime - * @todo Implement testGetRangeTime(). - */ - public function testGetRangeTime() - { - $start = "testGetRangeTimeStart"; - $end = "testGetRangeTimeEnd"; - Debug::remark($start); - usleep(20000); - // \think\Debug::remark($end); - - $time = Debug::getRangeTime($start, $end); - $this->assertLessThan(0.03, $time); - //$this->assertEquals(0.03, ceil($time)); - } - - /** - * @covers think\Debug::getUseTime - * @todo Implement testGetUseTime(). - */ - public function testGetUseTime() - { - $time = Debug::getUseTime(); - $this->assertLessThan(20, $time); - } - - /** - * @covers think\Debug::getThroughputRate - * @todo Implement testGetThroughputRate(). - */ - public function testGetThroughputRate() - { - usleep(100000); - $throughputRate = Debug::getThroughputRate(); - $this->assertLessThan(10, $throughputRate); - } - - /** - * @covers think\Debug::getRangeMem - * @todo Implement testGetRangeMem(). - */ - public function testGetRangeMem() - { - $start = "testGetRangeMemStart"; - $end = "testGetRangeMemEnd"; - Debug::remark($start); - $str = ""; - for ($i = 0; $i < 10000; $i++) { - $str .= "mem"; - } - - $rangeMem = Debug::getRangeMem($start, $end); - - $this->assertLessThan(33, explode(" ", $rangeMem)[0]); - } - - /** - * @covers think\Debug::getUseMem - * @todo Implement testGetUseMem(). - */ - public function testGetUseMem() - { - $useMem = Debug::getUseMem(); - - $this->assertLessThan(30, explode(" ", $useMem)[0]); - } - - /** - * @covers think\Debug::getMemPeak - * @todo Implement testGetMemPeak(). - */ - public function testGetMemPeak() - { - $start = "testGetMemPeakStart"; - $end = "testGetMemPeakEnd"; - Debug::remark($start); - $str = ""; - for ($i = 0; $i < 100000; $i++) { - $str .= "mem"; - } - $memPeak = Debug::getMemPeak($start, $end); - $this->assertLessThan(500, explode(" ", $memPeak)[0]); - } - - /** - * @covers think\Debug::getFile - * @todo Implement testGetFile(). - */ - public function testGetFile() - { - $count = Debug::getFile(); - - $this->assertEquals(count(get_included_files()), $count); - - $info = Debug::getFile(true); - $this->assertEquals(count(get_included_files()), count($info)); - - $this->assertContains("KB", $info[0]); - } - - /** - * @covers think\Debug::dump - * @todo Implement testDump(). - */ - public function testDump() - { - if (strstr(PHP_VERSION, 'hhvm')) { - return ; - } - - $var = []; - $var["key"] = "val"; - $output = Debug::dump($var, false, $label = "label"); - $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 (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)); - } - } -} + +// +---------------------------------------------------------------------- + +/** + * Debug测试 + * @author 大漠 + */ + +namespace tests\thinkphp\library\think; + +use tests\thinkphp\library\think\config\ConfigInitTrait; +use think\Config; +use think\Debug; +use think\Response; +use think\response\Redirect; + +class debugTest extends \PHPUnit_Framework_TestCase +{ + use ConfigInitTrait; + + /** + * + * @var Debug + */ + protected $object; + + /** + * Sets up the fixture, for example, opens a network connection. + * This method is called before a test is executed. + */ + protected function setUp() + { + $this->object = new Debug(); + } + + /** + * Tears down the fixture, for example, closes a network connection. + * This method is called after a test is executed. + */ + protected function tearDown() + {} + + /** + * @covers \think\Debug::remark + * @todo Implement testRemark(). + */ + public function testRemark() + { + $name = "testremarkkey"; + Debug::remark($name); + } + + /** + * @covers \think\Debug::getRangeTime + * @todo Implement testGetRangeTime(). + */ + public function testGetRangeTime() + { + $start = "testGetRangeTimeStart"; + $end = "testGetRangeTimeEnd"; + Debug::remark($start); + usleep(20000); + // \think\Debug::remark($end); + + $time = Debug::getRangeTime($start, $end); + $this->assertLessThan(0.03, $time); + //$this->assertEquals(0.03, ceil($time)); + } + + /** + * @covers \think\Debug::getUseTime + * @todo Implement testGetUseTime(). + */ + public function testGetUseTime() + { + $time = Debug::getUseTime(); + $this->assertLessThan(30, $time); + } + + /** + * @covers \think\Debug::getThroughputRate + * @todo Implement testGetThroughputRate(). + */ + public function testGetThroughputRate() + { + usleep(100000); + $throughputRate = Debug::getThroughputRate(); + $this->assertLessThan(10, $throughputRate); + } + + /** + * @covers \think\Debug::getRangeMem + * @todo Implement testGetRangeMem(). + */ + public function testGetRangeMem() + { + $start = "testGetRangeMemStart"; + $end = "testGetRangeMemEnd"; + Debug::remark($start); + $str = ""; + for ($i = 0; $i < 10000; $i++) { + $str .= "mem"; + } + + $rangeMem = Debug::getRangeMem($start, $end); + + $this->assertLessThan(33, explode(" ", $rangeMem)[0]); + } + + /** + * @covers \think\Debug::getUseMem + * @todo Implement testGetUseMem(). + */ + public function testGetUseMem() + { + $useMem = Debug::getUseMem(); + + $this->assertLessThan(35, explode(" ", $useMem)[0]); + } + + /** + * @covers \think\Debug::getMemPeak + * @todo Implement testGetMemPeak(). + */ + public function testGetMemPeak() + { + $start = "testGetMemPeakStart"; + $end = "testGetMemPeakEnd"; + Debug::remark($start); + $str = ""; + for ($i = 0; $i < 100000; $i++) { + $str .= "mem"; + } + $memPeak = Debug::getMemPeak($start, $end); + $this->assertLessThan(500, explode(" ", $memPeak)[0]); + } + + /** + * @covers \think\Debug::getFile + * @todo Implement testGetFile(). + */ + public function testGetFile() + { + $count = Debug::getFile(); + + $this->assertEquals(count(get_included_files()), $count); + + $info = Debug::getFile(true); + $this->assertEquals(count(get_included_files()), count($info)); + + $this->assertContains("KB", $info[0]); + } + + /** + * @covers \think\Debug::dump + * @todo Implement testDump(). + */ + public function testDump() + { + if (strstr(PHP_VERSION, 'hhvm')) { + return; + } + + $var = []; + $var["key"] = "val"; + $output = Debug::dump($var, false, $label = "label"); + $array = explode("array", json_encode($output)); + if (IS_WIN) { + $this->assertEquals("(1) {\\n [\\\"key\\\"] => string(3) \\\"val\\\"\\n}\\n\\r\\n\"", end($array)); + } elseif (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)); + } + } + + /** + * @expectedException \think\exception\ClassNotFoundException + */ + public function testInjectWithErrorType() + { + Config::set('trace', ['type' => 'NullDebug']); + + $response = new Response(); + $context = 'TestWithErrorType'; + + Debug::inject($response, $context); + } + + public function testInject() + { + Config::set('trace', ['type' => 'Console']); + + $response = new Response(); + $context = 'TestWithoutBodyTag'; + Debug::inject($response, $context); + $this->assertNotEquals('TestWithoutBodyTag', $context); + $this->assertStringStartsWith('TestWithoutBodyTag', $context); + + $response = new Response(); + $context = ''; + Debug::inject($response, $context); + $this->assertNotEquals('', $context); + $this->assertStringStartsWith('', $context); + $this->assertStringEndsWith('', $context); + + $response = new Redirect(); + $context = ''; + Debug::inject($response, $context); + $this->assertEquals('', $context); + } +} diff --git a/tests/thinkphp/library/think/lang/lang.php b/tests/thinkphp/library/think/lang/lang.php index 044b171b2fc6eb5aa14b621338dbe77b6183c187..96880b15632ee4de29c240e277b22466f2dcc563 100644 --- a/tests/thinkphp/library/think/lang/lang.php +++ b/tests/thinkphp/library/think/lang/lang.php @@ -1,4 +1,4 @@ '加载', -]; \ No newline at end of file +]; diff --git a/tests/thinkphp/library/think/log/driver/fileTest.php b/tests/thinkphp/library/think/log/driver/fileTest.php index 3453c09ba998f10858ecbffab9a065b6e15be2bb..ad6dd224b7913852deb928def9f303524cb17479 100644 --- a/tests/thinkphp/library/think/log/driver/fileTest.php +++ b/tests/thinkphp/library/think/log/driver/fileTest.php @@ -1,34 +1,34 @@ - -// +---------------------------------------------------------------------- - -/** - * Test File Log - */ -namespace tests\thinkphp\library\think\log\driver; - -use think\Log; - -class fileTest extends \PHPUnit_Framework_TestCase -{ - protected function setUp() - { - Log::init(['type' => 'file']); - } - - public function testRecord() - { - $record_msg = 'record'; - Log::record($record_msg, 'notice'); - $logs = Log::getLog(); - - $this->assertNotFalse(array_search($record_msg, $logs['notice'])); - } -} + +// +---------------------------------------------------------------------- + +/** + * Test File Log + */ +namespace tests\thinkphp\library\think\log\driver; + +use think\Log; + +class fileTest extends \PHPUnit_Framework_TestCase +{ + protected function setUp() + { + Log::init(['type' => 'file']); + } + + public function testRecord() + { + $record_msg = 'record'; + Log::record($record_msg, 'notice'); + $logs = Log::getLog(); + + $this->assertEquals([], $logs); + } +} diff --git a/tests/thinkphp/library/think/logTest.php b/tests/thinkphp/library/think/logTest.php index 554cf60d153343f5fbd41de547eac9146189eb3d..e786b17300067c9eb8f108b71458375e496ef07a 100644 --- a/tests/thinkphp/library/think/logTest.php +++ b/tests/thinkphp/library/think/logTest.php @@ -20,18 +20,6 @@ use think\Log; class logTest extends \PHPUnit_Framework_TestCase { - public function testRecord() - { - Log::clear(); - Log::record('test'); - $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(['info' => ['test']], Log::getLog()); - } - public function testSave() { Log::init(['type' => 'test']); diff --git a/tests/thinkphp/library/think/paginateTest.php b/tests/thinkphp/library/think/paginateTest.php index 5bbcc96128e4ff8d9f8cee40d29e183746dd141c..8cd4550738158ed49bc4ef27d249d0d41938178b 100644 --- a/tests/thinkphp/library/think/paginateTest.php +++ b/tests/thinkphp/library/think/paginateTest.php @@ -11,7 +11,6 @@ namespace tests\thinkphp\library\think; - use think\paginator\driver\Bootstrap; class paginateTest extends \PHPUnit_Framework_TestCase @@ -38,7 +37,4 @@ class paginateTest extends \PHPUnit_Framework_TestCase $this->assertEquals($render, $p->render()); } - - - -} \ No newline at end of file +} diff --git a/tests/thinkphp/library/think/requestTest.php b/tests/thinkphp/library/think/requestTest.php index 964b5c85969a44593bc7381ca12bc5e3098dccdc..3da94a2e9b897ecad591a2e208d2733877405ea8 100644 --- a/tests/thinkphp/library/think/requestTest.php +++ b/tests/thinkphp/library/think/requestTest.php @@ -17,7 +17,6 @@ namespace tests\thinkphp\library\think; use think\Config; use think\Request; -use think\Route; class requestTest extends \PHPUnit_Framework_TestCase { @@ -90,12 +89,14 @@ class requestTest extends \PHPUnit_Framework_TestCase public function testType() { - $request = Request::instance(); - $_SERVER['HTTP_ACCEPT'] = 'application/json'; + $request = Request::instance(); + $request->server(['HTTP_ACCEPT' => 'application/json']); + $this->assertEquals('json', $request->type()); $request->mimeType('test', 'application/test'); $request->mimeType(['test' => 'application/test']); - $_SERVER['HTTP_ACCEPT'] = 'application/test'; + $request->server(['HTTP_ACCEPT' => 'application/test']); + $this->assertEquals('test', $request->type()); } @@ -103,13 +104,13 @@ class requestTest extends \PHPUnit_Framework_TestCase { $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'] = 'DELETE'; - $request = new Request(); + $request = Request::create('', ''); $this->assertEquals('DELETE', $request->method()); $this->assertEquals('GET', $request->method(true)); Config::set('var_method', '_method'); $_POST['_method'] = 'POST'; - $request = new Request(); + $request = Request::create('', ''); $this->assertEquals('POST', $request->method()); $this->assertEquals('GET', $request->method(true)); $this->assertTrue($request->isPost()); @@ -130,7 +131,7 @@ class requestTest extends \PHPUnit_Framework_TestCase public function testVar() { Config::set('app_multi_module', true); - $request = new Request(); + $request = Request::create(''); $request->route(['name' => 'thinkphp', 'id' => 6]); $request->get(['id' => 10]); $request->post(['id' => 8]); @@ -156,28 +157,44 @@ class requestTest extends \PHPUnit_Framework_TestCase public function testIsAjax() { - $request = new Request(); + $request = Request::create(''); + + $this->assertFalse($request->isAjax()); + $_SERVER['HTTP_X_REQUESTED_WITH'] = 'xmlhttprequest'; + $this->assertFalse($request->isAjax()); + $this->assertFalse($request->isAjax(true)); + + $request->server(['HTTP_X_REQUESTED_WITH' => 'xmlhttprequest']); $this->assertTrue($request->isAjax()); } public function testIsPjax() { - $request = new Request(); + $request = Request::create(''); + + $this->assertFalse($request->isPjax()); + $_SERVER['HTTP_X_PJAX'] = true; + $this->assertFalse($request->isPjax()); + $this->assertFalse($request->isPjax(true)); + + $request->server(['HTTP_X_PJAX' => true]); + $this->assertTrue($request->isPjax()); + $request->server(['HTTP_X_PJAX' => false]); $this->assertTrue($request->isPjax()); } public function testIsMobile() { - $request = new Request(); + $request = Request::create(''); $_SERVER['HTTP_VIA'] = 'wap'; $this->assertTrue($request->isMobile()); } public function testBind() { - $request = new Request(); + $request = Request::create(''); $request->user = 'User1'; $request->bind(['user' => 'User2']); $this->assertEquals('User2', $request->user); diff --git a/tests/thinkphp/library/think/responseTest.php b/tests/thinkphp/library/think/responseTest.php index aa21a4b6a6224e303a2e676d0654e0019acf7ce7..62d1574734e540a95b3dfd9b45351127594304d7 100644 --- a/tests/thinkphp/library/think/responseTest.php +++ b/tests/thinkphp/library/think/responseTest.php @@ -1,95 +1,95 @@ - -// +---------------------------------------------------------------------- - -/** - * Response测试 - * @author 大漠 - */ - -namespace tests\thinkphp\library\think; - -use think\Config; -use think\Request; -use think\Response; - -class responseTest extends \PHPUnit_Framework_TestCase -{ - - /** - * - * @var \think\Response - */ - protected $object; - - protected $default_return_type; - - protected $default_ajax_return; - - /** - * Sets up the fixture, for example, opens a network connection. - * This method is called before a test is executed. - */ - protected function setUp() - { - // 1. - // restore_error_handler(); - // Warning: Cannot modify header information - headers already sent by (output started at PHPUnit\Util\Printer.php:173) - // more see in https://www.analysisandsolutions.com/blog/html/writing-phpunit-tests-for-wordpress-plugins-wp-redirect-and-continuing-after-php-errors.htm - - // 2. - // the Symfony used the HeaderMock.php - - // 3. - // not run the eclipse will held, and travis-ci.org Searching for coverage reports - // **> Python coverage not found - // **> No coverage report found. - // add the - // /** - // * @runInSeparateProcess - // */ - if (!$this->default_return_type) { - $this->default_return_type = Config::get('default_return_type'); - } - if (!$this->default_ajax_return) { - $this->default_ajax_return = Config::get('default_ajax_return'); - } - } - - /** - * Tears down the fixture, for example, closes a network connection. - * This method is called after a test is executed. - */ - protected function tearDown() - { - Config::set('default_ajax_return', $this->default_ajax_return); - Config::set('default_return_type', $this->default_return_type); - } - - /** - * @covers think\Response::send - * @todo Implement testSend(). - */ - public function testSend() - { - $dataArr = []; - $dataArr["key"] = "value"; - - $response = Response::create($dataArr, 'json'); - $result = $response->getContent(); - $this->assertEquals('{"key":"value"}', $result); - $request = Request::instance(); - $request->get(['callback' => 'callback']); - $response = Response::create($dataArr, 'jsonp'); - $result = $response->getContent(); - $this->assertEquals('callback({"key":"value"});', $result); - } - -} + +// +---------------------------------------------------------------------- + +/** + * Response测试 + * @author 大漠 + */ + +namespace tests\thinkphp\library\think; + +use think\Config; +use think\Request; +use think\Response; + +class responseTest extends \PHPUnit_Framework_TestCase +{ + + /** + * + * @var \think\Response + */ + protected $object; + + protected $default_return_type; + + protected $default_ajax_return; + + /** + * Sets up the fixture, for example, opens a network connection. + * This method is called before a test is executed. + */ + protected function setUp() + { + // 1. + // restore_error_handler(); + // Warning: Cannot modify header information - headers already sent by (output started at PHPUnit\Util\Printer.php:173) + // more see in https://www.analysisandsolutions.com/blog/html/writing-phpunit-tests-for-wordpress-plugins-wp-redirect-and-continuing-after-php-errors.htm + + // 2. + // the Symfony used the HeaderMock.php + + // 3. + // not run the eclipse will held, and travis-ci.org Searching for coverage reports + // **> Python coverage not found + // **> No coverage report found. + // add the + // /** + // * @runInSeparateProcess + // */ + if (!$this->default_return_type) { + $this->default_return_type = Config::get('default_return_type'); + } + if (!$this->default_ajax_return) { + $this->default_ajax_return = Config::get('default_ajax_return'); + } + } + + /** + * Tears down the fixture, for example, closes a network connection. + * This method is called after a test is executed. + */ + protected function tearDown() + { + Config::set('default_ajax_return', $this->default_ajax_return); + Config::set('default_return_type', $this->default_return_type); + } + + /** + * @covers think\Response::send + * @todo Implement testSend(). + */ + public function testSend() + { + $dataArr = []; + $dataArr["key"] = "value"; + + $response = Response::create($dataArr, 'json'); + $result = $response->getContent(); + $this->assertEquals('{"key":"value"}', $result); + $request = Request::instance(); + $request->get(['callback' => 'callback']); + $response = Response::create($dataArr, 'jsonp'); + $result = $response->getContent(); + $this->assertEquals('callback({"key":"value"});', $result); + } + +} diff --git a/tests/thinkphp/library/think/routeTest.php b/tests/thinkphp/library/think/routeTest.php index d6790347461d3036c384cee3a634afaf6ec4781d..31e1b8611f5f0eca2d4445f3fba0e68c824443da 100644 --- a/tests/thinkphp/library/think/routeTest.php +++ b/tests/thinkphp/library/think/routeTest.php @@ -227,7 +227,7 @@ class routeTest extends \PHPUnit_Framework_TestCase { $request = Request::instance(); Route::get('say/:name', '@index/hello'); - $this->assertEquals(['type' => 'controller', 'controller' => 'index/hello'], Route::check($request, 'say/thinkphp')); + $this->assertEquals(['type' => 'controller', 'controller' => 'index/hello', 'var' => []], Route::check($request, 'say/thinkphp')); } public function testRouteToMethod() @@ -235,8 +235,8 @@ class routeTest extends \PHPUnit_Framework_TestCase $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'], Route::check($request, 'user/thinkphp')); - $this->assertEquals(['type' => 'method', 'method' => ['\app\index\model\Info', 'getInfo']], Route::check($request, 'info/thinkphp')); + $this->assertEquals(['type' => 'method', 'method' => '\app\index\service\User::get', 'var' => []], Route::check($request, 'user/thinkphp')); + $this->assertEquals(['type' => 'method', 'method' => ['\app\index\model\Info', 'getInfo'], 'var' => []], Route::check($request, 'info/thinkphp')); } public function testRouteToRedirect() @@ -257,10 +257,10 @@ class routeTest extends \PHPUnit_Framework_TestCase $this->assertEquals(['index', 'blog', 'test'], $result['module']); Route::bind('\app\index\controller', 'namespace'); - $this->assertEquals(['type' => 'method', 'method' => ['\app\index\controller\blog', 'read']], Route::check($request, 'blog/read')); + $this->assertEquals(['type' => 'method', 'method' => ['\app\index\controller\Blog', 'read'], 'var' => []], Route::check($request, 'blog/read')); - Route::bind('\app\index\controller\blog', 'class'); - $this->assertEquals(['type' => 'method', 'method' => ['\app\index\controller\blog', 'read']], Route::check($request, 'read')); + Route::bind('\app\index\controller\Blog', 'class'); + $this->assertEquals(['type' => 'method', 'method' => ['\app\index\controller\Blog', 'read'], 'var' => []], Route::check($request, 'read')); } public function testDomain() diff --git a/tests/thinkphp/library/think/sessionTest.php b/tests/thinkphp/library/think/sessionTest.php index ab3e64f499175f721c5b6cd4a046c1152b83d5ac..5fd95f102cc5a14d3d9dca875d1270e7a0f76fb6 100644 --- a/tests/thinkphp/library/think/sessionTest.php +++ b/tests/thinkphp/library/think/sessionTest.php @@ -1,319 +1,319 @@ - -// +---------------------------------------------------------------------- - -/** - * Session测试 - * @author 大漠 - */ - -namespace tests\thinkphp\library\think; - -use think\Session; - -class sessionTest extends \PHPUnit_Framework_TestCase -{ - - /** - * - * @var \think\Session - */ - protected $object; - - /** - * Sets up the fixture, for example, opens a network connection. - * This method is called before a test is executed. - */ - protected function setUp() - { - // $this->object = new Session (); - // register_shutdown_function ( function () { - // } ); // 此功能无法取消,需要回调函数配合。 - set_exception_handler(function () {}); - set_error_handler(function () {}); - } - - /** - * Tears down the fixture, for example, closes a network connection. - * This method is called after a test is executed. - */ - protected function tearDown() - { - register_shutdown_function('think\Error::appShutdown'); - set_error_handler('think\Error::appError'); - set_exception_handler('think\Error::appException'); - } - - /** - * @covers think\Session::prefix - * - * @todo Implement testPrefix(). - */ - public function testPrefix() - { - Session::prefix(null); - Session::prefix('think_'); - - $this->assertEquals('think_', Session::prefix()); - } - - /** - * @covers think\Session::init - * - * @todo Implement testInit(). - */ - public function testInit() - { - Session::prefix(null); - $config = [ - // cookie 名称前缀 - 'prefix' => 'think_', - // cookie 保存时间 - 'expire' => 60, - // cookie 保存路径 - 'path' => '/path/to/test/session/', - // cookie 有效域名 - 'domain' => '.thinkphp.cn', - 'var_session_id' => 'sessionidtest', - 'id' => 'sess_8fhgkjuakhatbeg2fa14lo84q1', - 'name' => 'session_name', - 'use_trans_sid' => '1', - 'use_cookies' => '1', - 'cache_limiter' => '60', - 'cache_expire' => '60', - 'type' => '', // memcache - 'namespace' => '\\think\\session\\driver\\', // ? - 'auto_start' => '1', - ]; - - $_REQUEST[$config['var_session_id']] = $config['id']; - Session::init($config); - - // 开始断言 - $this->assertEquals($config['prefix'], Session::prefix()); - $this->assertEquals($config['id'], $_REQUEST[$config['var_session_id']]); - $this->assertEquals($config['name'], session_name()); - - $this->assertEquals($config['path'], session_save_path()); - $this->assertEquals($config['use_cookies'], ini_get('session.use_cookies')); - $this->assertEquals($config['domain'], ini_get('session.cookie_domain')); - $this->assertEquals($config['expire'], ini_get('session.gc_maxlifetime')); - $this->assertEquals($config['expire'], ini_get('session.cookie_lifetime')); - - $this->assertEquals($config['cache_limiter'], session_cache_limiter($config['cache_limiter'])); - $this->assertEquals($config['cache_expire'], session_cache_expire($config['cache_expire'])); - - // 检测分支 - $_REQUEST[$config['var_session_id']] = null; - session_write_close(); - session_destroy(); - - Session::init($config); - - // 测试auto_start - // PHP_SESSION_DISABLED - // PHP_SESSION_NONE - // PHP_SESSION_ACTIVE - // session_status() - if (strstr(PHP_VERSION, 'hhvm')) { - $this->assertEquals('', ini_get('session.auto_start')); - } else { - $this->assertEquals(0, ini_get('session.auto_start')); - } - - $this->assertEquals($config['use_trans_sid'], ini_get('session.use_trans_sid')); - - Session::init($config); - $this->assertEquals($config['id'], session_id()); - } - - /** - * 单独重现异常 - * @expectedException \think\Exception - */ - public function testException() - { - $config = [ - // cookie 名称前缀 - 'prefix' => 'think_', - // cookie 保存时间 - 'expire' => 0, - // cookie 保存路径 - 'path' => '/path/to/test/session/', - // cookie 有效域名 - 'domain' => '.thinkphp.cn', - 'var_session_id' => 'sessionidtest', - 'id' => 'sess_8fhgkjuakhatbeg2fa14lo84q1', - 'name' => 'session_name', - 'use_trans_sid' => '1', - 'use_cookies' => '1', - 'cache_limiter' => '60', - 'cache_expire' => '60', - 'type' => '\\think\\session\\driver\\Memcache', // - 'auto_start' => '1', - ]; - - // 测试session驱动是否存在 - // @expectedException 异常类名 - $this->setExpectedException('\think\exception\ClassNotFoundException', 'error session handler'); - - Session::init($config); - } - - /** - * @covers think\Session::set - * - * @todo Implement testSet(). - */ - public function testSet() - { - Session::prefix(null); - Session::set('sessionname', 'sessionvalue'); - $this->assertEquals('sessionvalue', $_SESSION['sessionname']); - - Session::set('sessionnamearr.subname', 'sessionvalue'); - $this->assertEquals('sessionvalue', $_SESSION['sessionnamearr']['subname']); - - Session::set('sessionnameper', 'sessionvalue', 'think_'); - $this->assertEquals('sessionvalue', $_SESSION['think_']['sessionnameper']); - - Session::set('sessionnamearrper.subname', 'sessionvalue', 'think_'); - $this->assertEquals('sessionvalue', $_SESSION['think_']['sessionnamearrper']['subname']); - } - - /** - * @covers think\Session::get - * - * @todo Implement testGet(). - */ - public function testGet() - { - Session::prefix(null); - - Session::set('sessionnameget', 'sessionvalue'); - $this->assertEquals(Session::get('sessionnameget'), $_SESSION['sessionnameget']); - - Session::set('sessionnamegetarr.subname', 'sessionvalue'); - $this->assertEquals(Session::get('sessionnamegetarr.subname'), $_SESSION['sessionnamegetarr']['subname']); - - Session::set('sessionnamegetarrperall', 'sessionvalue', 'think_'); - $this->assertEquals(Session::get('', 'think_')['sessionnamegetarrperall'], $_SESSION['think_']['sessionnamegetarrperall']); - - Session::set('sessionnamegetper', 'sessionvalue', 'think_'); - $this->assertEquals(Session::get('sessionnamegetper', 'think_'), $_SESSION['think_']['sessionnamegetper']); - - Session::set('sessionnamegetarrper.subname', 'sessionvalue', 'think_'); - $this->assertEquals(Session::get('sessionnamegetarrper.subname', 'think_'), $_SESSION['think_']['sessionnamegetarrper']['subname']); - } - - public function testPull() - { - Session::prefix(null); - Session::set('sessionnamedel', 'sessionvalue'); - $this->assertEquals('sessionvalue', Session::pull('sessionnameget')); - $this->assertNull(Session::get('sessionnameget')); - } - - /** - * @covers think\Session::delete - * - * @todo Implement testDelete(). - */ - public function testDelete() - { - Session::prefix(null); - Session::set('sessionnamedel', 'sessionvalue'); - Session::delete('sessionnamedel'); - $this->assertEmpty($_SESSION['sessionnamedel']); - - Session::set('sessionnamedelarr.subname', 'sessionvalue'); - Session::delete('sessionnamedelarr.subname'); - $this->assertEmpty($_SESSION['sessionnamedelarr']['subname']); - - Session::set('sessionnamedelper', 'sessionvalue', 'think_'); - Session::delete('sessionnamedelper', 'think_'); - $this->assertEmpty($_SESSION['think_']['sessionnamedelper']); - - Session::set('sessionnamedelperarr.subname', 'sessionvalue', 'think_'); - Session::delete('sessionnamedelperarr.subname', 'think_'); - $this->assertEmpty($_SESSION['think_']['sessionnamedelperarr']['subname']); - } - - /** - * @covers think\Session::clear - * - * @todo Implement testClear(). - */ - public function testClear() - { - Session::prefix(null); - - Session::set('sessionnameclsper', 'sessionvalue1', 'think_'); - Session::clear('think_'); - $this->assertNull($_SESSION['think_']); - - Session::set('sessionnameclsper', 'sessionvalue1', 'think_'); - Session::clear(); - $this->assertEmpty($_SESSION); - } - - /** - * @covers think\Session::has - * - * @todo Implement testHas(). - */ - public function testHas() - { - Session::prefix(null); - Session::set('sessionnamehas', 'sessionvalue'); - $this->assertTrue(Session::has('sessionnamehas')); - - Session::set('sessionnamehasarr.subname', 'sessionvalue'); - $this->assertTrue(Session::has('sessionnamehasarr.subname')); - - Session::set('sessionnamehasper', 'sessionvalue', 'think_'); - $this->assertTrue(Session::has('sessionnamehasper', 'think_')); - - Session::set('sessionnamehasarrper.subname', 'sessionvalue', 'think_'); - $this->assertTrue(Session::has('sessionnamehasarrper.subname', 'think_')); - } - - /** - * @covers think\Session::pause - * - * @todo Implement testPause(). - */ - public function testPause() - { - Session::pause(); - } - - /** - * @covers think\Session::start - * - * @todo Implement testStart(). - */ - public function testStart() - { - Session::start(); - } - - /** - * @covers think\Session::destroy - * - * @todo Implement testDestroy(). - */ - public function testDestroy() - { - Session::set('sessionnamedestroy', 'sessionvalue'); - Session::destroy(); - $this->assertEmpty($_SESSION['sessionnamedestroy']); - } -} + +// +---------------------------------------------------------------------- + +/** + * Session测试 + * @author 大漠 + */ + +namespace tests\thinkphp\library\think; + +use think\Session; + +class sessionTest extends \PHPUnit_Framework_TestCase +{ + + /** + * + * @var \think\Session + */ + protected $object; + + /** + * Sets up the fixture, for example, opens a network connection. + * This method is called before a test is executed. + */ + protected function setUp() + { + // $this->object = new Session (); + // register_shutdown_function ( function () { + // } ); // 此功能无法取消,需要回调函数配合。 + set_exception_handler(function () {}); + set_error_handler(function () {}); + } + + /** + * Tears down the fixture, for example, closes a network connection. + * This method is called after a test is executed. + */ + protected function tearDown() + { + register_shutdown_function('think\Error::appShutdown'); + set_error_handler('think\Error::appError'); + set_exception_handler('think\Error::appException'); + } + + /** + * @covers think\Session::prefix + * + * @todo Implement testPrefix(). + */ + public function testPrefix() + { + Session::prefix(null); + Session::prefix('think_'); + + $this->assertEquals('think_', Session::prefix()); + } + + /** + * @covers think\Session::init + * + * @todo Implement testInit(). + */ + public function testInit() + { + Session::prefix(null); + $config = [ + // cookie 名称前缀 + 'prefix' => 'think_', + // cookie 保存时间 + 'expire' => 60, + // cookie 保存路径 + 'path' => '/path/to/test/session/', + // cookie 有效域名 + 'domain' => '.thinkphp.cn', + 'var_session_id' => 'sessionidtest', + 'id' => 'sess_8fhgkjuakhatbeg2fa14lo84q1', + 'name' => 'session_name', + 'use_trans_sid' => '1', + 'use_cookies' => '1', + 'cache_limiter' => '60', + 'cache_expire' => '60', + 'type' => '', // memcache + 'namespace' => '\\think\\session\\driver\\', // ? + 'auto_start' => '1', + ]; + + $_REQUEST[$config['var_session_id']] = $config['id']; + Session::init($config); + + // 开始断言 + $this->assertEquals($config['prefix'], Session::prefix()); + $this->assertEquals($config['id'], $_REQUEST[$config['var_session_id']]); + $this->assertEquals($config['name'], session_name()); + + $this->assertEquals($config['path'], session_save_path()); + $this->assertEquals($config['use_cookies'], ini_get('session.use_cookies')); + $this->assertEquals($config['domain'], ini_get('session.cookie_domain')); + $this->assertEquals($config['expire'], ini_get('session.gc_maxlifetime')); + $this->assertEquals($config['expire'], ini_get('session.cookie_lifetime')); + + $this->assertEquals($config['cache_limiter'], session_cache_limiter($config['cache_limiter'])); + $this->assertEquals($config['cache_expire'], session_cache_expire($config['cache_expire'])); + + // 检测分支 + $_REQUEST[$config['var_session_id']] = null; + session_write_close(); + session_destroy(); + + Session::init($config); + + // 测试auto_start + // PHP_SESSION_DISABLED + // PHP_SESSION_NONE + // PHP_SESSION_ACTIVE + // session_status() + if (strstr(PHP_VERSION, 'hhvm')) { + $this->assertEquals('', ini_get('session.auto_start')); + } else { + $this->assertEquals(0, ini_get('session.auto_start')); + } + + $this->assertEquals($config['use_trans_sid'], ini_get('session.use_trans_sid')); + + Session::init($config); + $this->assertEquals($config['id'], session_id()); + } + + /** + * 单独重现异常 + * @expectedException \think\Exception + */ + public function testException() + { + $config = [ + // cookie 名称前缀 + 'prefix' => 'think_', + // cookie 保存时间 + 'expire' => 0, + // cookie 保存路径 + 'path' => '/path/to/test/session/', + // cookie 有效域名 + 'domain' => '.thinkphp.cn', + 'var_session_id' => 'sessionidtest', + 'id' => 'sess_8fhgkjuakhatbeg2fa14lo84q1', + 'name' => 'session_name', + 'use_trans_sid' => '1', + 'use_cookies' => '1', + 'cache_limiter' => '60', + 'cache_expire' => '60', + 'type' => '\\think\\session\\driver\\Memcache', // + 'auto_start' => '1', + ]; + + // 测试session驱动是否存在 + // @expectedException 异常类名 + $this->setExpectedException('\think\exception\ClassNotFoundException', 'error session handler'); + + Session::init($config); + } + + /** + * @covers think\Session::set + * + * @todo Implement testSet(). + */ + public function testSet() + { + Session::prefix(null); + Session::set('sessionname', 'sessionvalue'); + $this->assertEquals('sessionvalue', $_SESSION['sessionname']); + + Session::set('sessionnamearr.subname', 'sessionvalue'); + $this->assertEquals('sessionvalue', $_SESSION['sessionnamearr']['subname']); + + Session::set('sessionnameper', 'sessionvalue', 'think_'); + $this->assertEquals('sessionvalue', $_SESSION['think_']['sessionnameper']); + + Session::set('sessionnamearrper.subname', 'sessionvalue', 'think_'); + $this->assertEquals('sessionvalue', $_SESSION['think_']['sessionnamearrper']['subname']); + } + + /** + * @covers think\Session::get + * + * @todo Implement testGet(). + */ + public function testGet() + { + Session::prefix(null); + + Session::set('sessionnameget', 'sessionvalue'); + $this->assertEquals(Session::get('sessionnameget'), $_SESSION['sessionnameget']); + + Session::set('sessionnamegetarr.subname', 'sessionvalue'); + $this->assertEquals(Session::get('sessionnamegetarr.subname'), $_SESSION['sessionnamegetarr']['subname']); + + Session::set('sessionnamegetarrperall', 'sessionvalue', 'think_'); + $this->assertEquals(Session::get('', 'think_')['sessionnamegetarrperall'], $_SESSION['think_']['sessionnamegetarrperall']); + + Session::set('sessionnamegetper', 'sessionvalue', 'think_'); + $this->assertEquals(Session::get('sessionnamegetper', 'think_'), $_SESSION['think_']['sessionnamegetper']); + + Session::set('sessionnamegetarrper.subname', 'sessionvalue', 'think_'); + $this->assertEquals(Session::get('sessionnamegetarrper.subname', 'think_'), $_SESSION['think_']['sessionnamegetarrper']['subname']); + } + + public function testPull() + { + Session::prefix(null); + Session::set('sessionnamedel', 'sessionvalue'); + $this->assertEquals('sessionvalue', Session::pull('sessionnameget')); + $this->assertNull(Session::get('sessionnameget')); + } + + /** + * @covers think\Session::delete + * + * @todo Implement testDelete(). + */ + public function testDelete() + { + Session::prefix(null); + Session::set('sessionnamedel', 'sessionvalue'); + Session::delete('sessionnamedel'); + $this->assertEmpty($_SESSION['sessionnamedel']); + + Session::set('sessionnamedelarr.subname', 'sessionvalue'); + Session::delete('sessionnamedelarr.subname'); + $this->assertEmpty($_SESSION['sessionnamedelarr']['subname']); + + Session::set('sessionnamedelper', 'sessionvalue', 'think_'); + Session::delete('sessionnamedelper', 'think_'); + $this->assertEmpty($_SESSION['think_']['sessionnamedelper']); + + Session::set('sessionnamedelperarr.subname', 'sessionvalue', 'think_'); + Session::delete('sessionnamedelperarr.subname', 'think_'); + $this->assertEmpty($_SESSION['think_']['sessionnamedelperarr']['subname']); + } + + /** + * @covers think\Session::clear + * + * @todo Implement testClear(). + */ + public function testClear() + { + Session::prefix(null); + + Session::set('sessionnameclsper', 'sessionvalue1', 'think_'); + Session::clear('think_'); + $this->assertNull($_SESSION['think_']); + + Session::set('sessionnameclsper', 'sessionvalue1', 'think_'); + Session::clear(); + $this->assertEmpty($_SESSION); + } + + /** + * @covers think\Session::has + * + * @todo Implement testHas(). + */ + public function testHas() + { + Session::prefix(null); + Session::set('sessionnamehas', 'sessionvalue'); + $this->assertTrue(Session::has('sessionnamehas')); + + Session::set('sessionnamehasarr.subname', 'sessionvalue'); + $this->assertTrue(Session::has('sessionnamehasarr.subname')); + + Session::set('sessionnamehasper', 'sessionvalue', 'think_'); + $this->assertTrue(Session::has('sessionnamehasper', 'think_')); + + Session::set('sessionnamehasarrper.subname', 'sessionvalue', 'think_'); + $this->assertTrue(Session::has('sessionnamehasarrper.subname', 'think_')); + } + + /** + * @covers think\Session::pause + * + * @todo Implement testPause(). + */ + public function testPause() + { + Session::pause(); + } + + /** + * @covers think\Session::start + * + * @todo Implement testStart(). + */ + public function testStart() + { + Session::start(); + } + + /** + * @covers think\Session::destroy + * + * @todo Implement testDestroy(). + */ + public function testDestroy() + { + Session::set('sessionnamedestroy', 'sessionvalue'); + Session::destroy(); + $this->assertEmpty($_SESSION['sessionnamedestroy']); + } +} diff --git a/tests/thinkphp/library/think/template/taglib/cxTest.php b/tests/thinkphp/library/think/template/taglib/cxTest.php index a77824b69caa144942a327d8740b8847bda5fce5..8aee392fa2bd6512da16acd89f3bb2f8ddc37159 100644 --- a/tests/thinkphp/library/think/template/taglib/cxTest.php +++ b/tests/thinkphp/library/think/template/taglib/cxTest.php @@ -19,7 +19,7 @@ namespace tests\thinkphp\library\think\tempplate\taglib; use think\Template; use think\template\taglib\Cx; -class templateTest extends \PHPUnit_Framework_TestCase +class cxTest extends \PHPUnit_Framework_TestCase { public function testPhp() { @@ -47,7 +47,7 @@ EOF; {/volist} EOF; $data = <<\$vo): \$mod = (\$key % 2 );++\$key;?> +\$vo): \$mod = (\$key % 2 );++\$key;?> EOF; @@ -88,7 +88,7 @@ EOF; {/foreach} EOF; $data = <<\$val): ?> +\$val): ?> EOF; @@ -389,7 +389,7 @@ default {/empty} EOF; $data = <<isEmpty())): ?> +isEmpty())): ?> default EOF; @@ -402,7 +402,7 @@ default {/notempty} EOF; $data = <<isEmpty()))): ?> +isEmpty()))): ?> default EOF; @@ -538,13 +538,13 @@ EOF; 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 29100f4d9fd46096c7d9f14f9c79b1090c1d56e1..11358306940ab98deab5dc8b27dfb7f9fc0b2803 100644 --- a/tests/thinkphp/library/think/templateTest.php +++ b/tests/thinkphp/library/think/templateTest.php @@ -16,331 +16,160 @@ namespace tests\thinkphp\library\think; +use think\Cache; use think\Template; class templateTest extends \PHPUnit_Framework_TestCase { - public function testVar() - { - $template = new Template(); - - $content = << -EOF; - - $template->parse($content); - $this->assertEquals($data, $content); - - $content = << -EOF; - - $template->parse($content); - $this->assertEquals($data, $content); - - $content = << -EOF; - - $template->parse($content); - $this->assertEquals($data, $content); - - $content = << -EOF; - - $template->parse($content); - $this->assertEquals($data, $content); - - $content = << -EOF; - - $template->parse($content); - $this->assertEquals($data, $content); - - $content = << -EOF; - - $template->parse($content); - $this->assertEquals($data, $content); - - $content = << -EOF; - - $template->parse($content); - $this->assertEquals($data, $content); - - $content = << -EOF; - - $template->parse($content); - $this->assertEquals($data, $content); - - $content = << -EOF; - - $template->parse($content); - $this->assertEquals($data, $content); - - $content = << -EOF; - - $template->parse($content); - $this->assertEquals($data, $content); - - $content = << -EOF; - - $template->parse($content); - $this->assertEquals($data, $content); - - $content = << -EOF; - - $template->parse($content); - $this->assertEquals($data, $content); - - $content = <<parse($content); - $this->assertEquals($data, $content); - - $content = <<parse($content); - $this->assertEquals($data, $content); + /** + * @var Template + */ + protected $template; + public function setUp() + { + $this->template = new Template(); } - public function testVarFunction() + public function testAssign() { - $template = new Template(); + $reflectProperty = new \ReflectionProperty(get_class($this->template), 'data'); + $reflectProperty->setAccessible(true); - $content = << -EOF; - - $template->parse($content); - $this->assertEquals($data, $content); + $this->template->assign('version', 'ThinkPHP3.2'); + $data = $reflectProperty->getValue($this->template); + $this->assertEquals('ThinkPHP3.2', $data['version']); - $content = << -EOF; - - $template->parse($content); - $this->assertEquals($data, $content); - - $content = << - -EOF; - - $template->parse($content); - $this->assertEquals($data, $content); + $this->template->assign(['name' => 'Gao', 'version' => 'ThinkPHP5']); + $data = $reflectProperty->getValue($this->template); + $this->assertEquals('Gao', $data['name']); + $this->assertEquals('ThinkPHP5', $data['version']); } - public function testVarIdentify() + public function testGet() { - $config['tpl_begin'] = '<#'; - $config['tpl_end'] = '#>'; - $config['tpl_var_identify'] = ''; - $template = new Template($config); - - $content = << -EOF; - $data = <<a)) ? (is_array(\$info)?\$info['a']:\$info->a) : 'test'; ?> -EOF; + $this->template = new Template(); + $data = [ + 'project' => 'ThinkPHP', + 'version' => [ + 'ThinkPHP5' => ['Think5.0', 'Think5.1'] + ] + ]; + $this->template->assign($data); - $template->parse($content); - $this->assertEquals($data, $content); + $this->assertSame($data, $this->template->get()); + $this->assertSame('ThinkPHP', $this->template->get('project')); + $this->assertSame(['Think5.0', 'Think5.1'], $this->template->get('version.ThinkPHP5')); + $this->assertNull($this->template->get('version.ThinkPHP3.2')); + } - $content = << -EOF; - $data = <<a)) echo 'test'; ?> -EOF; + /** + * @dataProvider provideTestParseWithVar + */ + public function testParseWithVar($content, $expected) + { + $this->template = new Template(); - $template->parse($content); - $this->assertEquals($data, $content); + $this->template->parse($content); + $this->assertEquals($expected, $content); + } - $content = << -EOF; - $data = <<a)==(is_array(\$info)?\$info['b']:\$info->b)) echo 'test'; ?> -EOF; + /** + * @dataProvider provideTestParseWithVarFunction + */ + public function testParseWithVarFunction($content, $expected) + { + $this->template = new Template(); - $template->parse($content); - $this->assertEquals($data, $content); + $this->template->parse($content); + $this->assertEquals($expected, $content); + } - $content = << -EOF; - $data = <<a) !== ''?(is_array(\$info)?\$info['a']:\$info->a):'test')?'yes':'no'; ?> -EOF; - $template->parse($content); - $this->assertEquals($data, $content); + /** + * @dataProvider provideTestParseWithVarIdentify + */ + public function testParseWithVarIdentify($content, $expected, $config) + { + $this->template = new Template($config); - $template2 = new Template(); - $template2->tpl_var_identify = 'obj'; - $content = <<b)?'yes':'no'; ?> -EOF; - $template2->parse($content); - $this->assertEquals($data, $content); + $this->template->parse($content); + $this->assertEquals($expected, $content); } - public function testThinkVar() + /** + * @dataProvider provideTestParseWithThinkVar + */ + public function testParseWithThinkVar($content, $expected) { $config['tpl_begin'] = '{'; $config['tpl_end'] = '}'; - $template = new Template($config); + $this->template = new Template($config); $_SERVER['SERVER_NAME'] = 'server_name'; $_GET['action'] = 'action'; $_POST['action'] = 'action'; $_COOKIE['name'] = 'name'; $_SESSION['action'] = ['name' => 'name']; - define('SITE_NAME', 'site_name'); - $content = << -{\$Think.GET.action}
-{\$Think.POST.action}
-{\$Think.COOKIE.action}
-{\$Think.COOKIE.action.name}
-{\$Think.SESSION.action}
-{\$Think.SESSION.action.name}
-{\$Think.ENV.OS}
-{\$Think.REQUEST.action}
-{\$Think.CONST.SITE_NAME}
-{\$Think.LANG.action}
-{\$Think.CONFIG.action.name}
-{\$Think.NOW}
-{\$Think.VERSION}
-{\$Think.LDELIM}
-{\$Think.RDELIM}
-{\$Think.SITE_NAME}
-{\$Think.SITE.URL} -EOF; - $data = <<
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- -EOF; - $template->parse($content); - $this->assertEquals($data, $content); + $this->template->parse($content); + $this->assertEquals($expected, $content); + } + + /** + * @expectedException \think\exception\TemplateNotFoundException + */ + public function testFetchWithEmptyTemplate() + { + $this->template = new Template(); + + $this->template->fetch('Foo'); + } + + /** + * @dataProvider provideTestFetchWithNoCache + */ + public function testFetchWithNoCache($data, $expected) + { + $this->template = new Template(); + + $this->template->fetch($data['template'], $data['vars'], $data['config']); + + $this->expectOutputString($expected); } - public function testFetch() + public function testFetchWithCache() { - $template = new Template(); - $template->assign('name', 'name'); + $this->template = new Template(); + + $data = [ + 'name' => 'value' + ]; $config = [ - 'strip_space' => true, - 'view_path' => dirname(__FILE__) . DS, - 'cache_id' => '__CACHE_ID__', + 'cache_id' => 'TEST_FETCH_WITH_CACHE', 'display_cache' => true, ]; - $data = ['name' => 'value']; - $template->layout('layout')->fetch('display', $data, $config); + + $this->template->fetch(APP_PATH . 'views' . DS .'display.html', $data, $config); + $this->expectOutputString('value'); + $this->assertEquals('value', Cache::get($config['cache_id'])); } public function testDisplay() { - $config['view_path'] = dirname(__FILE__) . DS; - $config['view_suffix'] = '.html'; - $config['layout_on'] = true; - $config['layout_name'] = 'layout'; - $template = new Template($config); - $files = ['extend' => 'extend', 'include' => 'include']; - $template->assign('files', $files); - $template->assign('user', ['name' => 'name', 'account' => 100]); - $template->assign('message', 'message'); - $template->assign('info', ['value' => 'value']); + $config = [ + 'view_path' => APP_PATH . DS . 'views' . DS, + 'view_suffix' => '.html', + 'layout_on' => true, + 'layout_name' => 'layout' + ]; + + $this->template = new Template($config); + + $this->template->assign('files', ['extend' => 'extend', 'include' => 'include']); + $this->template->assign('user', ['name' => 'name', 'account' => 100]); + $this->template->assign('message', 'message'); + $this->template->assign('info', ['value' => 'value']); $content = << header
@@ -382,33 +211,205 @@ value: php code
EOF; - $template->display($content); - $this->expectOutputString($content2); -// $template->parse($content); - // var_dump($content); + $this->template->display($content); + $this->expectOutputString($expected); } - public function testVarAssign() + /** + * @dataProvider provideTestLayout + */ + public function testLayout($data, $expected) { - $template = new Template(); - $template->assign('name', 'value'); - $value = $template->get('name'); - $this->assertEquals('value', $value); + $this->template = new Template(); + + $this->template->layout($data['name'], $data['replace']); + + $this->assertSame($expected['layout_on'], $this->template->config('layout_on')); + $this->assertSame($expected['layout_name'], $this->template->config('layout_name')); + $this->assertSame($expected['layout_item'], $this->template->config('layout_item')); } - public function testVarGet() + public function testParseAttr() { - $template = new Template(); - $data = ['a' => 'a', 'b' => 'b']; - $template->assign($data); - $this->assertEquals($data, $template->get()); + $attributes = $this->template->parseAttr(""); + $this->assertSame(['version' => 'ThinkPHP', 'name' => 'Gao'], $attributes); + + $attributes = $this->template->parseAttr("TestCase", 'version'); + $this->assertSame('ThinkPHP', $attributes); } public function testIsCache() { - $template = new Template(['cache_id' => '__CACHE_ID__', 'display_cache' => true]); - $this->assertTrue($template->isCache('__CACHE_ID__')); - $template->display_cache = false; - $this->assertTrue(!$template->isCache('__CACHE_ID__')); + $this->template = new Template(); + $config = [ + 'cache_id' => rand(0, 10000) . rand(0, 10000) . time(), + 'display_cache' => true + ]; + + $this->assertFalse($this->template->isCache($config['cache_id'])); + + $this->template->fetch(APP_PATH . 'views' . DS .'display.html', [], $config); + $this->assertTrue($this->template->isCache($config['cache_id'])); + } + + public function provideTestParseWithVar() + { + return [ + ["{\$name.a.b}", ""], + ["{\$name.a??'test'}", ""], + ["{\$name.a?='test'}", ""], + ["{\$name.a?:'test'}", ""], + ["{\$name.a?\$name.b:'no'}", ""], + ["{\$name.a==\$name.b?='test'}", ""], + ["{\$name.a==\$name.b?'a':'b'}", ""], + ["{\$name.a|default='test'==\$name.b?'a':'b'}", ""], + ["{\$name.a|trim==\$name.b?='eq'}", ""], + ["{:ltrim(rtrim(\$name.a))}", ""], + ["{~echo(trim(\$name.a))}", ""], + ["{++\$name.a}", ""], + ["{/*\$name*/}", ""], + ["{\$0a}", "{\$0a}"] + ]; + } + + public function provideTestParseWithVarFunction() + { + return [ + ["{\$name.a.b|default='test'}", ""], + ["{\$create_time|date=\"y-m-d\",###}", ""], + ["{\$name}\n{\$name|trim|substr=0,3}", "\n"] + ]; + } + + public function provideTestParseWithVarIdentify() + { + $config['tpl_begin'] = '<#'; + $config['tpl_end'] = '#>'; + $config['tpl_var_identify'] = ''; + + return [ + [ + "<#\$info.a??'test'#>", + "a)) ? (is_array(\$info)?\$info['a']:\$info->a) : 'test'; ?>", + $config + ], + [ + "<#\$info.a?='test'#>", + "a)) echo 'test'; ?>", + $config + ], + [ + "<#\$info.a==\$info.b?='test'#>", + "a)==(is_array(\$info)?\$info['b']:\$info->b)) echo 'test'; ?>", + $config + ], + [ + "<#\$info.a|default='test'?'yes':'no'#>", + "a) ?: 'test')?'yes':'no'; ?>", + $config + ], + [ + "{\$info2.b|trim?'yes':'no'}", + "b)?'yes':'no'; ?>", + array_merge(['tpl_var_identify' => 'obj']) + ] + ]; + } + + public function provideTestParseWithThinkVar() + { + return [ + ["{\$Think.SERVER.SERVER_NAME}
", "server('SERVER_NAME'); ?>
"], + ["{\$Think.GET.action}
", "get('action'); ?>
"], + ["{\$Think.POST.action}
", "post('action'); ?>
"], + ["{\$Think.COOKIE.action}
", "
"], + ["{\$Think.COOKIE.action.name}
", "
"], + ["{\$Think.SESSION.action}
", "
"], + ["{\$Think.SESSION.action.name}
", "
"], + ["{\$Think.ENV.OS}
", "env('OS'); ?>
"], + ["{\$Think.REQUEST.action}
", "request('action'); ?>
"], + ["{\$Think.CONST.THINK_VERSION}
", "
"], + ["{\$Think.LANG.action}
", "
"], + ["{\$Think.CONFIG.action.name}
", "
"], + ["{\$Think.NOW}
", "
"], + ["{\$Think.VERSION}
", "
"], + ["{\$Think.LDELIM}
", "
"], + ["{\$Think.RDELIM}
", "
"], + ["{\$Think.THINK_VERSION}
", "
"], + ["{\$Think.SITE.URL}", ""] + ]; + } + + public function provideTestFetchWithNoCache() + { + $provideData = []; + + $this->template = [ + 'template' => APP_PATH . 'views' . DS .'display.html', + 'vars' => [], + 'config' => [] + ]; + $expected = 'default'; + $provideData[] = [$this->template, $expected]; + + $this->template = [ + 'template' => APP_PATH . 'views' . DS .'display.html', + 'vars' => ['name' => 'ThinkPHP5'], + 'config' => [] + ]; + $expected = 'ThinkPHP5'; + $provideData[] = [$this->template, $expected]; + + $this->template = [ + 'template' => 'views@display', + 'vars' => [], + 'config' => [ + 'view_suffix' => 'html' + ] + ]; + $expected = 'default'; + $provideData[] = [$this->template, $expected]; + + $this->template = [ + 'template' => 'views@/display', + 'vars' => ['name' => 'ThinkPHP5'], + 'config' => [ + 'view_suffix' => 'phtml' + ] + ]; + $expected = 'ThinkPHP5'; + $provideData[] = [$this->template, $expected]; + + $this->template = [ + 'template' => 'display', + 'vars' => ['name' => 'ThinkPHP5'], + 'config' => [ + 'view_suffix' => 'html', + 'view_base' => APP_PATH . 'views' . DS + ] + ]; + $expected = 'ThinkPHP5'; + $provideData[] = [$this->template, $expected]; + + return $provideData; + } + + public function provideTestLayout() + { + $provideData = []; + + $data = ['name' => false, 'replace' => '']; + $expected = ['layout_on' => false, 'layout_name' => 'layout', 'layout_item' => '{__CONTENT__}']; + $provideData[] = [$data, $expected]; + + $data = ['name' => null, 'replace' => '']; + $expected = ['layout_on' => true, 'layout_name' => 'layout', 'layout_item' => '{__CONTENT__}']; + $provideData[] = [$data, $expected]; + + $data = ['name' => 'ThinkName', 'replace' => 'ThinkReplace']; + $expected = ['layout_on' => true, 'layout_name' => 'ThinkName', 'layout_item' => 'ThinkReplace']; + $provideData[] = [$data, $expected]; + + return $provideData; } } diff --git a/tests/thinkphp/library/think/urlTest.php b/tests/thinkphp/library/think/urlTest.php index 94dfc71d628896385f624127e6810e9c414011b3..401d973480a79d45b48df4ebdad324c7eff6a1c9 100644 --- a/tests/thinkphp/library/think/urlTest.php +++ b/tests/thinkphp/library/think/urlTest.php @@ -16,16 +16,29 @@ namespace tests\thinkphp\library\think; +use tests\thinkphp\library\think\config\ConfigInitTrait; use think\Config; use think\Route; use think\Url; class urlTest extends \PHPUnit_Framework_TestCase { + use ConfigInitTrait; public function setUp() { - Route::rules([]); + Route::rules(['get' => [], + 'post' => [], + 'put' => [], + 'delete' => [], + 'patch' => [], + 'head' => [], + 'options' => [], + '*' => [], + 'alias' => [], + 'domain' => [], + 'pattern' => [], + 'name' => []]); Route::name([]); } @@ -63,7 +76,7 @@ class urlTest extends \PHPUnit_Framework_TestCase public function testBuildMethod() { Route::get('blog/:id', '\app\index\controller\blog@read'); - $this->assertEquals('/blog/10.html', Url::build('[\app\index\controller\blog@read]', 'id=10', 'html')); + $this->assertEquals('/blog/10.html', Url::build('\app\index\controller\blog@read', 'id=10', 'html')); } public function testBuildRoute() @@ -77,7 +90,7 @@ class urlTest extends \PHPUnit_Framework_TestCase public function testBuildNameRoute() { Route::get(['name', 'blog/:id'], 'index/blog'); - $this->assertEquals([['blog/:id', ['id' => 1], null]], Route::name('name')); + $this->assertEquals([['blog/:id', ['id' => 1], null, null]], Route::name('name')); Config::set('url_html_suffix', 'shtml'); $this->assertEquals('/blog/10.shtml', Url::build('name?id=10')); } @@ -86,21 +99,21 @@ 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')); + $this->assertEquals('/blog/10.shtml#detail', Url::build('index/blog#detail', 'id=10')); Config::set('url_common_param', true); - $this->assertEquals('/blog/10.shtml?foo=bar#detail', Url::build('/blog/10#detail', "foo=bar")); + $this->assertEquals('/blog/10.shtml?foo=bar#detail', Url::build('index/blog#detail', "id=10&foo=bar")); } public function testBuildDomain() { Config::set('url_domain_deploy', true); Route::domain('subdomain.thinkphp.cn', 'admin'); - $this->assertEquals('http://subdomain.thinkphp.cn/blog/10.shtml', Url::build('/blog/10')); + $this->assertEquals('http://subdomain.thinkphp.cn/blog/10.html', Url::build('/blog/10')); Route::domain('subdomain.thinkphp.cn', [ 'hello/:name' => 'index/hello', ]); - $this->assertEquals('http://subdomain.thinkphp.cn/hello/thinkphp.shtml', Url::build('index/hello?name=thinkphp')); + $this->assertEquals('http://subdomain.thinkphp.cn/hello/thinkphp.html', Url::build('index/hello?name=thinkphp')); } public function testRoot() diff --git a/tests/thinkphp/library/think/validateTest.php b/tests/thinkphp/library/think/validateTest.php index 4d7819b55332ad7c551eae991d05a50865e257d3..b5f43333049e99973f42b7e3f4f9789a3a9e7ec4 100644 --- a/tests/thinkphp/library/think/validateTest.php +++ b/tests/thinkphp/library/think/validateTest.php @@ -57,8 +57,8 @@ class validateTest extends \PHPUnit_Framework_TestCase 'ip' => 'ip|ip:ipv4', 'score' => 'float|gt:60|notBetween:90,100|notIn:70,80|lt:100|elt:100|egt:60', 'status' => 'integer|in:0,1,2', - 'begin_time' => 'after:2016-3-18', - 'end_time' => 'before:2016-10-01', + 'begin_time' => 'after:2016-3-18|beforeWith:end_time', + 'end_time' => 'before:2016-10-01|afterWith:begin_time', 'info' => 'require|array|length:4|max:5|min:2', 'info.name' => 'require|length:8|alpha|same:thinkphp', 'value' => 'same:100|different:status', @@ -88,7 +88,7 @@ class validateTest extends \PHPUnit_Framework_TestCase 'date' => '16-3-8', 'ok' => 'yes', 'value' => 100, - 'bool' => 'true', + 'bool' => true, 'title' => '流年ThinkPHP', 'city' => '上海', 'nickname' => '流年ThinkPHP_2016', diff --git a/tests/thinkphp/library/think/viewTest.php b/tests/thinkphp/library/think/viewTest.php index 56804798fd2653bab7a740595dad09354508a033..5bb7de16c0e40c2ed4f2d34d29fb1cfe2cbf1269 100644 --- a/tests/thinkphp/library/think/viewTest.php +++ b/tests/thinkphp/library/think/viewTest.php @@ -1,76 +1,76 @@ - -// +---------------------------------------------------------------------- - -/** - * view测试 - * @author mahuan - */ - -namespace tests\thinkphp\library\think; - -class viewTest extends \PHPUnit_Framework_TestCase -{ - - /** - * 句柄测试 - * @return mixed - * @access public - */ - public function testGetInstance() - { - \think\Cookie::get('a'); - $view_instance = \think\View::instance(); - $this->assertInstanceOf('\think\view', $view_instance, 'instance方法返回错误'); - } - - /** - * 测试变量赋值 - * @return mixed - * @access public - */ - public function testAssign() - { - $view_instance = \think\View::instance(); - $view_instance->key = 'value'; - $this->assertTrue(isset($view_instance->key)); - $this->assertEquals('value', $view_instance->key); - $data = $view_instance->assign(array('key' => 'value')); - $data = $view_instance->assign('key2', 'value2'); - //测试私有属性 - $expect_data = array('key' => 'value', 'key2' => 'value2'); - $this->assertAttributeEquals($expect_data, 'data', $view_instance); - } - - /** - * 测试引擎设置 - * @return mixed - * @access public - */ - public function testEngine() - { - $view_instance = \think\View::instance(); - $data = $view_instance->engine('php'); - $data = $view_instance->engine(['type' => 'php', 'view_path' => '', 'view_suffix' => '.php', 'view_depr' => DS]); - $php_engine = new \think\view\driver\Php(['view_path' => '', 'view_suffix' => '.php', 'view_depr' => DS]); - $this->assertAttributeEquals($php_engine, 'engine', $view_instance); - //测试模板引擎驱动 - $data = $view_instance->engine(['type' => 'think', 'view_path' => '', 'view_suffix' => '.html', 'view_depr' => DS]); - $think_engine = new \think\view\driver\Think(['view_path' => '', 'view_suffix' => '.html', 'view_depr' => DS]); - $this->assertAttributeEquals($think_engine, 'engine', $view_instance); - } - - public function testReplace() - { - $view_instance = \think\View::instance(); - $view_instance->replace('string', 'replace')->display('string'); - } - -} + +// +---------------------------------------------------------------------- + +/** + * view测试 + * @author mahuan + */ + +namespace tests\thinkphp\library\think; + +class viewTest extends \PHPUnit_Framework_TestCase +{ + + /** + * 句柄测试 + * @return mixed + * @access public + */ + public function testGetInstance() + { + \think\Cookie::get('a'); + $view_instance = \think\View::instance(); + $this->assertInstanceOf('\think\view', $view_instance, 'instance方法返回错误'); + } + + /** + * 测试变量赋值 + * @return mixed + * @access public + */ + public function testAssign() + { + $view_instance = \think\View::instance(); + $view_instance->key = 'value'; + $this->assertTrue(isset($view_instance->key)); + $this->assertEquals('value', $view_instance->key); + $data = $view_instance->assign(array('key' => 'value')); + $data = $view_instance->assign('key2', 'value2'); + //测试私有属性 + $expect_data = array('key' => 'value', 'key2' => 'value2'); + $this->assertAttributeEquals($expect_data, 'data', $view_instance); + } + + /** + * 测试引擎设置 + * @return mixed + * @access public + */ + public function testEngine() + { + $view_instance = \think\View::instance(); + $data = $view_instance->engine('php'); + $data = $view_instance->engine(['type' => 'php', 'view_path' => '', 'view_suffix' => '.php', 'view_depr' => DS]); + $php_engine = new \think\view\driver\Php(['view_path' => '', 'view_suffix' => '.php', 'view_depr' => DS]); + $this->assertAttributeEquals($php_engine, 'engine', $view_instance); + //测试模板引擎驱动 + $data = $view_instance->engine(['type' => 'think', 'view_path' => '', 'view_suffix' => '.html', 'view_depr' => DS]); + $think_engine = new \think\view\driver\Think(['view_path' => '', 'view_suffix' => '.html', 'view_depr' => DS]); + $this->assertAttributeEquals($think_engine, 'engine', $view_instance); + } + + public function testReplace() + { + $view_instance = \think\View::instance(); + $view_instance->replace('string', 'replace')->display('string'); + } + +} diff --git a/tests/thinkphp/library/traits/controller/.gitignore b/tests/thinkphp/library/traits/controller/.gitignore deleted file mode 100644 index a3a0c8b5f48c0260a4cb43aa577f9b18896ee280..0000000000000000000000000000000000000000 --- a/tests/thinkphp/library/traits/controller/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -* -!.gitignore \ No newline at end of file diff --git a/tests/thinkphp/library/traits/controller/jumpTest.php b/tests/thinkphp/library/traits/controller/jumpTest.php new file mode 100644 index 0000000000000000000000000000000000000000..47e9dbfe639ebe4118eab6bf1b8abfe95ff1df57 --- /dev/null +++ b/tests/thinkphp/library/traits/controller/jumpTest.php @@ -0,0 +1,339 @@ +testClass = new testClassWithJump(); + $this->request = Request::create(''); + + $this->originServerData = Request::instance()->server(); + } + + public function tearDown() + { + Request::instance()->server($this->originServerData); + } + + /** + * @dataProvider provideTestSuccess + */ + public function testSuccess($arguments, $expected, array $extra) + { + if (isset($extra['server'])) { + $this->request->server($extra['server']); + } + + $mock = $this->getMockBuilder(get_class($this->testClass))->setMethods(['getResponseType'])->getMock(); + $mock->expects($this->any())->method('getResponseType')->willReturn($extra['return']); + + try { + call_user_func_array([$mock, 'success'], $arguments); + $this->setExpectedException('\think\exception\HttpResponseException'); + } catch (\Exception $e) { + $this->assertInstanceOf('\think\exception\HttpResponseException', $e); + + /** @var Response $response */ + $response = $e->getResponse(); + + $this->assertInstanceOf('\Think\Response', $response); + $this->assertEquals($expected['header'], $response->getHeader()); + $this->assertEquals($expected['data'], $response->getData()); + } + } + + /** + * @dataProvider provideTestError + */ + public function testError($arguments, $expected, array $extra) + { + if (isset($extra['server'])) { + $this->request->server($extra['server']); + } + + $mock = $this->getMockBuilder(get_class($this->testClass))->setMethods(['getResponseType'])->getMock(); + $mock->expects($this->any())->method('getResponseType')->willReturn($extra['return']); + + try { + call_user_func_array([$mock, 'error'], $arguments); + $this->setExpectedException('\think\exception\HttpResponseException'); + } catch (\Exception $e) { + $this->assertInstanceOf('\think\exception\HttpResponseException', $e); + + /** @var Response $response */ + $response = $e->getResponse(); + + $this->assertInstanceOf('\Think\Response', $response); + $this->assertEquals($expected['header'], $response->getHeader()); + $this->assertEquals($expected['data'], $response->getData()); + } + } + + /** + * @dataProvider provideTestResult + */ + public function testResult($arguments, $expected, array $extra) + { + if (isset($extra['server'])) { + $this->request->server($extra['server']); + } + + $mock = $this->getMockBuilder(get_class($this->testClass))->setMethods(['getResponseType'])->getMock(); + $mock->expects($this->any())->method('getResponseType')->willReturn($extra['return']); + + try { + call_user_func_array([$mock, 'result'], $arguments); + $this->setExpectedException('\think\exception\HttpResponseException'); + } catch (\Exception $e) { + $this->assertInstanceOf('\think\exception\HttpResponseException', $e); + + /** @var Response $response */ + $response = $e->getResponse(); + + $this->assertInstanceOf('\Think\Response', $response); + $this->assertEquals($expected['header'], $response->getHeader()); + $this->assertEquals($expected['data'], $response->getData()); + } + } + + /** + * @dataProvider provideTestRedirect + */ + public function testRedirect($arguments, $expected) + { + try { + call_user_func_array([$this->testClass, 'redirect'], $arguments); + $this->setExpectedException('\think\exception\HttpResponseException'); + } catch (\Exception $e) { + $this->assertInstanceOf('\think\exception\HttpResponseException', $e); + + /** @var Redirect $response */ + $response = $e->getResponse(); + + $this->assertInstanceOf('\think\response\Redirect', $response); + $this->assertEquals($expected['url'], $response->getTargetUrl()); + $this->assertEquals($expected['code'], $response->getCode()); + } + } + + public function testGetResponseType() + { + Request::instance()->server(['HTTP_X_REQUESTED_WITH' => null]); + $this->assertEquals('html', $this->testClass->getResponseType()); + + Request::instance()->server(['HTTP_X_REQUESTED_WITH' => true]); + $this->assertEquals('html', $this->testClass->getResponseType()); + + Request::instance()->server(['HTTP_X_REQUESTED_WITH' => 'xmlhttprequest']); + $this->assertEquals('json', $this->testClass->getResponseType()); + } + + public function provideTestSuccess() + { + $provideData = []; + + $arguments = ['', null, '', 3, []]; + $expected = [ + 'header' => [ + 'Content-Type' => 'text/html; charset=utf-8' + ], + 'data' => View::instance(Config::get('template'), Config::get('view_replace_str')) + ->fetch(Config::get('dispatch_error_tmpl'), [ + 'code' => 1, + 'msg' => '', + 'data' => '', + 'url' => '/index.php/', + 'wait' => 3, + ]) + ]; + $provideData[] = [$arguments, $expected, ['server' => ['HTTP_REFERER' => null], 'return' => 'html']]; + + $arguments = ['thinkphp', null, ['foo'], 4, ['Power-By' => 'thinkphp', 'Content-Type' => 'text/html; charset=gbk']]; + $expected = [ + 'header' => [ + 'Content-Type' => 'text/html; charset=gbk', + 'Power-By' => 'thinkphp' + ], + 'data' => View::instance(Config::get('template'), Config::get('view_replace_str')) + ->fetch(Config::get('dispatch_error_tmpl'), [ + 'code' => 1, + 'msg' => 'thinkphp', + 'data' => ['foo'], + 'url' => 'http://www.thinkphp.cn', + 'wait' => 4, + ]) + ]; + $provideData[] = [$arguments, $expected, ['server' => ['HTTP_REFERER' => 'http://www.thinkphp.cn'], 'return' => 'html']]; + + $arguments = ['thinkphp', 'index', ['foo'], 5, []]; + $expected = [ + 'header' => [ + 'Content-Type' => 'application/json; charset=utf-8' + ], + 'data' => [ + 'code' => 1, + 'msg' => 'thinkphp', + 'data' => ['foo'], + 'url' => '/index.php/index.html', + 'wait' => 5, + ] + ]; + $provideData[] = [$arguments, $expected, ['server' => ['HTTP_REFERER' => null], 'return' => 'json']]; + + return $provideData; + } + + public function provideTestError() + { + $provideData = []; + + $arguments = ['', null, '', 3, []]; + $expected = [ + 'header' => [ + 'Content-Type' => 'text/html; charset=utf-8' + ], + 'data' => View::instance(Config::get('template'), Config::get('view_replace_str')) + ->fetch(Config::get('dispatch_error_tmpl'), [ + 'code' => 0, + 'msg' => '', + 'data' => '', + 'url' => 'javascript:history.back(-1);', + 'wait' => 3, + ]) + ]; + $provideData[] = [$arguments, $expected, ['return' => 'html']]; + + $arguments = ['thinkphp', 'http://www.thinkphp.cn', ['foo'], 4, ['Power-By' => 'thinkphp', 'Content-Type' => 'text/html; charset=gbk']]; + $expected = [ + 'header' => [ + 'Content-Type' => 'text/html; charset=gbk', + 'Power-By' => 'thinkphp' + ], + 'data' => View::instance(Config::get('template'), Config::get('view_replace_str')) + ->fetch(Config::get('dispatch_error_tmpl'), [ + 'code' => 0, + 'msg' => 'thinkphp', + 'data' => ['foo'], + 'url' => 'http://www.thinkphp.cn', + 'wait' => 4, + ]) + ]; + $provideData[] = [$arguments, $expected, ['return' => 'html']]; + + $arguments = ['thinkphp', '', ['foo'], 5, []]; + $expected = [ + 'header' => [ + 'Content-Type' => 'application/json; charset=utf-8' + ], + 'data' => [ + 'code' => 0, + 'msg' => 'thinkphp', + 'data' => ['foo'], + 'url' => '', + 'wait' => 5, + ] + ]; + $provideData[] = [$arguments, $expected, ['return' => 'json']]; + + return $provideData; + } + + public function provideTestResult() + { + $provideData = []; + + $arguments = [null, 0, '', '', []]; + $expected = [ + 'header' => [ + 'Content-Type' => 'text/html; charset=utf-8' + ], + 'data' => [ + 'code' => 0, + 'msg' => '', + 'time' => Request::create('')->server('REQUEST_TIME'), + 'data' => null, + ] + ]; + $provideData[] = [$arguments, $expected, ['return' => 'html']]; + + $arguments = [['foo'], 200, 'thinkphp', 'json', ['Power-By' => 'thinkphp']]; + $expected = [ + 'header' => [ + 'Power-By' => 'thinkphp', + 'Content-Type' => 'application/json; charset=utf-8' + ], + 'data' => [ + 'code' => 200, + 'msg' => 'thinkphp', + 'time' => 1000, + 'data' => ['foo'], + ] + ]; + + $provideData[] = [$arguments, $expected, ['server' => ['REQUEST_TIME' => 1000], 'return' => 'json']]; + + return $provideData; + } + + public function provideTestRedirect() + { + $provideData = []; + + $arguments = ['', [], 302, []]; + $expected = [ + 'code'=> 302, + 'url' => '/index.php/' + ]; + $provideData[] = [$arguments, $expected, []]; + + $arguments = ['index', 302, null, []]; + $expected = [ + 'code'=> 302, + 'url' => '/index.php/index.html' + ]; + $provideData[] = [$arguments, $expected, []]; + + $arguments = ['http://www.thinkphp.cn', 301, 302, []]; + $expected = [ + 'code'=> 301, + 'url' => 'http://www.thinkphp.cn' + ]; + $provideData[] = [$arguments, $expected, []]; + + return $provideData; + } +} + +class testClassWithJump +{ + use Jump { + success as public; + error as public; + result as public; + redirect as public; + getResponseType as public; + } +} diff --git a/tests/thinkphp/library/traits/model/.gitignore b/tests/thinkphp/library/traits/model/.gitignore deleted file mode 100644 index a3a0c8b5f48c0260a4cb43aa577f9b18896ee280..0000000000000000000000000000000000000000 --- a/tests/thinkphp/library/traits/model/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -* -!.gitignore \ No newline at end of file diff --git a/tests/thinkphp/library/traits/model/softDeleteTest.php b/tests/thinkphp/library/traits/model/softDeleteTest.php new file mode 100644 index 0000000000000000000000000000000000000000..8410efacec7113c2f159c9aef0e121b762e55d59 --- /dev/null +++ b/tests/thinkphp/library/traits/model/softDeleteTest.php @@ -0,0 +1,179 @@ +connection; + + $sql[] = <<execute($one); + } + } + + public function testTrashed() + { + /** @var testClassWithSoftDelete[] $selections */ + $selections = testClassWithSoftDelete::withTrashed()->select(); + + $this->assertFalse($selections[0]->trashed()); + $this->assertTrue($selections[1]->trashed()); + $this->assertTrue($selections[2]->trashed()); + } + + public function testDefaultTrashed() + { + $this->assertCount(3, testClassWithSoftDelete::all()); + } + + public function testWithTrashed() + { + $this->assertCount(5, testClassWithSoftDelete::withTrashed()->select()); + } + + public function testOnlyTrashed() + { + $this->assertCount(2, testClassWithSoftDelete::onlyTrashed()->select()); + } + + public function testSoftDelete() + { + $this->assertEquals(1, testClassWithSoftDelete::get(1)->delete()); + $this->assertNotNull(testClassWithSoftDelete::withTrashed()->find(1)->getData('delete_time')); + } + + public function testForceDelete() + { + $this->assertEquals(1, testClassWithSoftDelete::get(1)->delete(true)); + $this->assertNull(testClassWithSoftDelete::get(1)); + } + + public function testSoftDestroy() + { + $this->assertEquals(5, testClassWithSoftDelete::destroy([1, 2, 3, 4, 5, 6])); + $this->assertNotNull(testClassWithSoftDelete::withTrashed()->find(2)->getData('delete_time')); + $this->assertNotEquals(self::TEST_TIME, testClassWithSoftDelete::withTrashed()->find(2)->getData('delete_time')); + $this->assertNotEquals(self::TEST_TIME, testClassWithSoftDelete::withTrashed()->find(3)->getData('delete_time')); + $this->assertNotNull(testClassWithSoftDelete::withTrashed()->find(4)->getData('delete_time')); + $this->assertNotNull(testClassWithSoftDelete::withTrashed()->find(5)->getData('delete_time')); + } + + public function testForceDestroy() + { + $this->assertEquals(5, testClassWithSoftDelete::destroy([1, 2, 3, 4, 5, 6], true)); + $this->assertNull(testClassWithSoftDelete::withTrashed()->find(1)); + $this->assertNull(testClassWithSoftDelete::withTrashed()->find(2)); + $this->assertNull(testClassWithSoftDelete::withTrashed()->find(3)); + $this->assertNull(testClassWithSoftDelete::withTrashed()->find(4)); + $this->assertNull(testClassWithSoftDelete::withTrashed()->find(5)); + } + + public function testRestore() + { + /** @var testClassWithSoftDelete[] $selections */ + $selections = testClassWithSoftDelete::withTrashed()->select(); + + $this->assertEquals(0, $selections[0]->restore()); + $this->assertEquals(1, $selections[1]->restore()); + $this->assertEquals(1, $selections[2]->restore()); + $this->assertEquals(0, $selections[3]->restore()); + $this->assertEquals(0, $selections[4]->restore()); + + $this->assertNull(testClassWithSoftDelete::withTrashed()->find(1)->getData('delete_time')); + $this->assertNull(testClassWithSoftDelete::withTrashed()->find(2)->getData('delete_time')); + } + + public function testGetDeleteTimeField() + { + $testClass = new testClassWithSoftDelete(); + + $this->assertEquals('delete_time', $testClass->getDeleteTimeField()); + + $testClass->deleteTime = 'create_time'; + $this->assertEquals('create_time', $testClass->getDeleteTimeField()); + + $testClass->deleteTime = 'test.create_time'; + $this->assertEquals('create_time', $testClass->getDeleteTimeField()); + + $testClass->deleteTime = 'create_time'; + $this->assertEquals('__TABLE__.create_time', $testClass->getDeleteTimeField(true)); + } +} + +class testClassWithSoftDelete extends Model +{ + public $table = 'tp_soft_delete'; + + public $deleteTime = 'delete_time'; + + public $connection = [ + // 数据库类型 + 'type' => 'mysql', + // 服务器地址 + 'hostname' => '127.0.0.1', + // 数据库名 + 'database' => 'test', + // 用户名 + 'username' => 'root', + // 密码 + 'password' => '', + // 端口 + 'hostport' => '', + // 连接dsn + 'dsn' => '', + // 数据库连接参数 + 'params' => [], + // 数据库编码默认采用utf8 + 'charset' => 'utf8', + // 数据库表前缀 + 'prefix' => '', + // 数据库调试模式 + 'debug' => true, + // 数据库部署方式:0 集中式(单一服务器),1 分布式(主从服务器) + 'deploy' => 0, + // 数据库读写是否分离 主从式有效 + 'rw_separate' => false, + // 读写分离后 主服务器数量 + 'master_num' => 1, + // 指定从服务器序号 + 'slave_no' => '', + // 是否严格检查字段是否存在 + 'fields_strict' => true, + // 数据集返回类型 array 数组 collection Collection对象 + 'resultset_type' => 'array', + // 是否自动写入时间戳字段 + 'auto_timestamp' => false, + // 是否需要进行SQL性能分析 + 'sql_explain' => false, + ]; + + use SoftDelete { + getDeleteTimeField as public; + } +} diff --git a/tests/thinkphp/library/traits/think/instanceTest.php b/tests/thinkphp/library/traits/think/instanceTest.php new file mode 100644 index 0000000000000000000000000000000000000000..f93f22bb5f1c5fee33c70590af2cb59a139c901d --- /dev/null +++ b/tests/thinkphp/library/traits/think/instanceTest.php @@ -0,0 +1,60 @@ +assertInstanceOf('\tests\thinkphp\library\traits\think\InstanceTestFather', $father); + $this->assertEquals([], $father->options); + + $son = InstanceTestFather::instance(['son']); + $this->assertSame($father, $son); + } + + public function testCallStatic() + { + $father = InstanceTestFather::instance(); + $this->assertEquals([], $father->options); + + $this->assertEquals($father::__protectedStaticFunc(['thinkphp']), 'protectedStaticFunc["thinkphp"]'); + + try { + $father::_protectedStaticFunc(); + $this->setExpectedException('\think\Exception'); + } catch (\Exception $e) { + $this->assertInstanceOf('\think\Exception', $e); + } + } + + protected function tearDown() + { + call_user_func(\Closure::bind(function () { + InstanceTestFather::$instance = null; + }, null, '\tests\thinkphp\library\traits\think\InstanceTestFather')); + } +} + +class InstanceTestFather +{ + use Instance; + + public $options = null; + + public function __construct($options) + { + $this->options = $options; + } + + protected static function _protectedStaticFunc($params) + { + return 'protectedStaticFunc' . json_encode($params); + } +} + +class InstanceTestSon extends InstanceTestFather +{ +} diff --git a/tpl/dispatch_jump.tpl b/tpl/dispatch_jump.tpl index 18ee01bd55638ce56d46f9956576493205696243..583376bbb694e26c82a5852645745f93a0f9fda7 100644 --- a/tpl/dispatch_jump.tpl +++ b/tpl/dispatch_jump.tpl @@ -2,6 +2,7 @@ + 跳转提示