SW-X

SW-X 是一款基于Swoole 实现的常驻内存便捷开发式PHP框架,专为高性能、便捷开发而生,摆脱传统的FPM运行模式。 在开发上,我们为您准备了以下常用组件:

  • http 服务服务器
  • websocket 服务服务器
  • server 服务服务器
  • 数据库ORM
  • 图片验证码
  • 模板渲染引擎
  • 协程redis连接池
  • 协程mysql 连接池
  • 注解路由
  • 注解Ioc
  • 注解Aop
  • 注解Param
  • 双容器实现请求隔离

以上组件为常用组件,更多组件请看组件库文档

维护团队

  • 作者
    • 小黄牛 1731223728@qq.com
  • 团队成员
    • 暂无,期待您的加入

其他

    QQ交流群

    • SW-X官方一群 1012826310
  • 商业支持:

    • QQ 1731223728
  • 作者微信

    • junphper

注意事项

  • 不运行在代码中执行sleep类型函数,因为这样有可能导致系统堵塞,出现大量等待进程,
  • 不能使用exit/die语句,因为这样会导致worker进程重启,这样的后果是其进程下挂载的连接池也会跟着重启。
  • 连接池是使用完成之后,必须要return归还连接,否则将造成连接池队列被取空的问题。

约定规范

  • 项目中类名称与类文件(文件夹)命名,均为大驼峰,变量与类方法为小驼峰。
  • 在HTTP响应中,于业务逻辑代码中echo $var 并不会将$var内容输出至相应内容中,请调用实例中的fetch()方法实现。

SW-X版本更新记录

V1.2.24(2021年01月12)


  • 1、修复server服务无法正常启动的bug

  • 2、修复错误异常没正常监听到PHP报错的bug

  • 3、实现RPC微服务支持

V1.2.23(2021年01月08)


  • 1、修复sw-x start时,没有初始化进程PID记录文件的BUG

  • 2、修复HTTP上传文件时,框架getSaveName自动删除了ROOT_PATH,导致没有返回完整的地址

  • 3、修复Param注解,当设置允许为空,并设置了正则表达式等过滤参数时,参数为空时也跑过滤规则的BUG

  • 4、Param注解新增一个method参数,表示当为某个请求类型时,该注解才生效,不填写则默认任何请求都生效,该参数只对HTTP服务有效,WebSocket服务设置无效

V1.2.22(2021年01月07)


  • 1、新增HTTP请求记录WEB监控服务组件

  • 2、优化错误异常监听

  • 3、新增Db连接池小于等于0时异步调用生命周期回调通知

  • 4、新增Redis连接池小于等于0时异步调用生命周期回调通知

  • 5、新增默认时区配置

V1.2.21(2020年12月30)


  • 1、新增一个CMD命令支持,用于生成初始控制器文件

V1.2.20(2020年12月11)


  • 1、紧急修复,APP启动服务前错误载入了路由表,导致reload指令没办法正常重载业务代码

V1.2.19(2020年12月10)


  • 1、新增,HTTP-Request请求类,新增一个is_ajax方法,用于判断当前请求是否为ajax类型

V1.2.18(2020年11月07)


  • 1、修复,容器无法正确存储除对象、闭包函数之外的其他类型数据的bug

  • 2、新增,HTTP调试器,用于监听当前请求的框架处理流程和响应结果,便于调试,只有在app.de_bug == true的情况下开启

  • 3、修复,自定义注解在服务初始化时,也加载了其他未自定定义的注解标签吗,导致单元测试注解无效的BUG

  • 4、优化,单元测试调试时,路由地址没进行自适应大小写的问题

  • 5、修复,单元测试无法正常调试的BUG

V1.2.17(2020年10月30)


  • 1、新增,TestCase单元测试注解,暂只支持HTTP服务应用

  • 2、新增,Db-ORM新增一个test方法,用于支持TestCase单元测试注解

V1.2.16(2020年10月28)


  • 1、新增,自定义注解功能,所有自定义的注解类均为前置注解,加载顺序在内置环绕注解类之后。

  • 2、新增,Db-ORM支持whereOr操作

  • 3、新增,Db-ORM支持whereIn操作

  • 4、新增,Db-ORM支持whereNotIn操作

V1.2.15(2020年10月27)


  • 1、优化Db的where操作,当为数组条件时,例如$where[] = ['id', 'in(1,2,3)', null];时,null条件不再进行字符串解析。

  • 2、修复Param注解当过滤参数为数组类型时解析错误的BUG

  • 3、删除swoole/library/event/Route.php这个多余文件

  • 4、Mysql/Redis新增获取不到连接池实例时,返回false,该优化主要面对定时任务【定时任务再onstart事件载入,优先级高于连接池载入的实例】

  • 5、修复Client客户端发包,URL带端口号时不能正常发送,errCode为【704】的BUG

  • 6、优化了生命周期controller_error的判断流程,HTTP请求下没办法正确获取报错内容

V1.2.14(2020年9月30)


  • 1、增加,WebSocket服务在open、close阶段记录于销毁请求容器

  • 2、优化了生命周期controller_error的判断流程,修复获取websocket事件错误

  • 3、修复WebSocket服务下,使用param函数无法正确获取参数

  • 4、修复定时任务载入事件,从onStart改为onWorkerStart,只有第一个worker线程启动时载入

  • 5、修复定时器中无法正常使用Mysql、Redis实例

  • 6、修复定时器、Swoole事件中无法调用WebSocket基类的fetch方法,改为最后一个参数加入server实例传入

V1.2.13(2020年8月16)


  • 1、优化Db链:insert、insertGetId、update、setInc、setDec操作,字段名加入``字符串包裹,防止字段名冲突

V1.2.12(2020年8月8)


  • 1、修复生命周期,获取错websocket容器名称,导致没办法回调事件的BUG

  • 2、新增HTTP客户端组件封装,用于代替PHP-FPM-CURL组件

V1.2.11(2020年8月6)


  • 1、server新增一个配置项,package_max_length,修复文件上传不能大于2M的bug

  • 2、修复Client,HTTP请求无法正确调用Swoole原生支持方法的BUG

V1.2.10(2020年8月3)


  • 1、优化Param注解参数预设为真null时也执行,之前是isset为true时才执行

  • 2、修复HTTP文件无法正确上传,返回上传路径错误的BUG

  • 3、Db的select查询失败优化为返回空数组[]

  • 4、修复新版本在onWorkerStart阶段依旧读取老定时任务配置不存在的BUG

  • 5、优化Mysql连接池存活检测,改为15分钟检测50%的连接是否还存活

  • 6、修复Mysql连接池过期,存活检测没有自动补充新连接的BUG

  • 7、修复Ioc注解,初始化类传入参数无法正常解析的BUG

  • 8、修复Ioc注解,调用类方法时传入参数无法正常解析的BUG

  • 9、优化,Ioc注解不再支持对静态控制器方法的使用,规范控制器方法都必须为动态方法,若检测为静态方法,将对route_error生命周期抛出status=Ioc Static的错误

  • 10、新增,Db类支持切换临时数据库连接实例,但其连接为PDO短连接,与连接池无关,同样需要调用return清空实例

V1.2.9(2020年7月30)


  • 1、修复Mysql连接数统计不正确的BUG

  • 2、新增Mysql连接池定时器检测功能,修复长时间没连接,MySQL报 server has gone away的错误

V1.2.8(2020年7月29)


  • 1、修复控制器重定向读取实例错误的BUG

  • 2、控制器重定向301改为默认302

  • 3、修复Db,where条件传入0不能正确解析的BUG

  • 4、修复Db,count条件在不传入field的情况下无法正确获取*的bug

  • 5、修复Model获取表名,rtrim导致的部分表名获取错误的BUG

  • 6、优化Db,where条件数组方式的时候,使用|符号可以让多个字段支持OR操作

  • 7、修复Db,使用同一个实例时,切换不同的数据表不会清空前置条件的bug

  • 8、修复DB,where条件传入空条件时不能正确解析的BUG

  • 9、修复请求级容器某些场景下会出现内存溢出的BUG

  • 10、使用Swoole官方的连接池重写了Mysql连接池,不再支持多库实例,跟最小连接数

  • 11、使用Swoole官方的连接池重写了Redis连接池,不再支持最小连接数

  • 12、Db的query只允许执行原生select查询,查询成功调用返回fetchAll的结果集

  • 13、Db新增一个exec方法,只允许执行原生除select外的SQL语句

V1.2.7(2020年7月26)


  • 1、修复路由绑定时填写大写字母不兼容的BUG

  • 2、修复控制器重复调用fetch输出页面内容会发生致命异常的bug

V1.2.6(2020年7月22)


  • 1、删除部分无用配置项

  • 2、更换新的模板引擎支持

  • 3、修复Db的debug方法无效的问题

V1.2.5(2020年7月21)


  • 1、配置文件加入参数,是否开启连接池统计监听定时器

  • 2、websocket推送失败,加入生命周期回调事件

  • 3、优化致命异常不进行生命周期回调,只有普通异常才回调,因为致命异常在Swoole中已经跳出协程底层,会导致拿不到协程容器。

V1.2.4(2020年7月20)


  • 1、优化服务启动时自动初始化redis_pool_num.countmysql_pool_num.count文件

  • 2、紧急修复Model类无法正确注入表名的BUG

  • 3、紧急修复【写入类型】Mysql连接池创建参数读取错误的BUG

V1.2.3(2020年7月20)


  • 1、紧急修复WebSocket路由无法正确识别的BUG

  • 2、修复Param注解无法正确处理AES加密后的的数据包

  • 3、调整Websocket->param函数直接获取完整json,改为只获取data参数

V1.2.2(2020年7月20)


  • 1、Db的updatedelete方法新增判断条件,为无where条件时不执行返回false

  • 2、定时任务的注册方式,改为手动挂载在配置文件/config/crontab.php文件中

  • 3、sw-x status中加入当前Mysql连接数、Redis连接数状态、当前Swoole扩展版本、本机CPU最大支持核数

  • 4、修复Param注解不支持websocket参数过滤的BUG

  • 5、修复WebSocket服务的已知bug

V1.2.1(2020年7月19)


  • 1、重构了部分底层~

  • 2、新增了双容器实现~

  • 3、请求实例不再在实例之间传递,而是通过请求级容器获取、共享~

  • 4、实现了框架与请求实例之间的解耦

  • 5、实现了请求与控制器之间的解耦

捐赠

您的捐赠是对SW-X项目开发组最大的鼓励和支持。我们会坚持开发维护下去。 您的捐赠将被用于:

  • 持续和深入地开发
  • 文档和社区的建设和维护

支付宝

捐赠

微信

捐赠

通过微信捐赠的朋友,请留言你的侠号

捐赠者列表

  • *松鼠
  • *猫唇

环境要求

满足基本的环境要求才能运行框架,SW-X 框架对环境的要求十分简单,只需要满足运行 Swoole4.4+ 拓展的条件,并且 PHP 版本在 7.1 以上即可。

基础运行环境

  • 保证 PHP 版本大于等于 7.1
  • 保证 Swoole 拓展版本大于等于 4.4+
  • 需要 pcntl 拓展的任意版本

安装swoole

建议参考Swoole官方文档,毕竟官方是最新的安装方式。

框架安装

  • GitHub 求个小心心 (^.^)

国内可以使用阿里的镜像,但可能也会出现包丢失的情况

composer config -g repo.packagist composer https://mirrors.aliyun.com/composer/

删除镜像

composer config -g --unset repos.packagist

Composer 安装

按下面的步骤进行手动安装
(建议使用)

composer require swoolex/swoolex

或者(可能出错)

composer require swoolex/swoolex:^v1.2.24

官方建议:直接在首页,下载最新版本的安装包源码部署即可。

Hello Word

