lmz / thefair-skeleton
A coroutine framework that focuses on hyperspeed and flexible, specifically use for build microservices and middlewares.
Requires
- php: >=7.4
- ext-swoole: >=4.6
- thefair/thefairlib: ~4.4.0
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.0
- hyperf/devtool: ~2.2.0
- hyperf/testing: ~2.2.0
- hyperf/watcher: ^2.2
- mockery/mockery: ^1.0
- phpstan/phpstan: ^0.12
- swoole/ide-helper: ^4.5
Suggests
- ext-json: Required to use JSON.
- ext-openssl: Required to use HTTPS.
- ext-pdo: Required to use MySQL Client.
- ext-pdo_mysql: Required to use MySQL Client.
- ext-redis: Required to use Redis Client.
README
[TOC]
安装
仅可运行于 Linux 和 Mac 环境下,Windows 下也可以通过 Docker for Windows 来作为运行环境或虚拟机,通常来说 Mac 环境下,推荐本地环境部署
- PHP >= 7.2
- Swoole PHP 扩展 >= 4.4,并关闭了 Short Name
- OpenSSL PHP 扩展
- JSON PHP 扩展
- PDO PHP 扩展 (如需要使用到 MySQL 客户端)
- Redis PHP 扩展 (如需要使用到 Redis 客户端)
- Protobuf PHP 扩展 (如需要使用到 gRPC 服务端或客户端)
composer create-project lmz/thefair-skeleton test_service
本地配置文件 .env
安装包 composer up
启动服务开发 php dev_start.php -c
项目文件结构
├── app
│ ├── Constants // 常量约定
│ ├── Contract // 接口
│ ├── Controller // 控制器
│ ├── Event // 事件
│ ├── Exception // 异常处理
│ │ └── Handler // Error 监听器
│ ├── Job // 异步任务处理
│ ├── Library // 系统自定义库
│ ├── Middleware // 中间件
│ ├── Model // 数据模型,只处理数据库、缓存相关
│ ├── Process // 多进程管理
│ ├── Request // 参数约定
│ ├── Server // 系统核心服务
│ └── Service // 业务处理
├── listener // 事件监听器
├── bin
│ └── hyperf.php // 架构启动文件
├── composer.json
├── composer.lock
├── config // 配置文件
├── docs // 文档
│ └── sql
├── openapi.yaml // 自动生成文档
├── phpunit.xml // 单元测试
├── runtime // 日志目录
│ ├── container
│ ├── hyperf.pid
│ └── logs
├── test // 测试
│ ├── Cases
│ ├── HttpTestCase.php
│ └── bootstrap.php
├── Jenkinsfile // 自动化测试
├── README.md // 说明文档
├── .env // 开发配置
├── .env.example // demo 开发配置文件
├── .editorconfig // 编辑器参数约定
├── README.md // 说明文档
└── watch.php // 本地开发热更新文件
配置文件结构
config
├── autoload // 此文件夹内的配置文件会被配置组件自己加载,并以文件夹内的文件名作为第一个键值
│ ├── amqp.php // 用于管理 AMQP 组件
│ ├── annotations.php // 用于管理注解
│ ├── aspects.php // 用于管理 AOP 切面
│ ├── auth // 系统白单名
│ ├── async_queue.php // 用于管理基于 Redis 实现的简易队列服务
│ ├── cache.php // 用于管理缓存组件
│ ├── commands.php // 用于管理自定义命令
│ ├── consul.php // 用于管理 Consul 客户端
│ ├── databases.php // 用于管理数据库客户端
│ ├── devtool.php // 用于管理开发者工具
│ ├── exceptions.php // 用于管理异常处理器
│ ├── listeners.php // 用于管理事件监听者
│ ├── lock.php // 分布式锁
│ ├── logger.php // 用于管理日志
│ ├── middlewares.php // 用于管理中间件
│ ├── opentracing.php // 用于管理调用链追踪
│ ├── processes.php // 用于管理自定义进程
│ ├── redis.php // 服务限流
│ ├── rate_limit.php // 用于管理 Redis 客户端
│ ├── translation.php // 国际化配置
│ ├── validation.php // 参数自动验证
│ ├── rate_limit.php // 用于管理 Redis 客户端
│ └── server.php // 用于管理 Server 服务
├── config.php // 用于管理用户或框架的配置,如配置相对独立亦可放于 autoload 文件夹内
├── container.php // 负责容器的初始化,作为一个配置文件运行并最终返回一个 Psr\Container\ContainerInterface 对象
├── dependencies.php // 用于管理 DI 的依赖关系和类对应关系
│ ├── i18n // 国际化具体信息
│ │ └── languages
└── routes.php // 用于管理路由
开发必读
不能通过全局变量获取属性参数
在 PHP-FPM
下可以通过全局变量获取到请求的参数,服务器的参数等,在 Hyperf
和 Swoole
内,都 无法 通过 $_GET/$_POST/$_REQUEST/$_SESSION/$_COOKIE/$_SERVER
等$_
开头的变量获取到任何属性参数。
通过容器获取的类都是单例
通过依赖注入容器获取的都是进程内持久化的,是多个协程共享的,所以不能包含任何的请求唯一的数据或协程唯一的数据,这类型的数据都通过协程上下文去处理,具体请仔细阅读 依赖注入 和 协程 章节。
框架生命周期
Hyperf
是运行于 Swoole
之上的,想要理解透彻 Hyperf
的生命周期,那么理解 Swoole
的生命周期也至关重要。
Hyperf
的命令管理默认由 symfony/console
提供支持(如果您希望更换该组件您也可以通过改变 skeleton
的入口文件更换成您希望使用的组件),在执行 php bin/hyperf.php start
后,将由 Hyperf\Server\Command\StartServer
命令类接管,并根据配置文件 config/autoload/server.php
内定义的 Server
逐个启动。
关于依赖注入容器的初始化工作,我们并没有由组件来实现,因为一旦交由组件来实现,这个耦合就会非常的明显,所以在默认的情况下,是由入口文件来加载 config/container.php
来实现的。
禁止注入 model,实现单例方法
可以使用 UserInfoModel::xxxx
方法,或make
,new
请求与协程生命周期
Swoole
在处理每个连接时,会默认创建一个协程去处理,主要体现在 onRequest、onReceive、onConnect
事件,所以可以理解为每个请求都是一个协程,由于创建协程也是个常规操作,所以一个请求协程里面可能会包含很多个协程,同一个进程内协程之间是内存共享的,但调度顺序是非顺序的,且协程间本质上是相互独立的没有父子关系,所以对每个协程的状态处理都需要通过 协程上下文 来管理。
路由
路由必须是三级,/m/c/a
http 服务使用 @AutoController
Rpc 服务使用 @RpcService
如 * @RpcService(name="v2/test", protocol="jsonrpc-tcp-length-check", server="json-rpc")
参数过滤
路由 /v2/test/get_test
对应 \App\Controller\V2\Test::getTest
方法
必须新建一个对应的 app/Request/V2/Test/GetTest.php
文件
生成命令:php bin/hyperf.php gen:request V2/Test/GetTest
public function getTest() { $uid = input('uid'); $name = input('name'); $fields = input('fields', []); return $this->showResult([ $uid, $fields, $name, ]); }
input
可以获得GET|POST
参数,相当于$_REQUEST
app/Request/V2/Test/GetTest.php 源码
<?php declare(strict_types=1); namespace App\Request\V2\Test; use App\Request\BaseRequest; class GetTest extends BaseRequest { /** * Get the validation rules that apply to the request. */ public function rules(): array { return [ 'uid' => 'required|integer|get|post', 'name' => 'required|str', 'fields' => 'array', 'ignore_cache' => 'boolean', ]; } /** * 获取已定义验证规则的错误消息 */ public function messages(): array { return [ 'uid.required' => ':attribute 不能为空', 'uid.integer' => ':attribute 必须为整型', 'fields.array' => ':attribute 必须为数组', 'ignore_cache.boolean' => ':attribute 必须为 true 或 false', ]; } }
新增验证规则
mobile 国内手机号验证
'phone' => 'required|mobile'
str 对字符串进行编码
'phone' => 'required|str'
Model 模型
约束:只做数据库、缓存操作,不写业务逻辑,业务推荐写在 service 里面
分表自动创建模型
php bin/hyperf.php gen:dataModel user_info
<?php declare (strict_types=1); namespace App\Model\User; use TheFairLib\Model\DataModel; /** * @property int $uid 用户id,用于做sharding * @property string $username 用户名 * @property string $country_code 国家编码 * @property string $country 注册国家 * @property int $nationality 国籍 * @property string $nick 昵称 * @property string $sex 用户性别 * @property string $mobile 用户手机号(加密) * @property string $password 用户密码 * @property string $avatar 头像地址 * @property string $state 用户状态 * @property string $source 注册来源 * @property string $app_source 注册应用æ�¥æº� * @property string $self_desc 自我描述 * @property string $auth_desc 认证描述 * @property string $geek_desc 达人描述 * @property string $salt 加密salt * @property string $last_visit_time 最后访问时间 * @property string $last_visit_ip * @property string $reg_ip * @property string $ctime 创建时间 * @property string $utime 更新时间 */ class UserInfo extends DataModel { /** * The table associated with the model. * * @var string */ protected $table = 'user_info'; /** * The connection name for the model. * * @var string */ protected $connection = 'thefair_user'; /** * sharding num * * @var int */ protected $shardingNum = 20; /** * The attributes that are mass assignable. * * @var array */ protected $fillable = ['uid', 'username', 'country_code', 'country', 'nationality', 'nick', 'sex', 'mobile', 'password', 'avatar', 'state', 'source', 'app_source', 'self_desc', 'auth_desc', 'geek_desc', 'salt', 'last_visit_time', 'last_visit_ip', 'reg_ip', 'ctime', 'utime']; /** * The attributes that should be cast to native types. * * @var array */ protected $casts = ['uid' => 'integer', 'nationality' => 'integer']; }
shardingId 方法
model 类中重写表名,原方法$this->db()->table($this->_getTableName($uid))
public function getUserDevice($uid, $deviceId) { return (array)self::shardingId($uid)->where([ 'uid' => $uid, 'device_id' => $deviceId, ])->first(); }
全局使用方式
UserInfo::shardingId($uid)->where([ 'uid' => $uid, 'device_id' => $deviceId, ])->first();
sql 注入
demo 用例
/** * 原生 sql 测试 * * @return array * @throws ConnectionException */ public function rawSql() { $uid = "1 or 1=1"; return $this->db()->select("select * from thefair_user_info_0 where uid = {$uid}"); }
上面这条 sql 可以使用参数过滤做来,如强转 int
/** * 原生 sql 测试 * * @return array * @throws ConnectionException */ public function rawSql() { $nick = "1 or 1=1"; return $this->db()->select("select * from thefair_user_info_0 where nick = $nick"); }
上面这条 sql 传入的是一个字符串,如果用参数过滤很容易误杀,必须使用预处理查询
系统底层会监控 sql 语句,发现原生 sql 语法,自动报警
异常
普通异常
throw new ServiceException('用户已经注册', ['uid' => $checkUid, 'third_party_uid' => hideStr($thirdPartyUid), 'mobile' => hideStr($mobile)]); { "code": 40001, "message": { "text": "用户已经注册", "action": "toast" }, "result": { "uid": 1111, "third_party_uid": "933**************************8c1", "mobile": "186*****263", "exception": "App\\Exception\\ServiceException" } }
code 异常
throw new BusinessException(ErrorCode::CODE_RATE_LIMIT, ['host' => getServerLocalIp()]); { "code": 50003, "message": { "text": "192*******.43 服务器超时, 请稍后再试", "action": "toast" }, "result": { "exception": "App\\Exception\\BusinessException" } }
empty 异常
throw new EmptyException('数据为空', ['uid' => 1]); { "code": 40001, "message": { "text": "数据为空", "action": "toast" }, "result": { "uid": 1, "exception": "App\\Exception\\EmptyException" } }
新 api 访问 rpc 服务
$data = \TheFairLib\Service\JsonRpc\RpcClient\Client::Instance('thefair_service')->call('/v2/test/get_test', [ ]);
hyperf service 服务之间的访问
配置文件 config/autoload/services.php
<?php declare(strict_types=1); return [ 'consumers' => [ [ 'name' => 'v2/test', 'service' => '', 'protocol' => 'jsonrpc-tcp-length-check', 'load_balancer' => 'random', 'nodes' => [ [ 'host' => '192.168.0.249', 'port' => 2301, ], ], 'app_key' => 'xxx', 'app_secret' => 'xxx1111', // 配置项,会影响到 Packer 和 Transporter 'options' => [ 'connect_timeout' => 5.0, 'recv_timeout' => 5.0, 'settings' => [ // 根据协议不同,区分配置 'open_length_check' => true, 'package_length_type' => 'N', 'package_length_offset' => 0, 'package_body_offset' => 4, 'package_max_length' => 1024 * 1024 * 2, ], // 当使用 JsonRpcPoolTransporter 时会用到以下配置 'pool' => [ 'min_connections' => 1, 'max_connections' => 32, 'connect_timeout' => 10.0, 'wait_timeout' => 3.0, 'heartbeat' => -1, 'max_idle_time' => 60.0, ], ], ], ], ];
新建 RpcClient
<?php namespace App\Rpc; class Test extends \TheFairLib\Server\Client\JsonRpcClient { /** * 定义对应服务提供者的服务名称 * @var string */ protected $serviceName = 'v2/test'; }
访问
make(Test::class)->call('get_test')
线上服务启动
systemd 管理
用于 centos 7
新建:/etc/systemd/system/user.service
[Unit] Description=user service Http Server After=network.target After=syslog.target [Service] Type=simple LimitNOFILE=65535 ExecStart=/usr/bin/php /home/thefair/www/user_service/bin/hyperf.php start ExecReload=/bin/kill -USR1 $MAINPID Restart=always [Install] WantedBy=multi-user.target graphical.target
sudo systemctl --system daemon-reload
#启动服务 sudo systemctl start user.service #reload服务 sudo systemctl reload user.service #关闭服务 sudo systemctl stop user.service sudo systemctl status user.service
supervisorctl 管理项目
编辑新复制出来的配置文件 /etc/supervisord.d/supervisord.conf
,并在文件结尾处添加以下内容后保存文件:
# 新建一个应用并设置一个名称,这里设置为 user_service [program:user_service] # 设置命令在指定的目录内执行 directory=/home/thefair/www/user_service/ # 这里为您要管理的项目的启动命令 command=php ./bin/hyperf.php start # 以哪个用户来运行该进程 user=root # supervisor 启动时自动该应用 autostart=true # 进程退出后自动重启进程 autorestart=true # 进程持续运行多久才认为是启动成功 startsecs=1 # 重试次数 startretries=3 # stderr 日志输出位置 stderr_logfile=/home/thefair/www/user_service/runtime/stderr.log # stdout 日志输出位置 stdout_logfile=/home/thefair/www/user_service/runtime/stdout.log
启动 Supervisor
supervisord -c /etc/supervisord.d/supervisord.conf
使用 supervisorctl 管理项目
# 启动 user_service 应用 supervisorctl start user_service # 重启 user_service 应用 supervisorctl restart user_service # 停止 user_service 应用 supervisorctl stop user_service # 查看所有被管理项目运行状态 supervisorctl status # 重新加载配置文件 supervisorctl update # 重新启动所有程序 supervisorctl reload
单元测试
composer test