# QPHP **Repository Path**: lbnnbs/qphp ## Basic Information - **Project Name**: QPHP - **Description**: QPHP,一个简单现代的PHP开发框架,让PHP开发回归简单。 - **Primary Language**: PHP - **License**: AGPL-3.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 6 - **Forks**: 2 - **Created**: 2022-05-06 - **Last Updated**: 2025-09-16 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # QPHP ## 版本 v2.0 ## 简介 QPHP,一个简单现代的PHP开发框架,让PHP开发回归简单。支持PHP5.4及以上版本。 ## 特性 1. 采用MVC模式和自动路由,路由格式:/控制器名/动作名\[/参数名/参数值\],如:/index/index/id/1,将路由到Api模块(默认模块)、Index控制器、index方法,get参数为id=1。 2. 支持多应用模块,路由格式:/模块名/控制器名/动作名\[/参数名/参数值\],如:/api/index/index/id/1,将路由到Api模块、Index控制器、index方法,get参数为id=1。 3. 支持自定义路由,自定义路由优先自动路由。详见:[自定义路由](#自定义路由)。 4. 遵循PSR-2命名规范和PSR-4自动加载规范,Core和Libs目录作为核心类库和扩展类库目录使用,用户自定义类库也可以放到Libs目录,会按命名空间自动加载。 5. 内置一个简单的Daemon类库,可以实现后台定时任务。 6. 支持多语言、数据库主从配置,内置图片处理、分页、上传、数据校验、缓存(APCU、文件、Redis)、AES256加解密、鉴权等常用类库和函数。 ## 开发文档 该框架极为简单,代码量很少,任何功能不清楚都建议直接看源码。源码上也都有注释。 ### 安装配置 1. 以Nginx配置为例,假设框架安装在/www目录 2. 配置网站的访问目录到/www/Public 3. 启用路由重写 4. 设置php的可访问目录到/www目录 Nginx配置示例如下: ``` server { server_name www.xxx.com; index index.html index.php; root /www/Public; # 配置网站的访问目录到/www/Public location / { if (!-e $request_filename) { rewrite ^/(.*)$ /index.php/$1 last; # 启用路由重写 break; } } location ~ [^/]\.php(/|$) { fastcgi_pass 127.0.0.1:9000; fastcgi_index index.php; include fastcgi.conf; fastcgi_param PHP_ADMIN_VALUE "open_basedir=/www/:/tmp/:/proc/"; # 设置php的可访问目录到/www目录 include pathinfo.conf; } } ``` ### 目录结构 ``` www 根目录 | +---App 应用目录 | | functions.php 用户自定义函数 | | route.php 自定义路由 | | | +---Api 第1个应用模块,API接口类型的应用可参考该模块 | | | Bootstrap.php 模块启动时自动执行的文件 | | | Config.php 模块级别配置文件,配置项会覆盖Data/Config.php这个主配置文件的配置项 | | | | | \---Controller 控制器目录 | | Base.php 基类控制器 | | Index.php 控制器文件 | | | \---Web 第2个应用模块,Web网页类型的应用可参考该模块 | | Bootstrap.php 模块启动时自动执行的文件 | | Config.php 模块级别配置文件,配置项会覆盖Data/Config.php这个主配置文件的配置项 | | Route.php 模块路由配置文件,由App/route.php文件加载 | | | +---Controller 控制器目录 | | | Base.php 基类控制器 | | | Index.php 控制器文件 | | | Msgbox.php 控制器文件 | | | | | \---Route 控制器类库命名空间 | | Login.php 控制器文件 | | | +---Handle 处理器文件目录,相当于hook或者中间件,一般由路由配置文件调用 | | Cors.php 跨域处理器 | | Limit.php 限流处理器 | | | \---Template 视图模板目录 | +---Index 控制器Index对应的模板目录 | | index.html 控制器Index里的index动作对应的目录文件 | +---Core 框架核心类库目录 | Apcu.php APCU缓存类 | Application.php 应用启动文件,由框架入口文件调用 | arial.ttf 图片验证码使用的字体文件 | Cache.php 文件缓存类 | Controller.php 控制器基类 | Cookie.php Cookie读写类 | Dispatcher.php 调度器,用户输入到浏览器输出的流程都由这个文件统一调度 | Error.php 错误处理类 | Func.php 框架自用函数库 | Hmvc.php HMVC支持类库 | Http.php HTTP常用静态函数库 | Image.php 图片处理常用静态函数库 | Mcrypt.php 加解密常用静态函数库 | Mysql.php MySQL数据库操作类 | Page.php 数据集分页类 | Registry.php 对象仓库类,用于存取全局对象 | Request.php 用户请求处理类 | Route.php 路由处理类 | Session.php Session会话读写类 | Sys.php 框架自用静态函数库 | Template.php 视图模板解析类 | Uploader.php 文件上传处理类 | Verifier.php 数据验证常用静态函数库 | +---Data 数据存储目录 | | Config.php 框架运行配置文件 | | | +---Cache 文件缓存目录,使用文件缓存后会自动生成 | +---Logs 运行日志目录,写入日志时会自动生成 | +---Route 路由缓存目录,使用路由缓存后会自动生成 | \---Template 视图模板编译后文件存储目录,渲染视图后会自动生成 | +---Lang 多语言配置文件存储目录 | zh_cn.php 中文语言配置文件 | +---Libs 用户自定义扩展类库目录 | Auth.php 鉴权类 | TokenAuth.php Token鉴权类 | Daemon.php Daemon后台任务类 | Model.php 数据基础模型类 | RedisSession.php Session存Redis的处理类 | UserModel.php 用户相关表模型类,继承Model类 | +---Public 网站访问根目录 | favicon.ico 网站favicon图标 | index.php 网站入口文件 | \---Work Daemon后台任务目录 sample.php Daemon后台任务示例 ``` ### 运行配置 运行配置文件位置:Data/Config.php,内容如下: ``` /* * 是否显示PHP错误和DB错误 true:显示 false:写入日志 * PHP错误在:/Data/Logs/Php * DB错误在:/Data/Logs/Db * 建议:开发时打开,发布后关闭 */ 'display_error' => true, /* * 允许的PHP错误类型 * 注意:定义在这里的PHP错误类别将不会被显示或写入日志 * 建议:开发时保持数组为空,可以确保程序的健壮性更好;发布可以考虑允许PHP的提示错误,如: array(E_NOTICE, E_DEPRECATED) */ 'allow_error' => array(E_NOTICE, E_DEPRECATED), /* * 是否显示程序异常 true:显示 false:写入日志(目录在:/Data/Logs/App) * 建议:开发时打开,发布后关闭 */ 'display_exception' => true, /* * 设置时区,以免出现时间不正确的情况 */ 'timezone' => 'Asia/Shanghai', /* * 设置编码字符集 */ 'charset' => 'utf-8', /* * 设置身份鉴权 */ 'auth' => array( // 鉴权数据的存储前缀 'prefix' => 'qphp_', // 鉴权数据的存储时长,仅对token方式有效 'expire' => 1440, // 是否单用户登录,仅对token方式有效 'single' => true, ), /* * session设置 */ 'session' => array( // 是否自动启动session,如果是api,建议不要启用 'auto' => true, /* * 设置用户自定义的session处理器,必须实现SessionHandlerInterface * 可填写为RedisSession(框架自带的Session管理器,使用redis),或者留空,或者用户自定义 * 'handler' => 'RedisSession', */ 'handler' => '', // session过期时间,仅供自定义的session处理器使用 'expire' => 1440, ), /* * 模块设置,区分大小写 * 数组索引第一个为默认分组 */ 'modules' => array('Api', 'Web'), /* * 启用子域名绑定,支持泛域名,泛域名只需要写固定域名部分 * 启用后可以直接解析 api.xxx.com 到api模块 * 域名必须小写 */ 'sub_domain_bind' => array( // 'Api' => 'api.xxx.com', // 'Web' => 'www.xxx.com', ), /** * 系统基础加密秘钥,每个项目都应该使用不同的密钥,以便确保安全 */ 'base_secret_key' => '123456', /* * 数据库配置 */ 'database' => array( // 主库配置 'master' => array( 'host' => '127.0.0.1', 'port' => '3306', 'charset' => 'utf8mb4', 'username' => 'test', 'password' => 'test', 'database' => 'test', 'tablepre' => 'tb_', ), // 从库配置 'slave' => array( 'host' => '127.0.0.1', 'port' => '3306', 'charset' => 'utf8mb4', 'username' => 'test', 'password' => 'test', 'database' => 'test', 'tablepre' => 'tb_', ), ), /* * 默认缓存类型 * - file - 文件缓存(默认) * - apcu - APCU缓存 * - redis - Redis缓存 */ 'default_cache_type' => 'file', /* * redis配置 */ 'redis' => array( // 主库配置 'master' => array( 'host' => '127.0.0.1', 'port' => 6379, 'password' => '', 'prefix' => 'qphp:', // 相当于数据库名 ), ), /* * apcu配置 */ 'apcu' => array( 'prefix' => 'qphp:', ), ``` ### 运行流程 对于一个HTTP应用来说,从用户发起请求到响应输出结束,大致的请求流程如下: 1. 请求被nginx路由重写转发到Public/index.php文件; 2. index.php文件定义框架常用目录路径,初始化框架(加载Core目录的核心类库和函数库,以及App/functions.php的用户自定义函数库); 3. 加载主配置文件Data/Config.php里的配置; 4. 加载自定义路由App/route.php,根据自定义路由和自动路由规则,解析出应用模块、控制器、动作、参数; 5. 执行自定义路由中定义的handle处理器,完成控制器执行前的跨域设置、限流等动作; 6. 加载应用模块的配置文件,比如Api模块内的配置文件App/Api/Config.php。该文件的配置项会覆盖Data/Config.php这个主配置文件的配置项; 7. 执行应用模块的启动文件里的init()方法,完成应用模块的初始化工作,比如Api模块内的启动文件App/Api/Bootstrap.php; 8. 实例化控制器,比如Api/Controller/Index.php; 9. 根据配置文件的设置,如果有设置Session的处理器,则通过session_set_save_handler()函数将Session交由设置的处理器处理; 10. 根据配置文件的设置,决定是否自动开启Session; 11. 执行控制器的init()方法,完成控制器的初始化动作; 12. 执行控制器里的动作方法,比如Api/Controller/Index.php控制器里的index()方法; 13. 加载控制器和动作对应的视图模板文件,比如Api/Template/Index/index.html,完成视图的渲染并输出给用户; 至此,请求流程结束。 ### 路由 路由通俗的讲就是指访问路径,也就是网址。 #### 自动路由 1. 路由格式:/模块名/控制器名/动作名\[/参数名/参数值\]...,如:/api/index/index/id/1,将路由到Api模块、Index控制器、index方法,get参数为id=1。 2. 如果省略了模块名,会自动路由到默认模块(默认模块设置请参考 [运行配置](#运行配置))。路由格式:/控制器名/动作名\[/参数名/参数值\],如:/index/index/id/1,将路由到Api模块(默认模块)、Index控制器、index方法,get参数为id=1。 3. 也可以使用普通的参数名和参数值方式,比如:/api/index/index?id=1 #### 自定义路由 框架启动是会自动加载App/route.php文件,用户自定义路由配置写入到该文件即可。 1. 设置单条路由 ``` Route::set( array( // 请求地址,(<>)包裹的是参数绑定的参数名,和bind里面指定的参数名对应 'uri' => '/web/user/login/reg/username/()/password/()', // 参数绑定,和uri里指定的参数名对应,这里指定参数值的匹配方式,使用正则表达式进行匹配 'bind' => array('username' => '\d+', 'password' => '.*'), // 额外传递给控制器动作的参数 'param' => array('aaa' => 'xxx'), // 指定模块 'module' => 'Web', // 指定控制器 'controller' => 'User\\Login', // 指定动作 'action' => 'reg', // 命名路由,后面如果有相同命名的路由配置,将会覆盖该条路由配置 'name' => 'user/login/reg', // 指定控制器初始化前需要执行的动作,会自动依次执行对应类里面的handle()方法 'before' => array('\App\Web\Handle\Limit', '\App\Web\Handle\Cors'), ) ); ``` 2. 一次设置多条路由 ``` Route::set( // 注意这里是个数组 array( // 第一条 array( 'uri' => '/web/user/login/reg/username/()/password/()', 'bind' => array('username' => '\d+', 'password' => '.*'), 'param' => array('title' => '新用户注册'), 'module' => 'Web', 'controller' => 'User\\Login', 'action' => 'reg', 'name' => 'user/login/reg', // 在进入模块并执行控制器里的活动之前执行其他操作,比如:限流 'before' => array('\App\Web\Handle\Limit'), ), // 第二条 array( 'uri' => '/web/user/login/recover', 'module' => 'Web', 'controller' => 'User\\Login', 'action' => 'recover', ) ) ); ``` 3. 路由分组,路由分组会指定将分组前缀和分组内路由的各项配置拼接在一起形成最终的路由。 ``` Route::group( // 路由分组的分组前缀设置 array( 'uri' => '/web/route', 'controller' => 'Route', 'module' => 'Web', 'name' => 'route', ), // 分组内的多条路由 array( // 第1条路由 array( 'uri' => '/login/logout', 'controller' => 'Login', 'action' => 'logout', 'name' => 'login/logout', ), // 路由分组内嵌套路由分组 Route::group(array('uri' => '/login', 'controller' => 'Login', 'name' => 'login'), array( // 第2条路由 array( 'uri' => '/login/()/password/()', 'bind' => array('username' => '.*', 'password' => '.*'), 'action' => 'login', 'name' => 'login', // 在进入模块并执行控制器里的活动之前执行其他操作,比如:限流 'before' => array('\App\Web\Handle\Limit'), ), )) ) ); ``` 上面的路由最终生成的路由表如下: ``` Array ( [user/login/logout] => Array ( [uri] => /web/user/login/logout [controller] => User\Login [action] => logout [name] => user/login/logout [module] => Web ) [user/login/login] => Array ( [uri] => /web/user/login/login/()/password/() [bind] => Array ( [uid] => .* [password] => .* ) [action] => login [name] => user/login/login [controller] => User\Login [before] => Array ( [0] => \App\Web\Handle\Limit ) [module] => Web ) ) ``` 4. 路由缓存。如果项目较大,自定义路由很多,那么每次都需要解析路由配置文件会比较耗时。 此时可以使用路由好处功能讲生成的路由表缓存起来。缓存文件位于Data/Route目录, 如果需要更新了路由配置,记得清空该目录以便生成新的路由缓存。 ``` if (!$route = Route::cache('web')) { $route = array( 'uri' => '/web/user/login/reg', 'module' => 'Web', 'controller' => 'User\\Login', 'action' => 'reg', ); Route::cache('web', $route); } Route::set($route); ``` 5. 按请求路径前缀分开加载路由。如下为如果请求路径是以/web开头的,则加载Web/Route.php这个路由配置文件。 ``` if (Route::prematch('/web')) { include 'Web/Route.php'; } ``` 6. 读取路由。可以使用Route::get()读取路由配置。使用Route::get('路由名')读取已命名的路由。 7. 生成路由。在任何地方都可以通过\Sys::url()函数生成路由,如果在控制器内,也可以使用控制器的createUrl()函数生成路由。2个方法的参数和返回结果完全一样。实际createUrl就是调用了\Sys::url()。格式如下: \Sys::url(控制器, 动作, 参数字符串或数组, 是否把参数转换为斜杠分割模式, 模块名) ### 请求 #### 请求参数 请求参数分为3类,分别为post、get和param。其中: get参数是指在请求地址中,问号后面的参数。 post参数是指通过post方式发送请求时,在请求体中的参数。 param参数是指通过自动路由或者手工设置路由设置的参数。 在任何地方都可以通过 \Request::getInstance() 获取一个请求实例。获取请求参数可以用如下方法: 获取post、get、param参数,优先级为 post > get > param: \Request::getInstance()->get('参数名') 获取get参数: \Request::getInstance()->getGet('参数名') 获取param参数: \Request::getInstance()->getParam('参数名') 获取post参数: \Request::getInstance()->getPost('参数名') 获取上传文件: \Request::getInstance()->files('参数名') 获取header参数: \Request::getInstance()->header('参数名') 获取原始未处理的参数: \Request::getInstance()->rawParam('参数名') 获取命令行参数: \Request::getInstance()->argv('参数名') 所有方法的参数名部分都是可选的,如果不传递参数名,将返回所有参数的数组。 获取post请求的原始请求体内容: \Request::getInstance()->rawBody() #### 获取请求路径 \Request::getInstance()->currentUrl() 请求路径不带域名带问号后的参数 \Request::getInstance()->requestUri() 请求路径带域名带问号后的参数 \Request::getInstance()->requestPath() 请求路径带域名不带问号后的参数 \Request::getInstance()->serverLocation() 请求域名带端口号 \Request::getInstance()->serverPort() 请求端口号 \Request::getInstance()->isHttps() 是否https协议 \Request::getInstance()->protocol() 获取请求协议 \Request::getInstance()->frontUrl() 获取来源地址 \Request::getInstance()->getClientIp() 获取客户端IP #### 判断请求类型 \Request::getInstance()->isPost() 是否post请求 \Request::getInstance()->isAjax() 是否ajax请求 ### 控制器 #### 控制器文件路径和路由规范 控制器位于App\模块\Controller目录内,可以分多层级子目录,但目录和控制器命名应遵循PSR-4自动加载规范。 如果控制器只是位于Controller目录而非Controller内的子目录,则可以使用自动路由,否则需要手动配置路由才能访问的控制器。 自动路由,如:/api/index/index/id/1,将路由到Api模块、Index控制器、index方法,get参数为id=1。对应的控制器路径为:\App\Api\Controller\Index.php 手工配置路由,如:/web/user/login/logout,设置路由如下: ``` Route::set(array( 'uri' => '/web/user/login/logout', 'controller' => 'User\\Login', 'action' => 'logout', 'module' => 'Web', )); ``` 将路由到目标控制器路径为:\App\Web\Controller\User\Login.php,执行Login.php里面的logout()方法。 #### 生成路由和路由跳转 控制器内和路由有关的方法: **createUrl** 生成路由。createUrl(控制器, 动作, 参数字符串或数组, 是否把参数转换为斜杠分割模式, 模块名) 该方法将直接调用\Sys::url()方法,可以理解为只是\Sys::url()方法的一个别名。 **location** 跳转到路由地址,location($url, $client = false, $top = false)。 **参数1:** 跳转目的地址。 **参数2:** 是否在客户端浏览器跳转,默认是服务器端跳转。 **参数3:** 如果页面是iframe的子页面,跳转时是否应该在根页面跳转(调用top.location.replace)。默认只在当前页面跳转(调用location.replace)。 **gotoUrl** 生成并跳转到路由地址,实际就是先调用createUrl(),然后调用location()。 #### 获取请求参数 在控制器内部有一个request变量存储了请求实例。可以通过 $this->request 访问。具体使用参考: [请求](#请求) #### 渲染视图 控制器内的方法执行后,默认会自动渲染与方法名的同名的视图文件并作为响应输出给用户。视图文件的路径与控制器方法的对应关系如下; \App\Web\Controller\User\Login 的 reg() 方法 对应的视图文件为: \App\Web\Template\User\Login\reg.html **关闭自动渲染** 如果不需要自动渲染,可以调用控制器内的setViewAutoRender($bool)方法来控制。 如果需要变更默认的视图文件,可以调用控制器内的display($filename = '')方法来更换。比如,在reg()方法内调用: $this->diaplay('reg1'); 对应的视图文件就变为了: \App\Web\Template\User\Login\reg1.html **变更视图文件的路径。** 如果需要变更视图文件的路径,可以调用控制器内的setViewPath($path)方法($path为相对于\App\模块名\Template目录的相对路径)。这个功能常用于在PC端和移动端分别使用不同的页面。比如: ``` if (移动端) { $this->setViewPath('mobile'); } else { $this->setViewPath('web'); } ``` 那么在移动端访问时,视图文件路径为: \App\Web\Template\mobile\User\Login\reg.html 在PC端访问时,视图文件路径为; \App\Web\Template\web\User\Login\reg.html **获取视图内容。** 如果需要获取视图内容,可以盗用render($filename = '')方法。render方法会调用display方法对页面进行渲染并返回渲染后的页面代码字符串。但不会作为响应输出给用户。 **视图渲染引擎。** 视图文件需要渲染引擎渲染后才能生成最终的html代码,视图文件的代码和编写需要遵循渲染引擎的语法规则。具体请参考:[模板引擎](#模板引擎) #### 输出json 在控制器内调用json($code, $msg)方法可以输出json格式的响应。比如: $this->json(200, '欢迎'),将输出json为: ``` { code: 200, msg: '欢迎' } ``` $msg也可以是数组,比如: $this->json(200, ['msg' => '欢迎', 'data' => '这是其他数据']),将输出json为: ``` { code: 200, msg: '欢迎', data: '这是其他数据' } ``` 该方法还有2个简易方法,分别为: succ($msg = '操作成功'),该方法调用json()方法,默认code为1,默认输出消息为‘操作成功’。比如:$this->succ(); 和 fail($msg = '操作失败'),该方法调用json()方法,默认code为-1,默认输出消息为‘操作失败’。比如:$this->fail(); **注意:** json输出时,框架会自动添加跨域处理代码。对于小于99999999999(11位)的文本格式数字会自动转换为数字格式。 #### 浏览器页面缓存 **enablePageCache($bool)** 方法可以设置是否启用浏览器的页面缓存功能,默认不启用。 使用页面缓存可以在浏览器刷新页面,或者页面后退时保留表单数据,并在后退时避免出现“网页已过期”问题。 但同样的,使用页面缓存会导致页面在刷新或后退时不会重新从服务器获取页面,导致页面不是最新的。 一般只在有表单的页面启用页面缓存,防止刷新页面导致的表单上已经填写的数据丢失,影响用户体验。 #### 分页 _createPager(记录条数, 附加到url里的参数数组, 分页器参数),其中分页器支持的参数和默认值如下: ``` [ // 每页显示多少行记录 'perpage' => 10, // 显示多少个分页导航按钮 'navnum' => 10, // 页号用的参数名,如果与现有的参数有冲突可以改变这个值 'tagname' => 'page', // '上一页' 显示的字符 'prev' => '‹‹', // '下一页' 显示的字符 'next' => '››', ] ``` 比如在\App\Web\Controller\Index控制器的index()方法内调用 $this->_createPager(20, ['aaa' => 111]),输出的数据为: ``` Array ( [pagenum] => 2 // 总页数 [curpage] => 1 // 当前页面 [urls] => Array ( [next] => Array ( [text] => › // 上一页文本 [url] => /web/index/index?aaa=111&page=2 // 上一页链接 ) [pages] => Array ( [0] => Array ( [text] => 1 // 第1页文本 [url] => javascript:void(0); // 第1页链接 ) [1] => Array ( [text] => 2 // 第2页文本 [url] => /web/index/index?aaa=111&page=2 // 第2页链接 ) ) ) [offset] => 0 // 当前页面起始索引,用于设置数据库分页的起始记录偏移量 [length] => 10 // 当前页面总记录数量,用于设置数据库分页的读取记录条数 [url] => /web/index/index?aaa=111 // 不含页号参数的url地址 [count] => 20 // 总记录条数 ) ``` **请求参数保留** 当分页用户管理后台时,通常都会配合数据查询的参数一起使用。 比如订单管理页面,会查询某个时间段内的订单,并且因为订单多,需要分页。 那么在生成的分页链接上就需要带上时间段参数,否则点击到其他页时就会因为丢失时间段参数导致输出的结构不正确。 框架在这方面做了自动处理,只要提交到参数是以 filter_ 开头的,都会自动附加到生成的分页页面链接上。 比如在\App\Web\Controller\Index控制器的index()方法内调用 $this->_createPager(20, ['aaa' => 111]),但添加了请求参数 filter_bbb=222。 访问地址为:/web/index/index?filter_bbb=222,那么生成分页数据为: ``` Array ( [pagenum] => 2 [curpage] => 1 [urls] => Array ( [next] => Array ( [text] => › [url] => /web/index/index?filter_bbb=222&aaa=111&page=2 ) [pages] => Array ( [0] => Array ( [text] => 1 [url] => javascript:void(0); ) [1] => Array ( [text] => 2 [url] => /web/index/index?filter_bbb=222&aaa=111&page=2 ) ) ) [offset] => 0 [length] => 10 [url] => /web/index/index?filter_bbb=222&aaa=111 [count] => 20 ) ``` 并且filter_bbb参数还会自动注入到视图文件的变量里面,这样在视图文件上就可以通过echo这个变量来实现查询参数的保留。举例页面结构如下: ```
``` 如果我们在表单的filter_bbb文本框内填写222,表单提交后,返回的页面就会是: ```
``` 加上上面说到的分页链接也会自动注入filter_bbb=222这个参数,就实现了带查询的数据分页,并在分页的各页面间保留查询参数。 #### 向视图文件注入变量 在控制中想视图文件注入变量可以使用assign方法,也可以直接使用匿名变量。比如: ``` $this->assign('变量名', '变量值'); $this->assign(array( '变量1' => '值1', '变量2' => '值2', )); 或者 $this->变量名 = '变量值'; ``` 比如:$this->bbb = 222; 可以直接把变量bbb注入到视图文件。但需要注意的是,当前控制器内没有声明过$bbb这个变量。 否则无法注入bbb到视图,只是单纯的给控制器内的$bbb变量赋值,因为$this->变量名 = '变量值';这种形式的注入使用的是PHP内置的__set()魔法函数。 当然这也导致我们可以使用__get()魔法函数读取这个视图变量。echo $this->bbb; 会显示222。 #### 提示页面 只要调用succ()或者fail()方法,都可以给用户返回一个带倒计时和确定按钮的提示页面。 ``` succ($msg = '操作成功', $url = '', $backstep = 0) fail($msg = '操作失败', $url = '', $backstep = 0) $msg 参数是提示语 $url 参数是提示页面倒计时停止后,或用户点击确定按钮后,自动跳转的页面地址 $backstep 参数是页面后退的步数,指定该参数后,将忽略$url参数,并在提示页面上调用js的history.go(-步数)来实现页面后退。 ``` **注意:** 该用法必须存在Msgbox控制器和对应的视图文件,请参考:\App\Web\Controller\Msgbox.php 和 \App\Web\Template\Msgbox\index.html ### 模板引擎 框架的视图文件可以直接使用php代码,也可以使用模板语法。模板语法会被模板引擎解析,最终还是生成使用php代码的视图文件,并保存在/Data/Template目录。 模板语法如下: ``` 输出变量:{$var}或者{$arr['key']['key2']} 输出PHP:{:...} 等同于 ,可用于输出函数结果: {:trim($a) . 'bb'} 等同于 条件判断:{if $do == '1'}...{elseif $op['key'] == '2'}...{else}...{/if} 循环输出:{for $i 0 50}...{/for} 等同于 数组循环输出:{loop $list $item}...{$item['key']}...{/loop} 数组循环输出带下标:{loop $list $key $item}...{$key}...{$item['key']}...{/loop} 使用PHP:{php $var = 1;} 包含文件:{template '../header'} 相对于当前文件路径的文件名,不需要带文件后缀,如果是以/开头,表示是一个以模板文件目录作为根目录的绝对路径 包含文件带变量,支持变量赋值:{template '../header' a =1 c = 2 k= 'aa' f="me" a=$b} 输出花括号:{##...##} 输出文本或常量:{...} 输出注释: 原样输出:{literal} ... {/literal} 将直接输出 ... 的内容 ``` 需要注意的是,模板内使用{template 子模板}包含子模板文件时,子模板文件和当前模板是一起共享视图的变量的,并不存在子模板变量有单独作用域的情况。如果需要有单独的作用域,建议使用HMVC。 **用法示例** ``` 控制器内注入变量 $this->assign('bbb', 222); 视图文件输出变量
{$bbb}
最终生成的视图文件内容
用户看到的响应内容
222
``` ### HMVC HMVC实现了在控制器的直接调用,而不需要通过用户请求和路由。如果需要调用一个控制器,并获取控制器输出,可以使用Hmvc::load()。使用方法: ``` Hmvc::load(控制器, 动作, 参数) ``` 被调用的控制器内同样会获得当前请求的实例,因此也可以在被控制器内使用$this->request->get()或者\Request::getInstance()->get()获取请求参数。 ### 数据库操作 数据库操作都由Db()函数以及/Core/Mysql.php文件内的Mysql类库完成。 #### 数据库连接 Db()函数建立到数据库的连接,并返回一个Mysql类的实例。 Db()函数会默认使用/Data/Config.php文件内database节点中master子节点里的配置建立数据库连接。 如果需要主从,可以增加配置到database节点,比如增加从服务器节点名为slave,使用Db('slave')即可建立到从服务器的链接。 具体可参考[运行配置](#运行配置)里面数据库两个配置项。 框架内的Mysql类库在执行sql时会自动处理数据库连接中断重连的问题,所以可以放心的用于后台执行的任务。 **注意:** 如果同时使用Db() 和 Db('slave'),将建立2个Mysql实例和2个到MySQL服务器的连接。 这在我们需要同时使用带缓存和不带缓存的方式读取和更新数据时会非常有用。因为不带缓存方式开启后,在关闭游标前,就不能更新数据了。 此时我们可以使用另外一个Mysql实例和连接来更新数据。详见:[游标](#游标) #### 查询数据 ##### fetch系列函数 Mysql类库内置了fetch系列函数,用于查询数据: ``` fetchAll($sql, $bind = array()) //执行sql查询,返回符合条件的记录集(多条记录的二维数组) fetchAssoc($sql, $bind = array()) //执行sql查询,返回符合条件的记录集的关联数组(多条记录的二维数组,第一维数组以返回数据集的第一个字段为下标) fetchRow($sql, $bind = array()) //执行sql查询,返回符合条件的单条记录(返回单条记录的二维数组) fetchCol($sql, $bind = array()) //执行sql查询,返回符合条件的一列数据(返回单列数据的一维数组) fetchOne($sql, $bind = array()) //执行sql查询,返回单条记录的单个字段(返回一个值) ``` 参数$bind是一个数组,用户为$sql中的参数提供数据绑定,比如: ``` fetchAll('select * from table where id > :id', array('id' => 1)) ``` 实际执行的SQL语句就是:select * from table where id > 1,并且使用参数绑定还会自动处理字符转义,避免sql注入攻击。 ##### 查询构造器 fetch系列函数需要自己编写sql语句。get系列函数会返回查询构造器,并最终调用对应的fetch系列函数: ``` getAll($table = '') //参数为表名,最终调用fetchAll() getAssoc($table = '') //参数为表名,最终调用fetchAssoc() getRow($table = '') //参数为表名,最终调用fetchRow(),该函数会自动为sql语句添加limit 1语句 getCol($table = '') //参数为表名,最终调用fetchCol() getOne($table = '') //参数为表名,最终调用fetchOne(),该函数也会自动为sql语句添加limit 1语句 count($table = '') //参数为表名,调用getOne()->fields('COUNT(*)'),返回符合条件的记录条数 ``` count()函数虽然不是以get开头,但也返回查询构造器,所以放到一起说明。 查询构造器支持级联操作,有如下几个构造器函数: ``` table() // 设置表名,参数是字符串 fields() // 设置查询字段,参数可以是字符串或者数组,比如:'name, type, count(*)' 或者 array('name', 'type', 'count(*)') group() // 设置分组,参数是要分组的字段组成的字符串,比如:'name, type' having() // 设置分组后的行为,参数是having语句字符串,比如:name <> 'aaa' order() // 设置排序,参数可以是字符串或者数组,比如:'type desc, name asc' 或者 array('type'=> 'desc', 'name' => 'asc') limit($offset = null, $length = null) // 设置返回记录集的范围。如果只传$offset参数,就只限制返回的记录条数。如果传了2个参数,就返回从$offset位置开始的$length条的记录。比如:limit(10) 返回10条记录,limit(10, 5) 返回从第10条记录开始的5条记录 where() // 设置且条件,参数可以是字符串或者数组,比如:'id > 1' 或者 array('id' => array('>' => 1)) whereOr() // 设置或条件,参数和where()函数一样 when($condition, $callback = null, $elseCallback = null) // 按条件修改构造器 result() // 执行sql返回结果,这个函数最后调用的 ``` 举例如下: ``` Db()->getAll() ->table('table1') ->where(array( 'id' => array('>' => 1), )) ->where(array( 'id' => array('<' => 10), )) ->order(array( 'type' => 'desc', 'name' => 'asc', )) ->fields(array('name', 'type', 'count(*)')) ->group('name, type') ->having("name <> 'aaa'") ->limit(2, 5) ->result(); ``` 生成的sql语句为: ``` select name, type, count(*) from table1 where 1 and id > 1 and id < 10 group by name, type having name <> 'aaa' order by type desc, name asc limit 2, 5 ``` ##### where条件 where函数的条件格式较为灵活,下面把各类用法举例说明: ``` $where = array('id' => 1, 'name' => 'lbnnbs'); 解析后条件为: "id=1 AND name='lbnnbs'" $where = array('id'=>array('>=' => 1), 'name'=>array('like' => '%lbnnbs%')); 解析后条件为: "id>=1 AND name LIKE '%lbnnbs%'" $where = array('id'=>array('>=' => 1), 'name'=>array('not like' => '%lbnnbs%')); 解析后条件为: "id>=1 AND name NOT LIKE '%lbnnbs%'" $where = array('id'=>array('is' => null), 'name'=>array('is not' => null)); 解析后条件为: "id IS null AND name IS NOT null" $where = array('id' => array('between' => '6', 'and' => '7')); 解析后条件为: "id between '6' AND '7'" $where = array('id' => array('between' => 1, 'and' => 2)); 解析后条件为: "id BETWEEN 1 AND 2" $where = array('id' => array('in' => array(1, 2, 3, 4)); 解析后条件为: "id IN (1, 2, 3, 4)" $where = array('id' => array('in' => '1, 2, 3, 4'); 解析后条件为: "id IN (1, 2, 3, 4)" ``` when函数用于按条件修改查询构造器,使用较难理解,举例说明如下: ``` Db()->getAll('table1')->when( $a < 2, function($b){ // 调整为真时执行,$b 就是查询构造器本身,所以可以使用所有构造器可用的函数,比如fields, where, order, group等 $b->fields('aaa') }, function($b){ // 调整为假时执行 $b->fields('bbb') })->result(); ``` 如果$a < 2, 那么返回的sql语句为:select aaa from table1 如果$a >= 2, 那么返回的sql语句为:select bbb from table1 **表名前缀** Db()函数会自动读取框架数据库配置里的tablepre设置,get系列函数使用的表名,会自动在前面附加这个表名前缀。 比如下面的配置: ``` array( 'host' => '127.0.0.1', 'port' => '3306', 'charset' => 'utf8mb4', 'username' => 'test', 'password' => 'test', 'database' => 'test', 'tablepre' => 'tb_', ) ``` 使用 ``` Db()->getAll('user')->result(); ``` 生成的sql为: ``` select * from tb_user; ``` **注意** fetch系列函数不会自动添加表名前缀,但可以使用Db()->tablename()来生成带表名前缀的表名。 #### 执行SQL 执行sql,返回执行是否成功 ``` execute($sql, $bind array(), $unbuffer = false) ``` #### 更新数据 更新符合条件的记录,返回执行是否成功 ``` update($table, $arr, $where = '', $order = '', $limit = '', $group = '') ``` 示例: ``` Db()->update('table1', array('name' => 'aaa'), array('id' => 1)); ``` 生成的sql为: ``` update tb_table1 set name = 'aaa' where id = 1 ``` update函数也会自动添加表名前缀。where的用法详见:[where条件](#where条件) #### 插入数据 插入记录,返回执行是否成功 ``` insert($table, $arr, $replaceInto = false) ``` 示例: ``` Db()->insert('table1', array('name' => 'aaa', 'type' => 111)); ``` 生成的sql为: ``` insert into tb_table1 (name, type) values ('aaa', 111) ``` 如果$replaceInto为true,则如下: ``` Db()->insert('table1', array('name' => 'aaa', 'type' => 111), true); ``` 生成的sql为: ``` replace into tb_table1 (name, type) values ('aaa', 111) ``` insert函数也会自动添加表名前缀。 #### 删除数据 删除符合条件的记录,返回执行是否成功 ``` delete($table, $where, $order = '', $limit = '', $group = '') ``` 示例: ``` Db()->delete('table1', array('id' => 1)); ``` 生成的sql为: ``` delete from tb_table1 where id = 1 ``` delete函数也会自动添加表名前缀。where的用法详见:[where条件](#where条件) #### 游标 sql执行的时候会把记录缓存到内存,如果是读取大量的数据,可能会导致内存不足报错。 此时我们可以使用游标,读一条记录处理一条记录。 **注意:** 如果需要在使用游标读取数据的时候还需要更新数据,就需要使用Db()建立2个数据库连接。 因为不带缓存方式开启后,在关闭游标前,就不能更新数据了。此时我们可以使用另外一个Mysql实例和连接来更新数据。 步骤如下: 1、使用Db('slave')建立第1个数据库连接 2、首选使用execute()函数执行sql,并设置函数的第3个参数为true,表示不使用数据库缓存。 3、然后使用cursor()函数获取游标。 4、再使用mysqli_fetch_array等函数逐条读取记录 5、使用Db()建立第2个数据库连接来更新数据,避免报错 6、使用mysqli_free_result()关闭游标 代码示例: ``` if (Db('slave')->execute($sql, $bind, true)) { // 执行sql,不使用数据库缓存 $cursor = Db('slave')->cursor(); // 获取游标 while ($row = \mysqli_fetch_array($cursor, \MYSQLI_ASSOC)) { // 使用游标逐条读取记录 print_r($row); // 处理记录 Db()->update(); // 使用另外一个Mysql实例和数据库连接更新数据,否则会报错 } mysqli_free_result($cursor); // 关闭游标 } ``` #### 事务 开启事务: beginTransaction($isolation = '') // 参数为隔离级别(选填) 回滚事务: rollBack($dec_transaction_counter = true) // 参数为是否减少事务计数器(选填,默认会减少) 提交事务: commit($dec_transaction_counter = true) // 参数为是否减少事务计数器(选填,默认会减少) 使用示例: ``` function trans() { // 带事务处理的函数 $result = Db()->beginTransaction(); // 开启事务 if (!$result) { // 如果开启事务失败 return false; // 函数返回失败 } // 执行更新语句1 $result = Db()->update('table1', array('name' => 'aaa'), array('id' => 1)); // 如果执行语句失败,或者被更新的记录条数为0,都认定为更新失败 if (!$result || Db()->affectedRows() == 0) { Db()->rollBack(); // 回滚事务 return false; // 函数返回失败 } // 执行更新语句2 $result = Db()->update('table1', array('name' => 'bbb'), array('id' => 1)); // 如果执行语句失败,或者被更新的记录条数为0,都认定为更新失败 if (!$result || Db()->affectedRows() == 0) { // Db()->rollBack(); // 回滚事务 return false; // 函数返回失败 } return Db()->commit(); // 提交事务,函数返回事务执行是否成功 } ``` **事务支持嵌套** 框架内建了一个事务计数器,在开启事务时会累加计数器,在 **回滚** 或者 **提交** 事务时会减少计数器。 如果在使用Db()->commit()提交事务后,计时器为0,才会真正提交并关闭事务。否则只会减少计数器。不会真正提交事务。 代码示例: ``` function trans1() { Db()->beginTransaction(); // 开启事务,事务计数器加1 Db()->update(); // 执行更新 function trans2() { Db()->beginTransaction(); // 开启事务,事务计数器加1,此时事务计数器为2 if (!Db()->delete()) { // 执行删除 Db()->rollBack(); // 回滚删除操作,事务计数器减1,此时事务计数器为1,不会提交事务 return false; } Db()->commint(); // 提交事务,事务计数器减1,此时事务计数器为1,不会提交事务 } if (!trans2()) { // 调用带事务的函数(形成了事务嵌套) Db()->rollBack(); // 回滚更新操作,事务计数器减1,此时事务计数器为0,回滚后关闭事务 return false; } return Db()->commit(); // 提交事务,事务计数器减1,此时事务计数器为0,会提交事务 } ``` #### 其他常用数据库函数 使用时可直接用Db()调用,比如:Db()->getLastSQL(); ``` tablename($table) // 返回带表名前缀的完整表名 affectedRows() // 返回sql执行后影响的行数 getLastSQL() // 返回最后执行的sql语句 lastInsertId() // 返回插入记录后的表自增id rowCount() // 返回查询语句获得的记录条数,类似count()的效果 clear($table) // 清空(截断)表,调用truncate metaColumnNames($table) // 返回指定表的所有字段名 metaDatabases() // 返回服务器中所有数据库名 metaTables() // 返回数据库中所有表名 optimize($table = '') // 优化表,压缩表空间占用,如果不传表名参数,则优化当前数据库中所有表 repair($table = '') // 修复表,如果不传表名参数,则修复当前数据库中所有表 escape($string) // 转移不安全的字符,用于防范sql注入攻击 close() // 关闭数据库连接 ``` ### 基础数据模型 框架提供了一个基础数据模型,只要继承Model类(位于\Libs\Model.php),即可获得常用的数据操作对象。 下面以为用户表创建一个数据模型来举例说明: ``` class User extends \Model { static public $table = 'user'; // 表名,不带表名前缀 protected $_id = 'id'; // 主键id protected $_sort = 'sort'; // 排序字段 /* * 设置修改的字段和字段的数据类型,设置后调用模型内置方法时会自动进行数据校验 * 可用数据类型有:int、str、date、time、datetime、float */ protected $_fillable = array( 'nickname' => 'str', 'gender' => 'int', 'birthday' => 'date', 'create_at' => 'datetime', ); } ``` 基础模型具有下面的可用方法: **实例化** 创建模型实例 ``` $user = new User(); ``` **表名** 静态方法,获取不带表名前缀的表名 ``` User::$table // 返回表名 user ``` **table()** 静态方法,返回带表名前缀的完整的表名 ``` User::table() // 返回表名 tb_user(tb_是框架配置文件设置的表名前缀) ``` **add($arr)** 插入记录,返回自增id ``` $user->add(array( 'nickname' => 'aaa', 'gender' => '20', 'birthday' => '2000-01-01', 'create_at' => '2022-01-01 08:00:00', )) ``` **注意:** 表中必须有自增ID字段,如果没有自增ID字段,返回的是上一个有自增ID的插入语句的自增ID。碰到确实没有自增ID的表,可以在模型内重写基础模型的add()方法,或者直接使用Db()->inset()。 **update($id, $arr)** 按ID更新记录,返回执行是否成功 ``` $user->update(1, array( 'nickname' => 'bbb', 'gender' => '25', )) ``` **remove($id)** 按ID删除记录,返回执行是否成功 ``` $user->remove(1) ``` **get($id, $field = '')** 按ID读取单条记录,返回记录结果 ``` $user->get(1) // 返回id为1的单条记录的一维数组 $user->get(1, 'nickname') // 返回id为1的记录的nickname字段的值 ``` **getc($id, $field = '', $expire = 10)** 按ID读取单条记录,返回记录结果,该函数会缓存记录10秒,10秒内再次读取会直接从缓存内取出。具体使用的缓存函数有赖于框架配置文件内设置的默认缓存方式。参考:[缓存](#缓存)。 ``` $user->getc(1) // 返回id为1的单条记录的一维数组 $user->getc(1, 'nickname') // 返回id为1的记录的nickname字段的值 ``` **getAll($offset = null, $length = null, $filter = array(), $order = '')** 读取符合条件的多条记录 ``` $user->getAll(10, 5, array('id' => array('>' => 2)), 'id desc') // 返回id大于2,且按id倒序排的记录集,从第10条记录开始,共5条记录 ``` **countAll($filter = array())** 统计符合条件的记录条数 ``` $user->countAll(array('id' => array('>' => 2))) // 统计id大于2的记录条数 ``` **exportAll($filter = array())** 读取符合条件的多条记录,没有分页和排序,是getAll的简化版,常用于导出数据 ``` $user->getAll(array('id' => array('>' => 2))) // 读取id大于2的记录集 ``` **setSort($id, $sort)** 设置记录的排序值,该方法只在数据表含有排序字段时有效。通过模型的$_sort属性指定排序字段 ``` $user->setSort(1, 10) // 设置id为1的记录的sort字段的值为10 ``` 另外,框架还提供了一个用户基础模型,用于和用户有关的表,增加了和用户有关的操作方法。 该模型是个trait,所以只需要在需要集成用户模型的模型内use一下即可。但需要指定uid字段的字段名(不指定的话默认为uid) 下面以为用户的收货地址表创建一个数据模型来举例说明: ``` class Address extends \Model { use \UserModel; // 集成用户基础模型 static public $table = 'address'; // 表名,不带表名前缀 protected $_id = 'id'; // 主键id protected $_uid = 'uid'; // 用户id protected $_fillable = array( 'province' => 'str', 'city' => 'str', 'district' => 'str', 'address' => 'str', ); } ``` 集成用户基础模型后,模型会增加以下方法: **updateByUid($uid, $arr)** 更新等于指定用户id的所有记录 **updateInUid($id, $uid, $arr)** 更新指定id的单条记录,且该记录的用户id必须等于指定的用户id **removeByUid($uid)** 删除等于指定用户id的所有记录 **removeInUid($id, $uid)** 删除指定id的单条记录,且该记录的用户id必须等于指定的用户id **getByUid($uid, $field = '')** 读取指定用户id的单条记录,如果指定了字段名,则只返回该字段的值 **getInUid($id, $uid, $field = '')** 读取指定id的单条记录,且该记录的用户id必须等于指定的用户id,如果指定了字段名,则只返回该字段的值 **getListInUid($uid, $offset = null, $length = null, $filter = array(), $order = '')** 读取指定用户id的读条记录,其他参数和getAll函数一致 **countListInUid($uid, $filter = array())** 统计指定用户id的符合条件的记录条数 **setSortInUid($id, $uid, $sort)** 设置记录的排序值,且该记录的用户id必须等于指定的用户id 提供基础用户模型的In类方法是为了数据操作时做好用户级别的数据操作隔离,防止当前用户请求通过id更新了其他用户的数据。 所以还可以模仿用户基础模型来构建其他操作数据时有隔离要求的基础模型,比如SAAS里面的商户。 ### 缓存 框架内置的缓存函数包含了文件缓存、APCU缓存和Redis缓存 #### 文件缓存 文件缓存使用的类库是Cache,位于 /Core/Cache.php 文件。可以new一个该类库是使用文件缓存。 但框架也提供了简单的fcache函数来快速调用: ``` fcache($dir, $key, $data = null, $expire = 0, $remove = false) // 返回缓存的数据,或返回操作是否成功 参数说明: $dir 缓存目录,相对于/Data/Cache/目录,可以使用多级目录形式,比如:aaa/bbb,最终的存储目录就是:/Data/Cache/aaa/bbb/ $key 缓存键 $data 要缓存的数据,传如了该参数则写入缓存,该参数传null或不传,则为读取缓存 $expire 缓存过期时间 $remove 是否删除缓存,传入该参数后$data,$expire参数失效 ``` #### APCU缓存 APCU缓存使用的类库是Apcu,位于 /Core/Apcu.php 文件。可以new一个该类库是使用文件缓存。 但框架也提供了简单的ucache函数来快速调用: ``` ucache($type, $key, $data = null, $expire = 0, $remove = false) // 返回缓存的数据,或返回操作是否成功 参数说明: $type 缓存分类 $key 缓存键 $data 要缓存的数据,传如了该参数则写入缓存,该参数传null或不传,则为读取缓存 $expire 缓存过期时间 $remove 是否删除缓存,传入该参数后$data,$expire参数失效 ``` #### Redis缓存 Redis缓存没有类库,使用Rd()函数即可创建了一个Redis实例。框架也提供了简单的rcache函数来快速调用: ``` rcache($type, $key, $data = null, $expire = 0, $remove = false) // 返回缓存的数据,或返回操作是否成功 参数说明: $type 缓存分类 $key 缓存键 $data 要缓存的数据,传如了该参数则写入缓存,该参数传null或不传,则为读取缓存 $expire 缓存过期时间 $remove 是否删除缓存,传入该参数后$data,$expire参数失效 ``` 如果需要使用redis的其他数据类型和方法,可以使用Rd(),比如删除key:Rd()->del()。 另外框架还提供一个hcache函数,用于redis的哈希表操作: ``` hcache($key, $hashKey, $data = null, $remove = 0) // 返回缓存的数据,或返回操作是否成功 参数说明: $key 缓存键 $hashKey 缓存哈希建 $data 要缓存的数据,传如了该参数则写入缓存,该参数传null或不传,则为读取缓存 $remove 是否删除缓存,传入该参数后$data参数失效。注意,该参数的数量类型是整数:0 不删除,1 删除hashKey,2 删除key对应的整个hashtable ``` ### 日志 框架内置wlog函数用来写日志。日志文件保存在 /Data/Logs/$type参数名 目录内,且会按日期分子目录存储。 不传递$type参数,默认为user(既日志文件保存在 /Data/Logs/user)内。 ``` wlog($message, $type = 'user') 参数说明: $message 日志内容,可以是任何数据类型或者对象,非字符串时会使用print_r打印 $type 日志类型,可以使用斜杆来划分目录 ``` 比如: ``` wlog('127.0.0.1', 'login/ip') 假设写入时间为2024-01-01 01:01:01,日志会写入到:/Data/Logs/login/ip/2024-01/2024-01-01.log 文件内,记录为: ---------------[2024-01-01 01:01:01]--------------- 127.0.0.1 ``` ### 多语言 框架的多语言配置文件在/Lang目录内。默认的语言是简体中文(文件为 /Lang/zh_cn.php)。 读取和修改当前语言使用lang()函数: ``` echo lang() // 显示 zh_cn lang('en_us') // 设置当前语言为英语美国 echo lang() // 显示 en_us ``` 框架内置L()函数,用于获取当前语言的消息文本。 ``` L($paths, $args = array(), $lang = '') 参数说明: $paths 斜杠/ 分割的语言分类路径 $args 传递给语言配置项的字符串格式化参数,字符串格式化的使用方式参考php内置的vsprintf函数 $lang 指定语言,默认为lang()设定的语言 比如: L('common/unexpected_error') // 显示消息文本为:'未知错误' L('upload/image/unsupported_format', array('gif')) // 显示消息文本为:'不支持 gif 格式的图片文件' L('upload/image/unsupported_format', array('gif'), 'en_us') // 显示消息文本为:'Unsupport gif image format.' ``` ### API模式 如果需要使用QPHP框架用来开发API,可以使用控制器内的succ()和fail()方法来输出json格式数据。参考:[输出json](#输出json)。 但更方便的方法,可以参考Api模块,通过重写控制器的render()方法来支持json格式数据输出。 这样就不需要显式的调用控制器的succ()方法来输出json数据,控制器内的动作执行完成后,就会自动调用succ()方法输出json格式数据。 具体的使用参考框架的Api模块(/App/Api目录内的代码),特别是 /App/Api/Controller/Base.php的模块内基类控制器。 其他控制器只要继承Base控制器,即可在控制器内的动作执行完成后,自动调用succ()方法输出json格式数据。 /App/Api/Controller/Index.php就是继承了Base控制器,index() 动作不需要显式的调用控制器的succ()方法即可输出json格式数据。 ### 其他常用类库和函数 包含了程序开发常用的类库和函数,仅供选用。 #### 常用函数 位于/Core/Func.php文件,具体参数请直接查看源码的参数说明: 数组 ``` array_merge_deep(&$array1, $array2) // 递归合并两个数组 array_pretreat(&$array) // 对数组进行递归预处理,以便输出为json时有更好的数据格式 ``` 编解码和加解密 ``` base58_encode($string) // base58编码,该编码方式速度慢于base64_encode,但生成的编码不含视觉上易被混淆的字母和数字,且没有+=等特殊字符,对url友好 base58_decode($base58) // base58解码 MEncode($str) // 加密,使用aes-256-gcm MDecode($str) // 解密,使用aes-256-gcm ``` 缓存 ``` cache($type, $key, $data = null, $expire = 0, $remove = false) // 缓存,根据框架配置文件自动选择把数据缓存在磁盘、apcu或者redis fcache($dir, $key, $data = null, $expire = 0, $remove = false) // 文件缓存 rcache($type, $key, $data = null, $expire = 0, $remove = false) // redis缓存 hcache($key, $hashKey, $value = null, $remove = 0) // redis哈希表缓存 ucache($type, $key, $data = null, $expire = 0, $remove = false) // APCU缓存 ``` 字符串 ``` cstrlen($str) // 计算中英文混合字符串的长度 msubstr($str, $start = 0, $length, $suffix = true, $charset = "utf-8") // 截取字符串 filter_utf8mb4($str) // 过滤utf8mb4字符 md5Rand() // 返回永远唯一的32位md5的随机字符串 randNum($min = null, $max = null) // 返回随机数 randString($len = 6, $type = 3, $addChars = '') // 返回随机字符串 ``` HTML ``` stripTag($string, $allowed_tags = null) // 去除HTML标签,包括style和js,只保留存文本 toHtml($text) // 转换文本为html,会把空格和换行符都转换为html代码 ``` 文件 ``` fileExt($fileName, $dot = true) // 返回文件后缀名 file_date_path($abs_base_dir, $ext = '') // 生成按日期分割的文件路径 ``` 其他 ``` dump() // dump变量 clientip() // 获取客户端IP eol($message) // 命令行模式下带时间标识按行显示消息 print_br($message) // html中按行显示消息 millisecond() // 返回当前毫秒数 ``` #### 登录和身份识别 框架附带了一个Auth类库(/Libs/Auth.php),可以使用Session实现用户的登录和身份识别。 **登录:** 调用该方法将会把用户标识保存起来,后面通过checkAuthed()方法可以判断用户是否已登录 ``` Login($uid, $scope = 'user') 参数说明: $uid 用户的标识。通常为用户的ID $scope 用户的类型。普通用户、管理后台账号等可以使用不同的用户类型做标识。这样不同类型的账号可以允许相同的用户标识ID。 ``` **退出登录:** 清理Login()方法保存的用户标识,清理后调用checkAuthed()方法将返回false ``` Logout() ``` **检查身份:** 检查用户是否登录。如果未登录,则跳转到登录页面 ``` checkAuthed($scope = 'user', $redirect = '') 参数说明: $scope 用户的类型。 $redirect 如果未登录,则跳转到模块的Login控制器额index()方法,并带上该参数。方便用户在登录页登录后,可以跳转到这个指定的地址。 ``` **检查身份:** 检查用户是否登录。成功返回true,失败返回false ``` isAuthed($scope = 'user') 参数说明: $scope 用户的类型。 ``` **获取登录用户的用户标识ID:** 成功返回用户标识ID,失败返回null ``` getAuthedUid($scope = 'user') 参数说明: $scope 用户的类型。 ``` 框架附带了一个TokenAuth类库(/Libs/TokenAuth.php),可以使用Token实现用户的登录和身份识别。主要用于API请求。 **登录:** 调用该方法将会把用户标识保存起来,后面通过checkAuthed()方法可以判断用户是否已登录 ``` Login($uid, $scope = 'user') 参数说明: $uid 用户的标识。通常为用户的ID $scope 用户的类型。普通用户、管理后台账号等可以使用不同的用户类型做标识。这样不同类型的账号可以允许相同的用户标识ID。 返回参数: $token 返回用户Token字符串 ``` **退出登录:** 清理Login()方法保存的用户标识,清理后调用checkAuthed()方法将返回false ``` Logout($token, $scope = 'user') 参数说明: $token 用户Token $scope 用户的类型。普通用户、管理后台账号等可以使用不同的用户类型做标识。这样不同类型的账号可以允许相同的用户标识ID。 ``` **清理鉴权:** 清理指定用户的所有token,一般用于修改密码后退出所有登录的客户端 ``` Clear($uid, $scope = 'user') 参数说明: $token 用户Token $scope 用户的类型。普通用户、管理后台账号等可以使用不同的用户类型做标识。这样不同类型的账号可以允许相同的用户标识ID。 ``` **获取登录用户的用户标识ID:** 成功返回用户标识ID,失败返回null ``` GetAuthedUid($token, $scope = 'user') 参数说明: $token 用户Token $scope 用户的类型。 ``` **刷新Token有效期:** ``` Refresh($token, $scope = 'user') 参数说明: $token 用户Token $scope 用户的类型。 ``` #### HTTP相关 框架附带了一个Http类库(/Core/Http.php),具体参数请直接查看源码的参数说明: ``` Http::download($filename = '', $showname = '', $content = '') // 发送文件给浏览器下载 Http::sendStatus($code) // 发送http状态码 Http::sendAuthUser($hintMsg, $errorMsg = '') // 发送用户授权弹窗到浏览器 Http::getAuthUser() // 获取用户在浏览器授权弹窗内输入的用户名和密码 Http::setFormCache() // 启用页面缓存。使用页面缓存可以在浏览器刷新页面,或者页面后退时保留表单数据,并在后退时避免出现“网页已过期”问题。 Http::mimeType($ext) // 通过文件扩展名获取mime类型 ``` #### cookie 框架附带了一个Cookie类库(/Core/Cookie.php),具体参数请直接查看源码的参数说明: ``` Cookie::set($name, $value = '', $time = 0, $domain = null, $encode = false) // 设置cookie Cookie::get($name = '', $decode = false) // 获取cookie Cookie::remove($name) // 移除cookie Cookie::clear() // 清空cookie ``` #### session 框架附带了一个Session类库(/Core/Session.php),具体参数请直接查看源码的参数说明: ``` Session::set($name, $value = '') // 设置session Session::get($name = '', $decode = false) // 获取session Session::remove($name) // 移除session Session::clear() // 清空session ``` #### 使用redis接管session 框架附带了一个RedisSession类库(/Libs/RedisSession.php),如果在框架配置文件中配置了自定义的session处理器,且handle设置为了RedisSession。 那么框架就会使用RedisSession接管session,并把session存储到redis里面。 前提是安装有redis服务器,并在框架配置文件中设置好了redis相关配置项。 **注意:** session在redis存储是的key是以 PHPREDIS_SESSION 开头的。 #### 图像处理 框架附带了一个Image类库(/Core/Image.php),具体参数请直接查看源码的参数说明: ``` Image::imgVerify($length = 4, $mode = 3, $type = 'png', $width = 200, $height = 80, $highlight = false) // 生成图形验证码 Image::chkVerify($captcha_code, $captcha_key = null) // 验证图形验证码 Image::imgVerifyHighLight() // 生成高亮模式验证码,验证码字体颜色较亮,适合暗色背景页面 Image::getInfo($filename) // 获取图片信息 ``` #### 加解密 框架附带了一个Mcrypt类库(/Core/Mcrypt.php),具体参数请直接查看源码的参数说明: ``` Mcrypt::encrypt($plaintext, $key, $aad = '') // 加密 Mcrypt::decrypt($ciphertext, $key, $aad = '') // 解密 ``` #### 文件上传 框架附带了一个Uploader类库(/Core/Uploader.php),具体参数请直接查看源码的参数说明: ``` ``` ``` $uploader = new \Uploader('file1'); if ($uploader->hasUpload()) { // 如果有上传文件 $uploader->upload(); // 执行文件上传 if ($uploader->hasError()) { // 如果上传失败 fail($uploader->error()); // 显示上传失败的错误信息 } else { $filename = $uploader->getUploadFile(); // 获取上传后的文件路径 } } ``` #### 数据验证 框架附带了一个Verifier类库(/Core/Verifier.php),具体参数请直接查看源码的参数说明: ``` Verifier::isEmail($string) // 是否是邮箱地址 Verifier::isNumeric($string) // 是否是数字(没有小数点) Verifier::isPhone($string) // 是否是固定电话号码 Verifier::isMobile($string) // 是否是手机号码 Verifier::isMobilePhone($string) // 是否是电话号码(含固定电话号码和手机号码) Verifier::isChina($string) // 是否是中文 Verifier::hasChina($string) // 是否含有中文 Verifier::isUrl($string) // 是否是URL地址 Verifier::isIpAddr($string) // 是否是IP地址 Verifier::isZip($string) // 是否是邮编 Verifier::isWord($string) // 是否仅含有字母数字下划线 Verifier::isTime($string) // 是否是24小时制时间 Verifier::validGOTID($idcard) // 是否是港/澳/台身份证号 Verifier::validIDCard($idcard) // 是否是中国身份证号 ``` #### 后台任务 框架附带了一个Daemon类库(/Libs/Daemon.php),用于创建命令行执行的后台任务,仅支持linux系统。 使用示例请参考 /Work/sample.php,使用方式如下: ``` /* * 创建任务,名称为 sample * 任务运行用户为 www (一般就是php-fpm的运行用户,这里假设为 www) * 任务运行用户组为 www (一般就是php-fpm的运行用户组,这里假设为 www) * 任务相关文件(pid、日志等)保存路径为框架内的 /Data/Daemon 目录 */ $worker = new Daemon('sample', 'www', 'www', DATA_PATH . '/Daemon'); /* * 设置任务 * $interval 任务重复执行间隔,单位秒 * $callable 包含任务内容的函数 */ $worker->setTask(5, function () { wlog(time()); }); $worker->run(); ``` 启动: ``` php sample.php start php sample.php start -d // daemon模式 ``` 停止: ``` php sample.php stop ``` 重启: ``` php sample.php restart php sample.php restart -d // daemon模式 ``` 查看状态: ``` php sample.php status ```