打开根目录的index.php文件,通过service('http')设置为HTTP服务,然后再XShell界面通过命令php index.php启动服务。
如果端口错误,则先修改/config/http.php配置文件中间的port参数。

注意
HTTP服务中的控制器是依赖路由模式加载的,对应的路由模式可以再/config/route.php文件中进行修改。
当前大版本中的路由生成由注解类实现,不支持手动调用Route类注入路由规则。

wwwroot              项目部署目录
----------------------------------
├─app        应用目录
│  └─controller      http服务的控制器目录
│     └─Index.php    默认控制器文件

然后我们可以看下默认的控制器代码:

<?php
namespace app\controller;
use x\Controller;

class Index extends Controller
{
    /**
     * @RequestMapping(route="/", method="get", title="主页")
    */
    public function index() {
        return $this->fetch('SW-X:Hello Word!');
    }

}

反向代理

由于 Swoole Server 对 HTTP 协议的支持并不完整,建议仅将 SW-X 作为后端服务,并且在前端增加 NGINX 或 APACHE 作为代理,参照下面的例子添加转发规则

Nginx

server {
    root 绑定个目录,但不建议指定到server的目录,这样不安全;
    server_name 你的域名;

    # 解决静态文化访问问题
    location /public/ {
        root 你的静态根地址,不能带public;
    }

    location / {
        proxy_http_version 1.1;
        proxy_set_header Connection "keep-alive";
        proxy_set_header X-Real-IP $remote_addr;
        if (!-f $request_filename) {
             proxy_pass http://127.0.0.1:9502;
        }
    }
}

代理之后,可通过$this->header()中的x-real-ip获取客户端真实ip

Apache

<IfModule mod_rewrite.c>
    Options +FollowSymlinks
    RewriteEngine On
    RewriteCond %{REQUEST_FILENAME} !-d
    RewriteCond %{REQUEST_FILENAME} !-f
    #RewriteRule ^(.*)$ index.php/$1 [QSA,PT,L]  fcgi下无效
    RewriteRule ^(.*)$  http://127.0.0.1:9501/$1 [QSA,P,L]
    #请开启 proxy_mod proxy_http_mod request_mod
</IfModule>

框架设计

SW-X当前版本主要是由服务启动,事件监听注册,路由解析,注解绑定这4个部分组成。

服务端事件

SW-X底层默认监听了Swoole-Server的所有消息事件,并在onRequestonMessage事件中进行了二次开发,进而实现路由解析功能。
如果开发者需要二次使用消息事件,SW-X也支持了二次消息事件转发功能,其目录为/app/event/

关于调试模式

调试模式开关在/config/app.php文件中的de_bug选项,
当开启调试模式时true,SW-X会实现以下几个功能:

1、将报错写入到/runtime/log/目录下
2、将所有执行的SQL语句【除了select】都记录到/runtime/sql/目录下
3、所有view文件都会重新解析
4、v1.2.18版本起,HTTP服务加入了服务调试器,会出现在页面右下角

生命周期

生命周期的概念是在v1.1.x版本才开始引入,是框架核心在处理业务时的一些回调事件转发。
生命周期处理方法统一存放在:/lifecycle/目录下,应统一使用run()方法作为回调入口。
现框架支持以下生命周期回调处理:

annotate_param:注解Param标签校验失败时的独立回调事件(v1.1.5)
controller_error:应用监听错误的回调事件,只回调普通错误异常(v1.1.6)
route_error:当除了Param注解外,其他注解校验失败时的统一回调事件(v1.1.6)
route_start:路由扫描完成时的回调事件(v1.1.6)
websocket_push_error:当WebSocket->Push失败时的回调事件(v1.2.5)
testcase_callback:当单元测试不通过时的回调事件(v1.2.17)
mysql_pop_error:当Mysql连接数小于等于0时,会回调此事件(v1.2.22)
redis_pop_error:当Redis连接数小于等于0时,会回调此事件(v1.2.22)
rpc_error:当客户端RPC请求错误时,会回调此事件(v1.2.24)

内存溢出

在Swoole中很难定位内存溢出的bug点,为了防止内存溢出,在应用中不要直接调用自定义的静态类,若有必要调用,可以先将实例存储在\x\Container请求级容器中。
而且在sw-x status指令中,可以查看到当前应用的内存状态和\x\Container容器长度,
若内存一直飙升,容器长度无法下降的话的,就表示应用代码中发生了溢出现象,需要自行审计代码。

如果担心sw-x status不准的话,也可以使用linux的free -h,压测的过程中,开新窗口,查看打印出来的free有没有被回收,没的话就代表内存溢出了。

关于Swoole内存溢出的问题,之前认为没必要在文档中解析这个问题,但最近有些开发者向我咨询了,我还是觉得有必要讲解下。
1、由于Swoole是常驻内存的,所以静态类 或 静态方法所占用的内存开销,在Swoole中不会自动释放,需要手动unset销毁,这点需要注意,v1.2.10版本起,SW-X已经强制控制器的方法不再支持静态方法定义。
但若开发者自行封装处业务类时,也要谨慎使用静态属性定义。

2、还有global全局变量声明,在Swoole中服务可分为进程级、请求级,进程级对应Worker进程,请求级则对应其下的子线程,若在业务代码中声明变量为global,则该变量会从请求级升级为进程级变量,届时变量将无法自动销毁,因为Swoole会在请求结束自动释放其子线程所占用的内存开销,但不会销毁Worker进程相关的内存占用。

3、不要在代码中执行sleep以及其他睡眠函数,这样会导致整个Worker进程阻塞

4、不要在代码中执行exit\die,这样会导致整个Worker进程死亡

开启服务

v1.1.1版本之后,SW-X的服务维护,全部改为依赖sw-x文件,我们无需关心该文件下的代码,常用操作指令如下:

查看服务指令支持:
php sw-x

支持以下指令:
start [服务类型],以DeBug模式开启服务,此时服务不会以Daemon形式运行
start [服务类型] -d,以Daemon模式开启服务
status,查看服务器的状态
stop,停止服务器
reload,热加载所有业务代码
test [服务类型] [路由地址],单元测试执行命令,1.2.17版本起支持
controller [服务类型] [路由地址] [方法名称] [路由名称],控制器初始化自动生成命令,1.2.21版本起支持
monitor start,创建HTTP监控所需要的WEB控制台组件,1.2.22版本起支持

在启动服务之前,我们还应该先修改对应的配置文件,配置存放在/config/服务配置.php

通过:status输出的Memory_get_usageContainer_count信息,可以判断当前应用是否发生了内存溢出现象。
v1.2.2版本后,新增Mysql_connect_countRedis_connect_count可以查看到当前应用Mysql和Redis的在线连接数,数据为5秒更新一次(所有Worker进程连接池的总和)。

定时器自动载入

很多时候在实际开发中我们都需要启动一些定时任务,来处理定时任务,SW-X中提供了定时器统一加载的服务,开发者只需要将定时任务创建在/app/crontab/目录下即可。
该目录下的定时任务,会在onStart事件中触发载入。

v1.2.2版本起,不再支持自动载入定时任务,而是改为手动挂载定时任务,修改配置文件/config/crontab.php文件。定时任务的执行方法,必须接收一个$server参数,为当前Worker的实例。

Server服务

SW-X中的Server服务,主要是指Swoole的服务端TCP/UPD服务器;对该类基础的服务器,SW-X没有进行任何的二次处理,若是开启server服务的开发者,需要自己在/app/event/的消息事件中实现自己的业务逻辑。

HTTP服务加载流程

控制器对象是http组件中方便客户端与服务端交互的对象,它使用了对象池对象复用模式,以及注入requestresponse对象进行数据交互

加载流程

  • 用户A请求/index经过onRequest消息事件转发到路由器,然后根据路由器定位到/app/controller/对应路由规则处理文件.php控制器
  • 路由器将请求对象注入到基类控制器中。
  • 通过调用基类控制器的内置方法,进行页面间消息通信。

约定规范

  • 项目中类名称与类文件命名,均为大驼峰,变量与类方法为小驼峰。
  • (文件夹),均为小写。
  • 所有控制器均要继承\x\Controller基类控制器。
  • 在HTTP响应中,于业务逻辑代码中echo $var并不会将$var内容输出至相应内容中,请调用基类控制器中的的fetch()方法实现。

注意事项

  • 注解只对路由器指向的主方法有效。
  • 注解对跨控制器调用无效。

基础使用方法

sw-x中,使用基类控制器中的的$this->fetch()方法向页面输出内容。

<?php
namespace app\controller;
use x\Controller;

class Index extends Controller
{
    /**
     * 向页面输出内容
     * @RequestMapping(route="/", method="get", title="主页")
    */
    public function index() {
        return $this->fetch('Hello Word');
    }

    /**
     * 输出视图
     * @RequestMapping(route="/test")
    */
    public function index() {
        $this->assign('sw', 'Hello Word');
        return $this->view();
    }
}

HTTP服务路由

当前版本下HTTP服务的路由主要有三种模式,可以在/config/route.php配置文件中修改。
分别为:path_info模式路由表模式兼容模式

  • path_info模式:传统的根据Controller文件定位路由规则,例如/app/controller/index/Index.php->test()那路由指向规则就是:/index/Index/test
  • 路由表模式:启用注解实现路由绑定的功能,在控制器中通过@Controller@RequestMapping注解绑定路由规则。
  • 兼容模式:就是上面两种路由规则允许同时启用。

v1.0.2版本,已经取消路由模式支持,改为框架强制路由模式。

Request对象

v1.2.1版本后,Request对象已被存储在容器中,我们只需要通过\x\Container::getInstance()->get('request')即可全局获取到实例。 AOP操作中也不再回调Request对象,直接使用容器获取即可,但如果您需要更新Request对象信息,则注意需要重新set回容器。

Response对象

v1.2.1版本后,Response对象已被存储在容器中,我们只需要通过\x\Container::getInstance()->get('response')即可全局获取到实例。 AOP操作中也不再回调Response对象,直接使用容器获取即可,但如果您需要更新Response对象信息,则注意需要重新set回容器。

关于Request

v1.2.1版本起,之前Controller中关于请求信息的相关函数都独立转移到\x\Request请求类中了,相关函数的返回值没变。

获取header

使用\x\Request::header()可以获取到请求头信息。

获取GET参数

使用\x\Request::get()可以获取到GET请求的表单信息。

获取POST参数

使用\x\Request::post()可以获取到POST请求的表单信息。

IS_GET

使用\x\Request::is_get()可以判断是否GET请求。

IS_POST

使用\x\Request::is_post()可以判断是否POST请求。

IS_AJAX

v1.2.19版本起支持,使用\x\Request::is_ajax()可以判断是否AJAX类型请求。

URL相关

URL相关的函数,从v1.1.5版本起支持:

<?php
\x\Request::is_ssl(); // 是否 https
\x\Request::ip(); // 获取客户端ip
\x\Request::domain(); // 获取当前域名,不带路由和参数
\x\Request::route(); // 获取当前请求路由,不带参数
\x\Request::url(); // 获取当前URL,不带域名
\x\Request::url(true); // 获取当前URL,带域名
\x\Request::baseUrl(); // 获取当前URL,不带参数
\x\Request::baseUrl(true); // 获取当前URL,带参数

重定向

SW-X中并没有提供倒计时重定向的功能,但提供了直接重定向的支持。
使用方法很简单,举例如下:

<?php
namespace app\controller;
use x\Controller;

class Index extends Controller
{
    /**
     * 向页面输出内容
     * @RequestMapping(route="/", method="get", title="主页")
    */
    public function index() {
        // 直接跳转到:当前域名/index/test路由后缀 的URL
        return $this->redirect('index/test', 301);

        // 带get参数跳转
        // 直接跳转到:当前域名/index/test路由后缀?id=1 的URL
        return $this->redirect('index/test', 301, ['id' => 1]);

        // 直接用第三方域名跳转
        return $this->redirect('http://sw-x.cn', 301);
    }
}

视图渲染

在SW-X中,所有控制器都需要继承基础Controller,所以控制器调用视图,也是依赖基类控制器所提供的方法。
渲染模板最常用的是控制器类在继承系统控制器基类(\x\Controller)后调用display()方法,或view()方法。
这两个方法支持接收的参数是一样的,均为路由地址,默认为空时为当前路由地址,/路由则对应index/index

display()为直接输出解析后的html内容到客户端,view()方法则是获取解析后的html内容,不支持输出到客户端。

SW-X中的视图文件,统一存放在/app/view目录下,该路径可以在/config/view.php配置文件中进行修改,需要注意路径末尾不能带/符号。

视图赋值

在视图文件中,除了系统常量输出无需赋值外,其他变量如果需要在模板中进行日常操作,必须先进行模板赋值,否则会抛出异常,将Controller数据传递到view层有下面2种方式:

1、assign

<?php
namespace app\controller;
use x\Controller;

class Index extends Controller
{
	/**
	 * @RequestMapping(route="/", method="get", title="主页")
	*/
	public function index() {
		// 赋值变量到view层
		$this->assign('name', '小黄牛');
		$this->assign('list', [
			'id' => 1,
		]);
		// 输出模板
		return $this->display();
	}
}

2、view 或 display时直接赋值

<?php
namespace app\controller;
use x\Controller;

class Index extends Controller
{
	/**
	 * @RequestMapping(route="/", method="get", title="主页")
	*/
	public function index() {
		// 输出模板时直接赋值,view也一样
		return $this->display('/', [
			'name' => '小黄牛',
			'list' => [
				'id' => 1,
			]
		]);
	}
}

关于SW-X的模板引擎

SwooleX的模板引擎支持,是参考借鉴于ThinkPHP5.1的内置模板引擎设计,在这里向ThinkPHP的研发团队致敬,小黄牛也是一个用了7年ThinkPHP的忠实粉丝。ThinkPHP5.1的模板引擎是用过最顺手的,没有之一。

变量输出

在模板中输出变量的方法很简单,例如,在控制器中先传递数据:

$this->assign('name', 'SwooleX');
return $this->display();

然后就能够在模板中使用该变量:

Insist, {$name}!

如果传输的数据是个数组:

$this->assign('data', [
	'name' => 'SwooleX'
]);
return $this->display();

也支持这样解析:

Insist, {$data.name}!

使用默认值

在控制器中,我们通常会根据不同的业务逻辑,传递数据到视图中,这时候如果某个变量在一些场景中没有赋值到,这时候在模板的解析过程中就会报错。
为了应对这种情况,可以给该变量设置一个默认值:

{$user.name|default="该变量你忘了传数据啦"}

使用函数

在模板中,当开发者需要对变量使用函数时,可以这样用:

{$data.name|md5}

编译后的结果是:

<?php echo htmlentities(md5($data['name'])); ?>

其中htmlentities方法是系统默认添加的(无需手动指定。)
如果你不需要转义(例如你需要输出html表格等内容),可以使用:

{$data.name|raw}

编译后的结果是:

<?php echo $data['name']; ?>

系统还内置了以下八个固定的过滤规则(不区分大小写)

过滤方法描述
date日期格式化(支持各种时间类型)
format字符串格式化
upper转换为大写
lower转换为小写
first输出数组的第一个元素
last输出数组的最后一个元素
default默认值
raw不使用(默认)转义

例如

{$data.create_time|date='Y-m-d H:i'}
{$data.number|format='%02d'}

如果函数需要传递多个参数,可以这样使用:

{$data.email|substr=0,15}

编译后的结果是:

<?php echo htmlentities(substr($data['email'],0,15)); ?>

还支持多函数同时使用,多个函数之间用|符号分隔,例如:

{$data.name|md5|sha1|substr=5,10}

编译后的结果是:

<?php echo htmlentities(substr(sha1(md5($data,name)),5,10)); ?>

多函数的调用顺序时从左到右依次执行的,而系统附加的过滤规则会在最后加上。
如果你觉得这样的调用顺序不好记忆,或者不想调用系统的过滤规则,也可以这样写:

{:substr(sha1(md5($data.name)),5,10)}

运算符

在SW-X中,也支持对变量使用常用的PHP运算符,仅支持以下八种。

运算符使用示例
+{$a+$b}
-{$a-$b}
*{$a*$b}
/{$a/$b}
%{$a%$b}
++{$a++}{++$a}
--{$a--}{--$a}
综合运算{$a+$b*10+$c}

注意,当我们使用运算符的时候,系统附加的过滤规则函数则不会再使用。

三元运算

SW-X中,也支持视图中使用三元运算符,例如:

{$data.sex ? '有年龄' : '还没出生呢'}
{$data.sex ? $info.email : $info.phone }

也支持运算判断:

{$data.sex >= 18 ? '成年啦' : '未成年'}

同时也支持PHP7的三元精简写法:

{$name ?? '没输入'}

原样输出

我通常在编辑技术的文档等相关视图的时候,都会用到跟PHP语法相关的html内容,这时候会想系统不对某一区域内的内容进行模板解析,可以使用literal标签,例如:

{literal}
    Insist, {$name}!
{/literal}

模板布局

SW-X的模板引擎内置了布局模板功能支持,可以方便的实现模板布局以及布局嵌套功能。
需要在/config/view.php配置文件中,将layout_on项改为truelayout_name为布局文件路由。

开启layout_on后,我们的模板渲染顺序会发生变化,例如我们以为index/index路由视图为例:

在不开启layout_on布局模板之前,会直接渲染
app/view/index/index.html模板文件,
开启之后,会先渲染:
app/view/layout.html模板,布局模板的写法和其他模板的写法类似,本身也可以支持所有的模板标签以及包含文件,
区别在于有一个特定的输出替换变量{__CONTENT__},例如:
下面是一个典型的layout.html模板的写法:

{include file="public/header" /}
	{__CONTENT__}
{include file="public/footer" /}

经过上面布局文件的解析后,解析顺序则为:
includeapp/view/public/header.html文件。
includeapp/view/index/index.html文件,并将其最终解析的内容替换到{__CONTENT__}关键字。
最后再includeapp/view/public/footer.html文件。

包含文件

在上面的模板布局中,我们已经了解到SW-X在模板中是使用include包含文件,其file参数为视图文件对应的路由地址。
例如:

{include file="public/header" /}

则会includeapp/view/public/header.html文件。

输出替换

在实际开发中,我们经常需要配置一些静态文件的前置路由段,例如/public/index/js//public/index/css/其中的/public/index则为前置路由段。
为了方便维护,我们则需要使用到模板输出替换,注意:配置项严格区分大小写。
/config/view.php中,我们新增一个tpl_replace_string配置项(默认情况下是不存在的)并写入以下内容:

'tpl_replace_string'  =>  [
    '__INDEX__'=>'/public/index',
]

然后我们就可以在模板中这样使用:

<script src="__INDEX__/js/jquery.min.js"></script>

循环标签

视图的循环标签支持三种:foreachswlistfor
foreachswlist为遍历数组。
for则是创建循环递归器。

foreach标签

foreach标签的语法与原生PHPforeach基本一致:

{foreach $list as $key=>$value} 
    {$value.id} -> {$value.name}
{/foreach}

swlist标签

swlist标签的作用与foreach标签一致,但其语法更偏向模板引擎标签的写法:

{swlist name="list" id="value" key="key"}
    {$value.id} -> {$value.name}
{/swlist}

如果你想限制只读取前20条数据,可以加入length="20"的属性。

如果你想限制只从第10条才开始读取,可以加入offset="10"的属性。

for标签

for标签的语法,更偏向模板引擎标签的写法:

{for start="开始值" end="结束值" step="步进值" name="循环变量名" }
{/for}

name的默认值是istep的默认值是1,举例如下:

{for start="1" end="100"}
{$i}
{/for}

比较标签

比较标签常用于代替一些简单的判断语句,复杂的判断条件都应用if标签替换使用,比较标签为八个用法一致的标签组,其标签支持如下:

标签含义
eq或者 equal等于
neq 或者notequal不等于
gt大于
egt大于等于
lt小于
elt小于等于
heq恒等于
nheq不恒等于

例如,要求sex变量的值大于等于18就输出,可以使用:

{egt name="sex" value="18"}你成年啦{/egt}

条件判断

视图的条件判断标签支持两种:switchif
其语法都与原生PHP的语法相似。

switch标签

例如我们要判断订单状态:

{switch $order.status}
    {case 1}已支付{/case}
    {case 2}已出货{/case}
    {case 3}带退货{/case}
    {case 4}已退货{/case}
    {default /}暂无该状态
{/switch}

同时,case中也支持变量的传递,例如我们要判断学生的分数段

{switch $fraction}
    {case $fraction > 90}是个高手啊{/case}
    {case $fraction > 80}优秀{/case}
    {case $fraction > 70}还行{/case}
    {case $fraction > 60}刚刚好{/case}
    {default /}不及格
{/switch}

if标签

if标签基本与原生的IF一致,其用法如下,例如上面的判断学生成绩:

{if ($fraction > 90)}是个高手啊
{elseif ($fraction > 80) /}优秀
{elseif ($fraction > 70) /}还行
{elseif ($fraction > 60) /}刚刚好
{else /}不及格
{/if}

标签嵌套

SW-X中,循环标签和判断标签都均支持多重循环嵌套。

原生PHP

在实际开发中,配合循环标签等经常会用到原生得PHP代码做一些统计项等操作,这时候可以用到php标签,其语法如下:

{php}
    echo 'Insist, '.$name.'!';
{/php}

注意:{php}标签中只能使用原生的PHP语法,不能再使用任何模板引擎的语法,例如:{$data.name}之类的。

验证码

SW-X内置了一个图形验证码类,基于\x\Verify类做驱动支持。

下面,我们先来看下/config/app.php中,有关验证码的相关配置信息:

<?php
// +-----------------------------
// | 验证码设置
// +-----------------------------

'verify'             => [
    // 验证码字体大小(px)
    'fontsize' => 20,     
    // 验证码图片高度 
    'height'   => 32,      
    // 验证码图片宽度
    'width'    => 150,  
    // 验证码位数   
    'length'   => 4,       
    // 验证码字体样式
    'ttf' 	   => '6.ttf', 
    // 验证码过期时间,单位:秒
    'expire'   => 60,      
    // 是否添加混淆曲线
    'curve'	   => true,	   
    // 是否添加杂点
    'noise'	   => true,	 
    // 发起验证后是否需要更新验证码  
    'update'   => true,
],

生成验证码

$this->verify();

entry方法用于生成验证码,同时该参数可传递3个参数,分别为:
验证码类型:可传入1、2;分别对应英数、数学运算等2种图形模式,默认为1
验证码的seesion名称:默认为__vif__;
验证码参数:参考/config/app.php中的verify节点。

核验验证码

$this->verify_check(输入你看到的验证码);

check方法用于核验验证码,同时该参数可传递2个参数,分别为:
验证码内容
验证码的seesion名称:默认为__vif__;

文件上传

内置的上传只是上传到本地服务器,上传到远程或者第三方平台的话需要自己扩展。

假设表单代码如下:

<form action="/index/upload" enctype="multipart/form-data" method="post">
<input type="file" name="image" /> <br> 
<input type="submit" value="上传" /> 
</form> 

然后修改Index.php控制器为如下的代码:

<?php
namespace app\controller;
use x\Controller;

class Index extends Controller
{
    /**
     * 输出视图
     * @RequestMapping(route="/", method="get", title="主页")
    */
    public function index() {
        // 模板渲染
        return $this->view('index');
    }

    /**
     * @RequestMapping(route="index/upload", method="post", title="上传文件Demo")
    */
    public function index() {
        // 获取表单上传文件 例如上传了001.jpg
        $file = $this->file('image');
        // 移动到框架应用根目录/uploads/ 目录下
        $info = $file->move(ROOT_PATH.'/uploads');
        if($info){
            // 成功上传后 获取上传信息
            // 输出 保存的相对路径 /uploads/文件保存地址
            echo $info->getSaveName();
            // 输出 保存的文件名
            echo $info->getFilename(); 
        }else{
            // 上传失败获取错误信息
            echo $file->getError();
        }
    }
}

上传验证

支持对上传文件的验证,包括文件大小、文件类型和后缀:

<?php
namespace app\controller;
use x\Controller;

class Index extends Controller
{
    /**
     * 输出视图
     * @RequestMapping(route="/", method="get", title="主页")
    */
    public function index() {
        // 模板渲染
        return $this->view('index');
    }

    /**
     * @RequestMapping(route="index/upload", method="post", title="上传文件Demo")
    */
    public function index() {
        // 获取表单上传文件 例如上传了001.jpg
        $file = $this->file('image');
        // 移动到框架应用根目录/uploads/ 目录下
        $info = $file->validate(['size'=>15678,'ext'=>'jpg,png,gif'])->move(ROOT_PATH.'/uploads');
        if($info){
            // 成功上传后 获取上传信息
            // 输出 保存的相对路径 /uploads/文件保存地址
            echo $info->getSaveName();
            // 输出 保存的文件名
            echo $info->getFilename(); 
        }else{
            // 上传失败获取错误信息
            echo $file->getError();
        }
    }
}

如果上传文件验证不通过,则move方法返回false。

验证参数说明
size上传文件的最大字节
ext文件后缀,多个用逗号分割或者数组
type文件MIME类型,多个用逗号分割或者数组

上传配置

SW-X对文件上传有默认配置,存放在/config/app.phpfile节点下。

<?php
// +-----------------------------
// | 文件上传配置
// +-----------------------------

'file' => [
    // 最大上传大小(KB)
    'size' => 15678,
    // 允许上传路径
    'ext' => 'jpg,jpeg,png,gif',
    // 保存目录不存在是否自动创建
    'auto_save' => true,
    // 文件名生成算法,支持sha1,md5,time三种
    'name_algorithm' => 'time',
    // 文件默认保存目录
    'path' => ROOT_PATH.'/upload/',
],

系统默认提供了几种上传命名规则,包括:

规则描述
date根据日期和随机数生成
md5对文件使用md5(time规则)散列生成
sha1对文件使用sha1(time规则)散列生成

上传文件组件暂不支持多文件上传功能,但可以对$this->file($FILES['pic'][0])的方式实现循环上传。

注意:在Swoole中默认文件最大上传限制只有2M,我们除了修改框架的文件上传限制外,还需要修改server.php配置文件中的package_max_length选项,该参数在v1.2.11版本后才加入。

HTTP请求监控组件

v1.2.22版本起支持,在/config/server.php配置文件中,通过http_monitor_status参数开启监控。
同时在配置该文件中,可以修改默认的控制台账号密码,默认为:swoolex

该组件会在onRequest阶段记录每一个请求的状态,并存储在/runtime/HttpMonitor/目录中,以天为单位进行存储。

该组件主要用于代替PHP-FPMstatus服务,便于后期请求进程BUG排查,不建议生产环境中长时间开启,健康的服务中不建议开启该组件。

注意:HTTP监控的WEB组件,需要通过sw-x monitor startCMD命令进行创建。

WebSocket服务使用规范

在SW-X中,默认是使用官方提供的json数据交互模式。然后会根据请求的json报文中的action字段作为请求路由,转发到/app/socket/目录下。
/app/socket/就相当于HTTP的/app/controller/为控制器层。
该目录下的所有socket控制器类均都需要继承于\x\WebSocekt基类。
命名和使用规范一致参考HTTP的Controller规范约定。

WSS支持

SW-X中开启WSS很简单,只需要修改/config/websocket.php配置项中的,ssl_cert_filessl_key_file证书路径即可。

自定义请求处理

很多时候官方预设的JSON通信方式并不一定适合所有开发者,所以SW-X支持自定义自己的通信方式,只需要修改/config/websocket.php配置中的is_onMessagefalse即可。
这样就表示框架不再监听onMessage事件,改由自己监听/app/event/下的onMessage事件,进而由自己实现数据分包。

官方请求处理

/config/websocket.php配置中的is_onMessagetrue时【默认true】,则启用框架对onMessage事件进行监听。

SW-X只支持JSON格式的数据包提交,支持启用AES数据加解密,只需要修改/config/websocket.php中的配置项即可。

onMessage事件中接收到的数据格式为:

{
    "action":"请求路由",
    "data":请求数据
}

事件控制器

WebSocekt的控制器会根据action字段进行路由匹配,最终找到/app/socket/目录下的控制器文件进行处理。
改目录下的事件控制器,都需要继承\x\WebSocekt基类。

<?php
namespace app\socket;
use x\WebSocket;

/**
 * @Controller(prefix="test")
*/
class Index extends WebSocket
{
    /**
     * @RequestMapping(route="/index", title="action为test/index访问这里")
    */
    public function index() {
        return $this->fetch(200, '描述', []);
    }

    /**
     * @RequestMapping(route="/demo", title="action为test/demo访问这里")
    */
    public function demo() {
        return $this->fetch(301, '描述');
    }
}

获取请求参数

v1.2.2版本起,支持使用$this->param()方法接收json数据包中的data参数,如果启用AES加密传输,则会自动解密后返回。

输出返回值

WebSocket控制器中输出返回值跟HTTP控制器一样,都是调用fetch()方法,只不过传入的参数格式不一样。

return $this->fetch(状态值, 描述, 返回值[默认是空数组])

最终的返回格式如下:

{
    "action":"状态值",
    "msg":"描述",
    "data":"返回值",
}

关于RPC微服务

SW-X的RPC微服务主要实现了TCP的跨应用调用请求。
客户端的节点获取使用简单的评分制,平均获取可用的服务节点,当定时轮询ping到无法使用的节点时,会通过生命周期进行相关消息通知。
同时,同一个服务可以配置无数个节点,方便应用负载。

数据加密传输

RPC的数据传输默认开启了AES加密方式,若需要明文传输即可,则在/config/rpc.php配置中关闭即可。
注意:当客户端或服务端开启或关闭加密方式时,另一端必须保持参数一致。

启动服务

RPC服务端启动服务很简单,在/config/server.php配置中修改自己想要的端口号,然后在cmd中执行php sw-x start rpc即可启动服务。

服务存放

RPC服务端的逻辑代码,均需要统一存放在/app/rpc/目录中,并且无需继承任何基类。
同时注意:

1、每个被允许访问的rpc操作方法,都必须时public类型,同时不能为static
2、不能定义名为$headers、$param的成员属性变量,因为该变量已被系统赋值占用
3、rpc操作方法的返回值,既为客户端所获得的返回值。

请求参数获取

RPC的请求参数获取可以直接通过$this->param直接获取。

请求头获取

RPC的请求头获取可以直接通过$this->headers直接获取。

返回值

RPC的返回值为操作方法的return内容,例如下面的案例,返回了一个数组,那么客户端获取到的也将会是这个数组。

namespace app\rpc\order;

class create {

	// rpc - order/create->run()服务
	public function run() {
		// 请求参数
		var_dump($this->param);
		// 请求头
		var_dump($this->headers);
		// 返回值
		return ['id' => 1];
	}
	
}

请求参数配置

RPC客户端的相关配置参数在/config/rpc.php文件中进行修改。
客户端请求发包,是使用了Swoole的\Swoole\Coroutine\Client组件。

服务初始化

客户端的初始化服务配置在/rpc/map.php文件中进行管理。
配置格式如下:

// 路由名称
'order/create' => [
	// 操作方法
	'run' => [
		// 多个连接池
		[
			'title' => '30机器', // 节点名称
			'ip' => '127.0.0.1', // 节点IP
			'port' => '9502', // 节点端口
		]
	]
],

轮询延迟检测

RPC客户端的延时检测,是按照轮询3秒一次的shell ping命令进行IP检测,若ping不通,则标记该节点不可用。
若ping通过,则记录当前延时,并修改该节点评分值。

服务评分

RPC客户端的评分是根据ping的多次延时浮动来进行加减管理,评分越高的节点会被优先使用。

服务获取

RPC客户端的节点获取,是依靠评分值、当前节点请求数、当前节点延时数来进行排序选取的。

更新某条配置

RPC客户端除了在/rpc/map.php配置文件中设置节点外,还支持动态设置某个节点。
调用方法为:
\x\Rpc::run()->setOne(请求类名, 请求方法, 节点参数);
当节点存在时为修改,节点不存在时为新增,例子如下:

\x\Rpc::run()->setOne('order/create', 'run', [
	'title' => '30机器', // 节点名称
	'ip' => '127.0.0.1', // 节点IP
	'port' => '9502', // 节点端口
]);

删除某条配置

RPC客户端也支持动态删除某条节点,
调用方法为:
\x\Rpc::run()->deleteOne(请求类, 请求方法, 节点名称);
当节点名称为空时,则代表删除该请求方法下的所有节点,例子如下:

\x\Rpc::run()->deleteOne('order/create', 'run', '30机器');

调用服务

RPC客户端调用代码,主要依赖\x\RpcClient框架核心类。
使用:RpcClient->run()方法发送RPC请求。
参数支持如下:
RpcClient->run(请求类,请求方法, 请求参数=[], 请求头=[])
成功返回RPC内容,失败返回false,例子如下:

$Rpc = new \x\RpcClient();
$body = $Rpc->run('order/create', 'run', [
	'id' => 1,
	'name' => '小黄牛会员'
]);

请求超时设置

有时候当我们的RPC节点很多时,若多个节点均被攻击或请求过高,无法请求成功时,系统是会自动轮询下一个节点,直到其中一个节点请求成功时,再返回请求结果的。这样就会导致客户端可能被堵塞等待很长时间。
为了应对这种情况,SW-X的RPC服务支持配置每个RPC请求的最大超时时间,单位为(s)秒。
全局的配置参数在/config/rpc.php配置文件中,默认为30秒。

若需要配置单个请求,可以使用RpcClient->set('out_time', 10)方法。

判断是否请求成功

有时候RPC服务只返回bool布尔值,这时候我们单纯的通过if run()结果,是没办法正确判断是否真的请求成功。
为了应对这种请求,SW-X提供了一个RpcClient->isSuccess()方法,用于判断是否真正请求成功。
使用例子如下:

$Rpc = new \x\RpcClient();
$body = $Rpc->run('order/create', 'run', [
	'id' => 1,
	'name' => '小黄牛会员'
]);
if ($Rpc->isSuccess()) {
	var_dump($body);
} else {
	var_dump('no~!');
}

获取错误日志

RPC客户端请求失败,run()方法只会返回false,要获取错误日志内容,需要通过RpcClient->getMsg()方法获取。

获取错误状态码

RPC客户端请求失败的原因有很多种,我们除了通过RpcClient->getMsg()方法查看错误内容外,还可以通过RpcClient->getMsg()方法查看错误状态码,若请求成功,状态码则为200

容器的设计原理

容器是v1.2.1版本后才接入的框架核心,SW-X的容器实现是双容器模式,分为进程级和请求级容器,进程级容器为Worker容器,用于存储框架核心的对象实例,开发者无需关心进程级容器的调用和维护。
请求级容器为应用容器,主要用于存储请求级的对象实例,例如我们在控制器中使用容器set了一个对象,那么get出来的,也只是这个请求对应的set对象,不会获取到别的请求实例,实现了真正的请求隔离。
同时,在Swoole中静态类不会自动释放内存空间,如果开发者自定义静态类就需要非常小心的维护其内存栈,这时候就应该将静态类的实例存储在容器中,在请求结束时,容器会自动销毁该请求持有的所有容器对象。

has

容器的调用,基于框架\x\Container::getInstance()实例,其实现为单例模式。
调用has()方法,可以检测该对象是否存储在容器中。成功返回true,失败返回false

set

容器的调用,基于框架\x\Container::getInstance()实例,其实现为单例模式。
调用set()方法,写入一个value到容器中,value可以是一个闭包或者实例。该方法没有返回值。
调用方法如下:

<?php
\x\Container::getInstance()->set(KEY, VALUE);
// KEY 为容器标识符,唯一
// VALUE 为闭包或对象实例

get

容器的调用,基于框架\x\Container::getInstance()实例,其实现为单例模式。
调用get()方法,获得一个容器存储实例。成功返回存储内容,失败返回false。 调用方法如下:

<?php
\x\Container::getInstance()->get(KEY);
// KEY 为容器标识符,唯一

delete

容器的调用,基于框架\x\Container::getInstance()实例,其实现为单例模式。
调用delete()方法,立即删除一个容器存储实例。该方法没有返回值。 调用方法如下:

<?php
\x\Container::getInstance()->delete(KEY);
// KEY 为容器标识符,唯一

注解实现方式

SW-X的注解主要依赖反射类实现,通过扫描HTTP/SOCKET控制器目录解析整个应用的注解项并常驻内存中。

注解支持范围

由于注解是基于路由表实现的依赖注入,所以注解只对路由加载的主控制器方法有效,当再主方法内调用其他成员方法,又或者跨控制器调用它类方法时,依旧只有主控制器的注解有效。

下面我们用一个HTTP的控制器来讲解下:

<?php
namespace app\controller;
use x\Controller;

class Index extends Controller
{
    /**
     * 输出视图
     * @RequestMapping(route="/", method="get", title="主页")
    */
    public function index() {
        $Db = $this->test();
    }

    /**
     * @Ioc(class="\x\Db", name="Db")
     * @RequestMapping(route="/test", method="post", title="测试获取")
    */
    public function test() {
        var_dump($this->Db);
        return $this->Db;
    }
}

上面的方法,在我们访问/test路由的时候,Db属性是注入成功的,但当我们访问/路由的时候就会发现,test()方法是错误的,因为注解没有被继承,所以Ioc实际上并没有执行。

SW-X的注解,只对被路由器载入的控制器主方法有效。

当然,调用流程上,主方法挂载的注解,其流程上调用到的后续方法都可以使用。

注解规范

SW-X中使用注解注入需要极强的规范要求,否则注解会解析失败,具体要求如下:

  • 1、注解必须在/** */注释体内所包含
  • 2、一行注释为一条注解
  • 3、只对框架内置的注解元有效
  • 4、每条注解中的属性参数,都必须使用双引号做标记,例如:@Ioc(class="依赖的类", name="注入的成员属性名称")
  • 5、基本所有注解都是属性名称="",的方式传递参数,都是强制""双引号,后面接入一个,英文逗号

SW-X中支持的所有注解元如下,也可以参考这个作为注解的使用规范:


/**
 * @TestCase(class="\testcase\index\test1", title="用例一")
 * @Ioc(class="\x\Db", function="name('user')", name="Db")
 * @AopBefore(class="app\aop\Demo", function="before")
 * @AopAfter(class="app\aop\Demo", function="after")
 * @AopAround(class="app\aop\Demo", function="around")
 * @AopThrows(class="app\aop\Demo", function="throws")
 * @Param(name="id", type="int|string", value="1", empty="true", min="10", max="20", chinese="true", callback="\lifecycle\annotate_param")
 * @RequestMapping(route="/index", method="GET|POST", title="路由描述")
 * @Controller(prefix="user")
 * @onRoute
*/

每个注解元的解释如下:

  • TestCase:单元测试绑定
  • Ioc:属性注入
  • AopBefore:AOP前置操作
  • AopAfter:AOP后置操作
  • AopAround:AOP环绕操作
  • AopThrows:AOP异常转发
  • RequestMapping:方法对应的路由绑定
  • Controller:控制器的前置路由绑定
  • onRoute:申明某个方法不能被路由访问
  • Param:对GET或POST参数进行前置校验

Ioc注入

主要依赖 * @Ioc()注解元实现,主要作用是对某个方法进行前置的成员属性注入。主要参数有三个:

  • class:类文件的命名空间地址
  • function:需要调用的class类对应的方法,该参数可为空。
  • name:需要注入的成员属性名称,注意:如果被注入的方法是静态的,那么请先在类的内部定义好该成员属性,并且只能为public权限,否则将注入失败。

具体使用案例如下:

<?php
namespace app\controller;
use x\Controller;

class Index extends Controller
{
    /**
     * Ioc注入可以多个同时使用
     * @Ioc(class="\x\Db", name="Db")
     * @Ioc(class="\x\Redis", name="Redis")
     * @Ioc(class="\x\Db", function="name('user')", name="User")
    */
    public function index() {
        var_dump($this->Db);
        var_dump($this->Redis);
    }
}

Aop绑定

主要依赖 * @Aop*()注解元实现,主要作用是实现AOP切面注入行为。该注解元主要参数有2个:

  • class:类文件的命名空间地址
  • function:指定接收的class类对应的方法,该参数为空的时候默认为【run】。

Aop主要支持以下4个注解元:

AopBefore 前置
AopAfter 后置
AopAround 环绕
AopThrows 异常处理

具体使用案例如下:

<?php
namespace app\controller;
use x\Controller;

class Index extends Controller
{
    /**
     * 注意:同一类的Aop操作,只会生效一个
     * @AopBefore(class="app\aop\Demo", function="before")
     * @AopAfter(class="app\aop\Demo", function="after")
     * @AopAround(class="app\aop\Demo", function="around")
     * @AopThrows(class="app\aop\Demo", function="throws")
    */
    public function index() {
        return $this->fetch('AOP注入测试');
    }

}

同时我们需要注意,对应的function都应该接收两种AOP定义的不同属性,例如上面实例中所提到的app\aop\Demo类:

<?php
namespace app\aop;

class Demo
{
    //aop 除了异常通知,其余AOP事件都需要return true程序才会向下执行,否则会抛出异常
    //aop 都需要接收以下参数格式

    // 前置
    public function before() {
        return true;
    }
    // 后置
    public function after() {
        return true;
    }
    // 环绕
    public function around() {
        return true;
    }
    // 异常通知
    public function throws($error) {

    }

}

SW-X建议:所有的AOP类都应该统一存放在/app/aop/目录下,当然这不是强制的。

注意:同一类的Aop操作,只会生效一个。例如,同时标注两个AopBefore,只有最后一个会生效。

路由绑定

注解元中关于路由的操作比较多,主要有以下三类:


RequestMapping 控制器对应方法使用的注解元,用于绑定路由
Controller 控制器类使用的全局注解元,用于绑定该类下的全局路由前缀
onRoute 控制器对应方法使用的注解元,用于声明该方法不允许通过路由访问

RequestMapping注解元主要有3个参数:

  • route:路由规则
  • method:允许的请求类型,默认为空表示不限制,多个类型允许用|号间隔,例如:method="GET|POST"
  • title:路由描述,允许为空

Controller注解元只有1个参数:

  • prefix:路由规则

onRoute注解元没有参数,所以在使用的时候我们不需要在后面带()符号,只需要这样使用:@onRoute即可

使用案例如下:

<?php
namespace app\controller;
use x\Controller;

/**
 * @Controller(prefix="index")
*/
class Index extends Controller
{
    /**
     * @RequestMapping(route="/test", method="GET|POST", title="我是测试路由")
    */
    public function index() {
        return $this->fetch('我的路由是:index/test');
    }

    /**
     * @RequestMapping(route="/demo")
     * @onRoute
    */
    public function demo() {
        return $this->fetch('虽然我定义了路由,但用index/demo,你并不能访问我');
    }
}

参数过滤

#Param注解从v1.1.4版本起支持。
用于对请求参数的过滤,分别支持参数传递如下:

<?php
name:参数名称
method:注解启用类型,支持GET或POST输入,只对HTTP请求有效,当输入该参数时,只有请求类型相符该注解才会生效,默认为空直接生效(v1.2.23版本起支持)
type:参数类型,多个支持使用|分隔
value:为空是对参数的预设值
empty:是否不为空,默认false,传入true既开启判断
min:最小长度
max:最大长度
chinese:判断长度是否使用mb_strlen,默认false,传入true既使用
tips:当过滤不通过时,输出的提示内容,不传入则使用系统编译提示
regular:正则表达式
callback:当过滤不通过时,系统调用的回调处理类,或函数。不填时,默认使用系统生命周期处理:\lifecycle\annotate_param->run();

注意:
1、PHP中对参数是弱类型处理,例如POST(int)1,PHP中接收到的参数即为:(string)1,所以在type中需要小心使用int类型判断,否则很容易造成is_int函数不通过。
2、callback支持自定义处理类,直接传入类的命名空间地址即可,系统会默认调用该类的run()方法,也可以输入函数名称。具体的参数接收,跟处理过程,可以参考\lifecycle\annotate_param->run()类。

一个完整的参数过滤注解大概如下:

<?php
/**
 * @RequestMapping(route="param", method="get", title="测试param注解")
 * @Param(name="id", method="POST", type="int|string", value="1", empty="true", min="10", max="20", chinese="true", callback="\lifecycle\annotate_param")
*/
public function param() {
    return $this->fetch(null);
}

可以解释为:
HTTP请求类型为POST请求时该注解生效,或者WebSocket请求时生效,
param参数id,不允许为空
不存在或为null时,默认为1
只允许intstring类型
最小长度10
最大长度20
不使用mb_strlen获取字节长度
以上参数不通过时,使用\lifecycle\annotate_param类处理回调。

什么是单元测试

单元测试注解支持,从v1.2.17版本起支持。
主要实现,通过使用$Db->test()方法标记声明Db语句,同时在注解中通过TestCase注解元,绑定对应的测试用例,再通过CMD命令行,使用php sw-x test [服务类型] [路由地址]的方式,发起单元测试调试。
标记声明Db语句,在单元测试时可防止DAO污染,达到了数据隔离的效果。
同时,同个路由地址,可以绑定多个TestCase单元测试,会按绑定顺序依次执行。

单元测试的当前支持

1、当前单元测试注解,只支持http服务,暂不支持websocketserver服务。
2、单元测试注解,只对action注解生效,对全局controller注解无效。

注意事项

主要依赖 * @TestCase()注解元实现,作用是实现单元测试,防止数据库DAO污染隔离。主要参数有2个:

  • class:用例类文件的命名空间地址
  • title:可不声明,用于简单说明该用例的场景。

1、官方建议,但不强制要求,@TestCase()class类文件,都应该统一存放在/testcase/目录下。
2、所有绑定了单元测试的操作方法,如果都含有Db操作,其Db语句,都应该使用test()方法,申明数据隔离标识,防止DAO被测试污染。

命名规范

1、建议测试文件,应该存放在/testcase/目录下。
2、所有测试文件,都应该继承至\x\doc\lable\TestBasics抽象基类,并实现public function getData() : array{}public function getHeaders() : array{}方法。
3、当申明了Db数据隔离标识时,测试文件中其对应的数据申明,应该以成员变量的方式进行存储,访问权限只能为public

案例DEMO

下面提供一个完整的测试demo:
1、HTTP控制器文件\app\controller\Index.php

<?php
namespace app\controller;
use x\Controller;

/**
 * @Controller(prefix="")
*/
class Index extends Controller
{
	
	/**
	 * @TestCase(class="\testcase\index\test", title="用例一")
	 * @TestCase(class="\testcase\index\test", title="用例二")
	 * @TestCase(class="\testcase\index\test", title="用例三")
	 * @RequestMapping(route="/testcase", method="get", title="单元测试注解demo")
	 * @Ioc(class="\x\Db", name="Db")
	*/
	public function testcase() {
		$list = $this->Db->name('admin')->test('A1')->find();
		$this->Db->return();
		
		if ($list['name'] == '1') {
			return $this->fetch('使用测试用例');
		} else {
			return $this->fetch($list['name']);
		}
	}
}

2、单元测试文件\testcase\index\test.php

<?php
namespace testcase\index;
// 必须继承至单元测试抽象类
use \x\doc\lable\TestBasics;

class test extends TestBasics
{
	/**
	 * A1-数据库DB
	*/
	public $A1 = [
		'name' => '1',
	];

	// 返回请求数据结构
	public function getData() : array 
	{
		return [];
	} 

	// 返回请求头
	public function getHeaders() : array 
	{
		return [];
	}
}

这时候,我们在CMD命令行界面,输入php sw-x test http /testcase就能查看到对应的测试结果。
效果如下图:


而如果我们直接在浏览器中访问这个路由,则不受影响。

什么是自定义注解

注解机制支持自定义实现,从v1.2.16版本起支持。
主要用于支持开发者自定义实现除系统内置注解元以外的,任意自定义注解标签,在新的源码包中实现了一个Test注解标签案例,可参考查看。

使用场景

可能有人会说,官方已经实现了这么丰富的注解标签支持,为什么还要浪费性能实现自定义注解呢?
下面我们来看个场景:
项目A需要实现一个复杂的前置操作挂载,中间涉及了1,2,3种鉴权流程,A接口权限高3种都要挂,B接口只需要挂后两种,这时候原系统内置的AopBefore前置注解就没办法很好的实现了。
而自定义注解的实现,就能很自由多变的应对这类场景。

注意事项

1、自定义的注解标签都应该统一存放在/annotation/目录下;
2、自定义的注解标签均可以多次声明;
3、同个操作方法中多次声明同一个自定义的注解标签时,系统只会回调一次,但会携带多个标签参数,按声明顺序组合成list传入。

命名规范

1、所有自定义的注解标签,都需要统一继承至\x\doc\lable\Basics注解基类;
2、并统一实现public function run($route, $type){}接口,用于注解回调处理;
3、当注解逻辑处理不通过时,应对调用return $this->route_error(自定义说明);方法,用于中断后续流程;该方法最终会除非框架的route_error生命周期处理;
4、当逻辑处理通过时,应对调用return $this->_return();方法,用于告知系统继续向下执行;
5、假设,当我们声明一个注解标签为@TestCase注解元时,/annotation/下对应的文件名(类名)应该为TestCase.php

案例DEMO

下面我们就来看下系统自带的案例注解标签@Test,是如何实现的吧。
1、首先,我们先在/annotation/目录下,声明一个Test.php文件,并写入以下代码:

<?php
namespace annotation;
use \x\doc\lable\Basics;

class Test extends Basics
{
    /**
        * 启动项
        * @todo 无
        * @author 小黄牛
        * @version v1.2.10 + 2020.07.30
        * @deprecated 暂不启用
        * @global 无
        * @param array $route 路由参数
        * @param type int 路由类型 1.控制器注解 2.操作方法注解
        * @return bool 返回true表示继续向下执行
    */
    public function run($route, $type){
        // $route是多维数组
        // 当同一注释中,多次声明同一个注解时,只会回调一次,多次参数分别存放在该数组中
        var_dump($route);
        var_dump($type);

        // return route_error函数抛出自定义错误异常
        return $this->route_error('Msg内容自己随便写啦');

        // 若注解通过,应该调用_return()函数,代替return true;
        return $this->_return();
    }
}

2、然后,我们就可以选择一个控制器类,进行注解元挂载测试了:

<?php
namespace app\controller;
use x\Controller;

/**
 * @Test(msg="我是自定义的注解1")
 * @Test(msg="我是自定义的注解2")
 * @Test(msg="我是自定义的注解3")
 * @Controller(prefix="")
*/
class Index extends Controller
{
    /**
     * @Test(msg="我是自定义的注解1")
     * @Test(msg="我是自定义的注解2")
     * @Test(msg="我是自定义的注解3")
     * @RequestMapping(route="/", method="get", title="主页")
    */
    public function index() {
        return $this->fetch('我是主页');
    }

}

重启服务之后,我们访问该路由地址,就能在XShell里查看对应的注解参数,在浏览器中可以查看到生命周期回调的处理信息。

支持范围

SW-X的路由只对HTTPWebSocket服务有效,并且如果是WebSocket服务,则必须启用框架处理模式。

路由模式

SW-X的路由模式是共用的,也就是说,不管是HTTP还是WebSocket服务,都是可以公用框架的路由配置,具体看:HTTP服务路由篇

请求类型过滤

在路由注解元中我们通过method参数可以前置限制路由的请求类型,若请求不符合,则会被框架调用404重定向逻辑,具体配置404参考:自定义404篇

获取全站路由表

可能会有开发者有疑问,如果注解中使用了很多路由规则,那我要怎么知道都有哪些路由呢?为了应对这个问题,SW-X提供了获取应用全路由表的方法。
具体使用方法如下:

$array = \x\route\Table::route(); // 获取全部路由
$http = $array['http'];
$websokcet = $array['websokcet'];

最终返回的数组结构是个多维数组,下面是可能存在的节点:

[
    '路由地址' => [
        'n' => 命名空间地址,
        'name' => '方法名称',
        'method' => '请求类型',
        'title' => '路由描述',
        'father' => 父class的注解
        'own' => function本身的注解
    ]
]

设置请求地址

SW-X从v1.2.11版本起,支持HTTP客户端,用于代替PHP-FPM的CURL模块。
组件的依赖命名空间为:\x\Client,调用http()方法建立HTTP客户端实例。
示例:

<?php
$httpClient = new \x\Client();
$httpClient->http();

建立HTTP客户端实例后,调用domain()方法,传入请求地址,该方法返回当前实例,可进行链式操作。

<?php
$httpClient = (new \x\Client())->http();
$httpClient->domain('https://www.sw-x.cn/api.html');

设置请求体

请求中,如果需要传递请求参数,可以调用body()方法,该方法返回当前实例,可进行链式操作。

<?php
$httpClient = new \x\Client();
$res = $httpClient->http()
	   ->domain('https://www.sw-x.cn/api.html')
	   ->body(['name' => 'SW-X']);

发送请求

HTTP客户端组件,支持3类请求:getpostdownload。成功返回请求内容。

get

用于发起GET请求:

<?php
$httpClient = new \x\Client();
$res = $httpClient->http()
		->domain('https://www.sw-x.cn/api.html')
		->body(['name' => 'SW-X'])
		->get();

post

用于发起POST请求:

<?php
$httpClient = new \x\Client();
$res = $httpClient->http()
		->domain('https://www.sw-x.cn/api.html')
		->body(['name' => 'SW-X'])
		->post();

download

用于下载远程文件,该方法可传入两个参数:
filename:文件保存路径
offset:是否覆盖文件,为0时覆盖,默认为0

<?php
$httpClient = new \x\Client();
$res = $httpClient->http()
		->domain('https://www.sw-x.cn/img/Logo.png')
		->download();

获取errCode

调用errCode()方法,获得请求的错误状态码,errCode 的值等于 Linux errno

<?php
$httpClient = (new \x\Client)->http();
$res = $httpClient->domain('https://www.sw-x.cn/api.html')
		->body(['name' => 'SW-X'])
		->post();

$errCode = $httpClient->errCode();

获取statusCode

statusCode为请求的HTTP响应状态码,可用于判断Swoole-Client请求异常状态:

<?php
$httpClient = (new \x\Client)->http();
$res = $httpClient->domain('https://www.sw-x.cn/api.html')
		->body(['name' => 'SW-X'])
		->post();

$statusCode = $httpClient->statusCode();

获取返回的Headers

headers方法,返回 HTTP 响应的头信息:

<?php
$httpClient = (new \x\Client)->http();
$res = $httpClient->domain('https://www.sw-x.cn/api.html')
		->body(['name' => 'SW-X'])
		->post();

$headers = $httpClient->headers();

获取返回的Cookies

cookies方法,返回 HTTP 响应的 cookie 内容:

<?php
$httpClient = (new \x\Client)->http();
$res = $httpClient->domain('https://www.sw-x.cn/api.html')
		->body(['name' => 'SW-X'])
		->post();

$cookies = $httpClient->cookies();

其他更多支持

HTTP客户端除了以上封装的组件支持以外,还支持原生Swoole-Client的一些格外操作,例如setHeaders()setCookies()set()等。
示例:

<?php
$httpClient = new \x\Client();
$res = $httpClient->http()
		->domain('https://www.sw-x.cn/api.html')
		->body(['name' => 'SW-X'])
		->set([
			'timeout' => 10, 
			'keep_alive' => false,
		])
		->setHeaders([
			'User-Agent' => 'Chrome/49.0.2587.3',
			'Accept' => 'text/html,application/xhtml+xml,application/xml',
		])
		->setCookies([
			'cookie' => 'Hm_lvt_5d9f29e57619d3dab924a9fb...'
		])
		->post();

更多的原生方法支持,请参考Swoole官方手册:HTTP客户端篇

数据库连接池说明

SW-X中的数据库操作主要使用连接池实现,启动Swoole服务的时候会根据/config/mysql.php中的配置项来初始化对应的PDO连接,每一次的Db请求,都会从这些连接中挑出一个来进行使用。
不过使用完成之后,记得调用return()方法归还连接,否则将会造成连接池为空的严重BUG。

读写分离

SW-X的数据库设计是强制读写分离的,我们在/config/mysql.php中可以看到三种不同的数据库配置,分别是:日志三大类。
如果你不需要使用读写分离,则只需要填入的配置即可。

SW-X中调用不同的数据库连接池,是通过实例化\x\Db类时声明的,具体如下:

$Db = \x\Db(); // 默认是调用写的连接池
$Db = \x\Db('select'); //调用读的连接池
$Db = \x\Db('log'); // 调用写日志的连接池    

SW-X的数据库连接池是可以配置多个不同的数据库配置项的。
假设我们在的连接池中配置了3个不同IP段的Mysql连接参数,那么在服务启动的时候会对array长度进行求余,平均生成PDO连接数。并不会随机生成。

使用方式

SW-X的数据库操作主要使用ORM的链式操作,为了面向熟悉MVC框架的PHPer,SW-X兼容了95%的ThinkPHP5.1数据库操作链。

数据库归还连接

我们在使用Db类时,实际上是从连接池中取出了一个连接,所以在我们处理完SQL业务之后,都应该调用$Db->return()方法,将连接归还到连接中。

这是一个必须遵守的规范。

临时切换数据库

v1.2.10版本起,支持临时切换数据库配置,创建一个PDO短链接实例,该连接与Mysql连接池无关,调用return方法会释放该连接。
创建案例如下:

$Db = \x\Db([
	'host'     => '127.0.0.1', // 地址
	'port'     => '3306', // 端口
	'user'     => 'root', // 用户名
	'password' => 'root', // 密码
	'database' => 'websocket', // 库
	'charset'  => 'utf8mb4', // 字符集	
]); // 传入配置创建临时PDO连接

name

name(表名):选择操作表。

SQL语句构造器,表前缀使用/config/mysql.php配置里的,prefix字段。
所以我们使用name()链的时候不需要带表前缀。其使用demo如下:

$Db = new \x\Db();
$Db->name('user');

test

v1.2.17版本起,支持为Db操作,标记单元测试用例标识,当前启动测试调试时,获取用例对应的数据值,代替Db操作返回值,防止污染DAO。
而正常流程时,test()声明不会影响正常流程解析。
标识字符串,只允许为英文字母开头,遵循声明变量的命名规范。
其使用demo如下:

$Db = new \x\Db();
$Db->name('user')->test('A1');

alias

alias(别名):用于设置当前数据表的别名,便于使用其他的连贯操作例如join方法等。
示例:

$Db = new \x\Db();
$Db->name('user')->alias('A');
// 当然你也可以这样写
$Db->name('user AS A');

最终生成的SQL语句类似于:FROM tp_user AS A;

where

where():用于构造SQL执行条件,该语法实现了三种场景支持,并没有完全实现ThinkPHP5.1的where语法。

场景一:多条件数组查询

$where = [
    ['id' , '<>' , 1],
    ['money', '>=', 100],
    ['name', 'like', '%小黄牛%'],
];
$Db = new \x\Db();
$Db->name('user')->where($where);

最终生成的SQL语句类似于:
FROM tp_user WHERE id <> 1 AND money >= 100 AND name like '%小黄牛%';
场景二:多条件多链查询

$where = [
    ['id' , '<>' , 1],
    ['money', '>=', 100],
    ['name', 'like', '%小黄牛%'],
];
$Db = new \x\Db();
$Db->name('user')->where('id' , '<>' , 1)->where('money', '>=', 100)->where($where);

最终生成的SQL语句类似于:
FROM tp_user WHERE id <> 1 AND money >= 100 AND name like '%小黄牛%';
场景三:便捷等于查询

$Db = new \x\Db();
$Db->name('user')->where('id' , 1)->where('name', '小黄牛');

最终生成的SQL语句类似于:
FROM tp_user WHERE id=1 AND name='小黄牛';

最后注意:where()链在一条SQL语句中是可以多次使用的,其执行顺序是先进先执行,相同的语句并不会覆盖,所以使用的时候需要自己注意下。

whereOr

whereOr()v1.2.16版本起开始支持。
主要用于配合where()方法实现Sql语句的OR条件语句构造。
whereOr()不支持数组的方式传递参数,不支持单独使用,不支持最左使用。
使用示例:

$Db = new \x\Db();
    $Db->name('user')->where('id' , 1)->whereOr('name', '小黄牛')->whereOr('name', '=', '小黄猪');

最终生成的SQL语句类似于:
FROM tp_user WHERE ((id=1) OR name="小黄牛" OR name="小黄猪");

最后注意:where()whereOr链在一条SQL语句中是可以多次使用的,其执行顺序是先进先执行,相同的语句并不会覆盖,所以使用的时候需要自己注意下。

whereIn

whereIn()v1.2.16版本起开始支持。
主要用于实现Sql语句的In包含查询语句构造。
使用示例:

$Db = new \x\Db();
    $Db->name('user')->where('id' , 1)->whereIn('pid', '(1,2,3,4)');

最终生成的SQL语句类似于:
FROM tp_user WHERE (id=1 AND pid IN (1,2,3,4));

whereNotIn

whereNotIn()v1.2.16版本起开始支持。
主要用于实现Sql语句的Not In不包含查询语句构造。
使用示例:

$Db = new \x\Db();
    $Db->name('user')->where('id' , 1)->whereNotIn('pid', '(1,2,3,4)');

最终生成的SQL语句类似于:
FROM tp_user WHERE (id=1 AND pid NOT IN (1,2,3,4));

field

field():方法主要作用是标识要返回或者操作的字段,可以用于查询和写入操作。
示例:

$Db = new \x\Db();
$Db->name('user')->field('id, name');

最终生成的SQL语句类似于: id,name FROM tp_user;
注意:如果不使用field()链执行查询操作,默认是*符号。

limit

limit():方法主要用于指定查询和操作的数量。
示例:

$Db = new \x\Db();
$Db->name('user')->limit(10, 20);

最终生成的SQL语句类似于: FROM tp_user limit 10,20;
也可以这样调用:

$Db = new \x\Db();
$Db->name('user')->limit(10);

最终生成的SQL语句类似于: FROM tp_user limit 10;

page

page():方法主要用于分页查询。最终生成的语法也是limit结构。
示例:

$Db = new \x\Db();
$Db->name('user')->page(1, 10);

最终生成的SQL语句类似于: FROM tp_user limit 0, 10;
之后的页数是这样:

$Db = new \x\Db();
$Db->name('user')->page(2, 10);

最终生成的SQL语句类似于: FROM tp_user limit 10, 10;

order

order():方法用于对操作的结果排序或者优先级限制。
示例:

$Db = new \x\Db();
$Db->name('user')->order('id DESC, money ASC');

最终生成的SQL语句类似于: FROM tp_user ORDER BY id DESC, money ASC;

having

having():方法用于配合group方法完成从分组的结果中筛选(通常是聚合条件)数据。
示例:

$Db = new \x\Db();
$Db->name('user')->having('count(id)>3');

最终生成的SQL语句类似于: FROM tp_user HAVING count(id)>3;

group

group():方法通常用于结合合计函数,根据一个或多个列对结果集进行分组 。
group()方法只有一个参数,并且只能使用字符串。
示例:

$Db = new \x\Db();
$Db->name('user')->group('id');

最终生成的SQL语句类似于: FROM tp_user GROUP BY id;

join

join():方法用于根据两个或多个表中的列之间的关系,从这些表中查询数据。join通常有下面几种类型,不同类型的join操作会影响返回的数。

INNER JOIN: 左右表有匹配,则返回组合行
LEFT JOIN: 即使右表中没有匹配,也从左表返回所有的行(默认的JOIN类型)
RIGHT JOIN: 即使左表中没有匹配,也从右表返回所有的行
FULL JOIN: 只要其中一个表中存在匹配,就返回行

同时注意:join语法中的表名不需要带表前缀,Class会自动读取配置文件中的前缀设置。

示例:

$Db = new \x\Db();
$Db->name('user')->alias('A')->join('user_data B', 'A.id=B.user_id');

最终生成的SQL语句类似于: FROM tp_user AS A LEFT JOIN tp_user_data AS B ON A.id=B.user_id;
我们也可以自己指定JOIN方式:

$Db = new \x\Db();
$Db->name('user')->alias('A')->join('user_data B', 'A.id=B.user_id', 'inner');

最终生成的SQL语句类似于: FROM tp_user AS A INNER JOIN tp_user_data AS B ON A.id=B.user_id;
注意:join链可以多个使用,顺序是先用先执行。

select

select():是链式操作的终结方法之一,该链支持Db类的所有查询表达式,主要用于查询多条记录。
该方法调用后会返回最终构造成的SQL语句。
示例:

$Db = new \x\Db();
$Db->name('user')->field('id')->where('id', 1)->order('id DESC')->select();

最终生成的SQL语句类似于:SELECR id FROM tp_user where id=1 ORDER BY id DESC;
同时select()还支持传入false表示不执行SQL语句,只返回最终构造的SQL语句字符串。

$Db = new \x\Db();
$Db->name('user')->select(false);

更多的链式组合可以自己尝试下。

find

find():是链式操作的终结方法之一,该链支持上述9种查询表达式,主要用于查询一条记录。主要不支持limitpage链。
该方法调用后会返回最终构造成的SQL语句。
示例:

$Db = new \x\Db();
$Db->name('user')->field('id')->where('id', 1)->order('id DESC')->find();

最终生成的SQL语句类似于:SELECR id FROM tp_user where id=1 ORDER BY id DESC limit 1;
同时find()还支持传入false表示不执行SQL语句,只返回最终构造的SQL语句字符串。

$Db = new \x\Db();
$Db->name('user')->find(false);

更多的链式组合可以自己尝试下。

value

value()v1.1.7版本起开始支持。
是链式操作的终结方法之一,该链支持上述9种查询表达式,主要用于查询一条记录的某个字段值。 主要不支持limitpage链。
该方法调用后会返回最终构造成的SQL语句。
示例:

$Db = new \x\Db();
$Db->name('user')->where('id', 1)->value('id');

最终生成的SQL语句类似于:SELECR id FROM tp_user where id=1 limit 1;
成功返回field对应的值,失败返回false

delete

delete():是链式操作的终结方法之一,用于构造删除语句,该链只支持主要的where查询表达式,不支持:orderpagelimit类型的表达式。
该方法调用后会返回最终构造成的SQL语句。

v1.2.2版本起,若该方法没有前置where条件,则会直接返回false不允许执行。 示例:

$Db = new \x\Db();
$Db->name('user')->where('id', 1)->order('id DESC')->delete();

最终生成的SQL语句类似于:DELETE FROM tp_user where id=1 ORDER BY id DESC;
更多的链式组合可以自己尝试下。

update

update():是链式操作的终结方法之一,用于构造单条记录更新语句,该链只支持主要的where查询表达式,不支持:orderpagelimit类型的表达式。
该方法调用后会返回最终构造成的SQL语句。

v1.2.2版本起,若该方法没有前置where条件,则会直接返回false不允许执行。 示例:

$Db = new \x\Db();
$Db->name('user')->where('id', 1)->update(['name' => '小黄牛', 'money' => 100]);

最终生成的SQL语句类似于:UPDATE tp_user SET name='小黄牛',money=100 where id=1;

insert

insert():是链式操作的终结方法之一,用于构造单条多条记录新增语句。
该方法调用后会返回最终构造成的SQL语句。
单条新增示例:

$Db = new \x\Db();
$Db->name('user')->insert(['name' => '小黄牛', 'money' => 100]);

最终生成的SQL语句类似于:INSERT INTO tp_user (name,money) VALUES ('小黄牛',100);
多条新增示例:

$data = [
    ['name' => '小蓝牛', 'money' => 50],
    ['name' => '小红牛', 'money' => 70],
    ['name' => '小黄牛', 'money' => 100],
];
$Db = new \x\Db();
$Db->name('user')->insert($data);

最终生成的SQL语句类似于:INSERT INTO tp_user (name,money) VALUES ('小蓝牛',50),('小红牛',70),('小黄牛',100);
注意:当使用批量新增时,所有的插入数据结构顺序需要与第一条数据顺序一致,否则将会出错,例如下拉语句就是错误的:

$data = [
    ['name' => '小蓝牛', 'money' => 50],
    ['money' => 70, 'name' => '小红牛'],
    ['money' => 100],
];
$Db = new \x\Db();
$Db->name('user')->insert($data);

insertGetId

insertGetId方法从v1.1.10版本起支持,起用法与insert方法一致,但不支持批量新增。
若新增成功则返回自增主键ID,起获取主键ID的语法为:SELECT LAST_INSERT_ID() as num;

setInc

setInc():是链式操作的终结方法之一,用于构造自增语句,该链只支持主要的where查询表达式,不支持:orderpagelimit类型的表达式。
该方法调用后会返回最终构造成的SQL语句。
示例:

$Db = new \x\Db();
$Db->name('user')->where('id', 1)->setInc('money', 20);

最终生成的SQL语句类似于:UPDATE tp_user SET money=money+20 where id=1;
如果我们不填自增数,默认会是1

$Db = new \x\Db();
$Db->name('user')->where('id', 1)->setInc('money');

最终生成的SQL语句类似于:UPDATE tp_user SET money=money+1 where id=1;

setDec

setDec():是链式操作的终结方法之一,用于构造自减语句,该链只支持主要的where查询表达式,不支持:orderpagelimit类型的表达式。
该方法调用后会返回最终构造成的SQL语句。
示例:

$Db = new \x\Db();
$Db->name('user')->where('id', 1)->setDec('money', 20);

最终生成的SQL语句类似于:UPDATE tp_user SET money=money-20 where id=1;
如果我们不填自减数,默认会是1

$Db = new \x\Db();
$Db->name('user')->where('id', 1)->setDec('money');

最终生成的SQL语句类似于:UPDATE tp_user SET money=money-1 where id=1;

count

count()v1.1.7版本起支持。
是链式操作的终结方法之一,用于统计数量,参数是要统计的字段名(可选)。支持基本的查询构造器。
示例:

$Db = new \x\Db();
$Db->name('user')->where('id', 1)->count();

最终生成的SQL语句类似于:SELECT COUNT(*) AS swoolex FROM `sw_user` WHERE id=1 LIMIT 1;
成功返回统计值,失败返回false

max

max()v1.1.7版本起支持。
是链式操作的终结方法之一,用于获取最大值,参数是要统计的字段名(必须)。支持基本的查询构造器。
示例:

$Db = new \x\Db();
$Db->name('user')->where('id', 1)->max('score');

最终生成的SQL语句类似于:SELECT MAX(*) AS swoolex FROM `sw_user` WHERE id=1 LIMIT 1;
成功返回统计值,失败返回false

min

min()v1.1.7版本起支持。
是链式操作的终结方法之一,用于获取最小值,参数是要统计的字段名(必须)。支持基本的查询构造器。
示例:

$Db = new \x\Db();
$Db->name('user')->where('id', 1)->min('score');

最终生成的SQL语句类似于:SELECT MIN(*) AS swoolex FROM `sw_user` WHERE id=1 LIMIT 1;
成功返回统计值,失败返回false

avg

avg()v1.1.7版本起支持。
是链式操作的终结方法之一,用于获取平均值,参数是要统计的字段名(必须)。支持基本的查询构造器。
示例:

$Db = new \x\Db();
$Db->name('user')->where('id', 1)->avg('score');

最终生成的SQL语句类似于:SELECT AVG(*) AS swoolex FROM `sw_user` WHERE id=1 LIMIT 1;
成功返回统计值,失败返回false

sum

sum()v1.1.7版本起支持。
是链式操作的终结方法之一,用于获取总分,参数是要统计的字段名(必须)。支持基本的查询构造器。
示例:

$Db = new \x\Db();
$Db->name('user')->where('id', 1)->sum('score');

最终生成的SQL语句类似于:SELECT SUM(*) AS swoolex FROM `sw_user` WHERE id=1 LIMIT 1;
成功返回统计值,失败返回false

执行原生SQL

SW-X中支持直接调用query()exec()方法执行原生的SQL语句,但其中的表名称并不能使用到配置文件中的表前缀配置项。

query()用于执行select语句
exec()用于执行除了select外的其他SQL语句


$Db = new \x\Db();
$Db->query('select * from tp_user;');

子查询构造器

SW-X中只推荐使用buildSql()方法搭配table()方法来构造子查询语句。buildSql()方法是不执行SQL语句,返回子查询结构语句:

$Db = new \x\Db();
$Db->name('user')->field('id')->where('id', 1)->order('id DESC')->buildSql();

最终生成的SQL语句类似于:( SELECR id FROM tp_user where id=1 ORDER BY id DESC )
再配合table()方法就能实现子查询操作:

$Db = new \x\Db();
$sql = $Db->name('user')->field('id')->where('id', 1)->order('id DESC')->buildSql();
$Db->table($sql.' AS A')->select();

生成的SQL语句为:SELECT * FROM ( SELECR id FROM tp_user where id=1 ORDER BY id DESC );

table

table()不会自动调用配置中的表前缀,该方法一般只用于配置子查询时查询的构造语句。

参考子查询构造器。

调试SQL

当我们不想要执行SQL语句,只想查看SQL语句字符串的时候,可以使用debug()方法,该方法是表示不执行SQL语句,并返回SQL字符串:

$Db = new \x\Db();
$Db->name('user')->field('id')->where('id', 1)->order('id DESC')->debug()->select();
// 等同于
$Db->name('user')->field('id')->where('id', 1)->order('id DESC')->select(false);
// 但debug语法一样适用于delete、insert、update、query语句

时间查询

whereTime()方法提供了日期和时间字段的快捷查询,示例如下:

$Db = new \x\Db();
// 大于某个时间
$Db->name('user')
    ->whereTime('create_time', '>=', '1970-10-1')
    ->select();
// 小于某个时间
$Db->name('user')
    ->whereTime('create_time', '<', '2000-10-1')
    ->select();
// 时间区间查询
$Db->name('user')
    ->whereTime('create_time', 'between', ['1970-10-1', '2000-10-1'])
    ->select();
// 不在某个时间区间
$Db->name('user')
    ->whereTime('create_time', 'not between', ['1970-10-1', '2000-10-1'])
    ->select();

同时,whereTime()方法还提供了更方便的时间表达式查询,例如::

$Db = new \x\Db();
// 获取今天的博客
$Db->name('blog')
    ->whereTime('create_time', 'today')
    ->select();
    
// 获取昨天的博客
$Db->name('blog')
    ->whereTime('create_time', 'yesterday')
    ->select();
    
// 获取本周的博客
$Db->name('blog')
    ->whereTime('create_time', 'week')
    ->select();   
    
// 获取上周的博客
$Db->name('blog')
    ->whereTime('create_time', 'last week')
    ->select();    
    
// 获取本月的博客
$Db->name('blog')
    ->whereTime('create_time', 'month')
    ->select();   
    
// 获取上月的博客
$Db->name('blog')
    ->whereTime('create_time', 'last month')
    ->select();      
    
// 获取今年的博客
$Db->name('blog')
    ->whereTime('create_time', 'year')
    ->select();    
    
// 获取去年的博客
$Db->name('blog')
    ->whereTime('create_time', 'last year')
    ->select();     

事务

$Db = new \x\Db();
// 启动事务
$Db->begin();
try {
    $Db->name('user')->find();
    $Db->name('user')->where('id', 1)->delete();
    // 提交事务
    $Db->commit();
} catch (\Exception $e) {
    // 回滚事务
    $Db->rollback();
}

事务隔离

在SW-X中,数据库事务不能跨连接池使用,也就是说在$Db中,不能包含$Db2的业务,否则会查询事务隔离级别的报错。

Model说明

Model支持从V1.1.3版本起,其实现主要以Db实例转发实现。
Model类建议存放在/app/model目录下,但不强制要求。
每个Model类,都必须继承\x\Model基类。
命名规则为:表名[不带前缀]Model.php。表名首字母大写。
注意,如果您的表名是由多个下划线_所组成,例如:user_action_log,那么Model名即为:UserActionLogModel.php
具体可以参考框架包下的示例代码:/app/model目录下。

注意:Model中禁止使用静态方法,防止Db连接池无法在实例释放后自动释放。

关于连接归还

在ORM操作中,我们new \x\Db后,当使用完连接池,则需要手动调用$this->Db->return()方法归还连接。
Model类中则不需要,当实例调用结束后,会自动调用$this->return()归还连接。

选择连接池类型

原来在new \x\Db()时,我们可以传入参数,例如select,调用读的连接池。
而在Model中也是一样,例如我们定义了一个/app/model/UserModel.php类,我们也可以跟Db一样,new \app\model\UserModel('select');即可。

配置说明

SW-X的配置架构没有系统跟应用配置之分,统一都是读取/config/目录下的所有文件。

同时,SW-X的配置全部都是二级配置,当没有指定配置读取的时候,表示获取所有配置项。

/config/下的文件名为一级配置项,其文件内部的数组内容,则为对应的二级配置。

如果需要自定义跟框架无关的配置,只需要在/config/目录下创建一个新文件即可。

配置读取

\x\Config配置类使用单例模式实现,在框架初始化的时候,会拉起全部配置并常驻内存。

\x\Config的读取和修改都需要通过run()先获取单例实例

例如获取app配置项的file参数:

<?php
\x\Config::run()->get('app.file');    
// 如果还需要获取三级配置项,可以这样使用
\x\Config::run()->get('app.file.size');  

get()方法支持无限极配置读取,只需要使用.符号间隔即可。

配置修改

配置的修改,使用set()方法实现。

例如修改app配置项的file参数:

<?php
\x\Config::run()->set('app.file', [
    // 最大上传大小(KB)
    'size' => 15678,
    // 允许上传路径
    'ext' => 'jpg,jpeg,png,gif',
    // 保存目录不存在是否自动创建
    'auto_save' => true,
    // 文件名生成算法,支持sha1,md5,time三种
    'name_algorithm' => 'time',
    // 文件默认保存目录
    'path' => ROOT_PATH.'/upload/',
]);    

// 如果只需要修改三级配置项,可以这样使用
\x\Config::run()->set('app.file.size', 15678);  

set()方法支持无限极配置修改,只需要使用.符号间隔即可。
同时注意:由于配置项是缓存到常驻内存中的,所以配置项的修改不是局部,而是全局生效的,这点需要注意。

Redis

\x\Redis类使用连接池实现,对应的配置在/config/redis.php配置项中修改。

获取Redis连接池,只需要new \x\Redis();即可,不过跟Mysql连接池一样,当使用完后,需要调用return()归还连接。

具体使用案例如下:

<?php
// 获取连接
$redis = \x\Redis();
// 执行指令
$redis->set('name', '小黄牛');
$redis->get('name');
// 归还连接
$redis->return();

Session说明

SW-X的Session实现,主要依赖Redis存储,不支持原生的PHP-SESSION,所以在使用SESSION之前,请先到/config/redis.php配置项中开始redis支持。

Session的配置项可以在/config/app.php配置中进行修改。

has

SESSION的操作,主要依赖\x\Session类实现,has()方法用于判断一个session是否存在。

<?php
if (\x\Session::has('admin')) {
    echo '登陆中';
} else {
    echo '已退出';
}

get

get()方法用于读取一个session值。

<?php
$admin = \x\Session::get('admin');

set

set()方法用于设置一个session值。

<?php
\x\Session::set('admin', '小黄牛');
// 你还可以设置自动过期时间,默认7200S
\x\Session::set('admin', '小黄牛', 3600);

delete

delete()方法用于删除一个session。

<?php
$res = \x\Session::delete('admin');

clear

clear()方法用于清空所有session。

<?php
$res = \x\Session::clear();

Cookies

Cookie的配置项可以在/config/app.php配置中进行修改。

has

Cookie的操作,主要依赖\x\Cookie类实现,has()方法用于判断一个cookie是否存在。

<?php
if (\x\Cookie::has('admin')) {
    echo '登陆中';
} else {
    echo '已退出';
}

get

get()方法用于读取一个cookie值。

<?php
$admin = \x\Cookie::get('admin');

set

set()方法用于设置一个cookie值。

<?php
\x\Cookie::set('admin', '小黄牛');
// 你还可以设置自动过期时间,默认7200S
\x\Cookie::set('admin', '小黄牛', 3600);

delete

delete()方法用于删除一个cookie。

<?php
$res = \x\Cookie::delete('admin');

clear

clear()方法用于清空所有cookie。

<?php
$res = \x\Cookie::clear();

多语言

多语言的实现,依赖于\x\Lang类,该类由单例模式设计,统一使用\x\Lang::run()->get();的方式获取语言项。

语言包统一存放在/lang/目录下,以文件名为语言包名称进行命名。

对于的语言包启用设置,在/config/app.php文件中进行配置。

如果你不想修改配置文件,也可以通过传入run()参数进行临时获取,例如:

<?php
// 读取默认语言包
\x\Lang::run()->get('hello word sw-x');

// 临时读取en语言包中的项
\x\Lang::run('en')->get('hello word sw-x');

dd

系统函数:dd(),主要是用于打印数据类型,由于swoole中,直接var_dump是没办法输出到页面上的,所以系统提供了这个函数由于代替var_dump,具体用法如下:

<?php
// 假设下面是HTTP控制器中
return $this->fetch(dd([
    'name' => 'SW-X',
    'des' => '真帅!'
]));

系统常量

VERSION SW-X版本号
ROOT_PATH 项目根地址,末尾不带/符号
RUNTIME_PATH 日志类文件存放根地址,末尾带/符号